From 3c0b914567fdc00a576cae81617c46a9b4125ed7 Mon Sep 17 00:00:00 2001 From: James Hunter Date: Fri, 21 Nov 2025 16:57:55 -0500 Subject: [PATCH 1/2] Move doc gen to archive. Start new template for Content Gen --- README.md | 30 +-- .../.azdo}/pipelines/azure-dev.yml | 0 .../.devcontainer}/devcontainer.json | 0 .../.devcontainer}/setup_env.sh | 0 .flake8 => archive-doc-gen/.flake8 | 0 archive-doc-gen/.gitattributes | 3 + .../.github}/CODEOWNERS | 0 .../.github}/ISSUE_TEMPLATE/bug_report.md | 0 .../ISSUE_TEMPLATE/feature_request.md | 0 .../.github}/ISSUE_TEMPLATE/subtask.md | 0 .../.github}/dependabot.yml | 0 .../.github}/pull_request_template.md | 0 .../Scheduled-Dependabot-PRs-Auto-Merge.yml | 0 .../.github}/workflows/azure-dev.yml | 0 .../workflows/broken-links-checker.yml | 0 .../.github}/workflows/create-release.yml | 128 +++++----- .../.github}/workflows/deploy-v2.yml | 0 .../.github}/workflows/deploy.yml | 0 .../workflows/docker-build-and-push.yml | 0 .../.github}/workflows/node.js.yml | 0 .../.github}/workflows/pr-title-checker.yml | 0 .../.github}/workflows/pylint.yml | 0 .../.github}/workflows/python-app.yml | 0 .../.github}/workflows/stale-bot.yml | 0 .../workflows/telemetry-template-check.yml | 0 .../.github}/workflows/test-automation.yml | 0 .../.github}/workflows/tests.yml | 0 archive-doc-gen/.gitignore | 15 ++ .../.vscode}/launch.json | 0 .../.vscode}/settings.json | 0 archive-doc-gen/CODE_OF_CONDUCT.md | 9 + archive-doc-gen/CONTRIBUTING.md | 14 ++ archive-doc-gen/LICENSE | 21 ++ archive-doc-gen/README.md | 225 ++++++++++++++++++ archive-doc-gen/SECURITY.md | 41 ++++ .../app-azure.yaml | 0 azure.yaml => archive-doc-gen/azure.yaml | 0 .../docs}/ACRBuildAndPushGuide.md | 0 .../docs}/AppAuthentication.md | 0 .../docs}/AzureAccountSetUp.md | 0 .../docs}/AzureGPTQuotaSettings.md | 0 .../docs}/AzureSemanticSearchRegion.md | 0 .../docs}/CustomizingAzdParameters.md | 0 .../docs}/DeleteResourceGroup.md | 0 .../docs}/DeploymentGuide.md | 0 .../docs}/PowershellSetup.md | 0 {docs => archive-doc-gen/docs}/QuotaCheck.md | 0 .../docs}/README_LOCAL.md | 0 .../docs}/SampleQuestions.md | 0 .../docs}/TRANSPARENCY_FAQ.md | 0 .../docs}/TroubleShootingSteps.md | 0 .../docs}/container_registry_migration.md | 0 .../docs}/create_new_app_registration.md | 0 .../docs}/images/AddDetails.png | Bin .../docs}/images/AddPlatform.png | Bin .../docs}/images/AddRedirectURL.png | Bin .../docs}/images/AppAuthIdentityProvider.png | Bin .../images/AppAuthIdentityProviderAdd.png | Bin .../images/AppAuthIdentityProviderAdded.png | Bin .../docs}/images/AppAuthentication.png | Bin .../images/AppAuthenticationIdentity.png | Bin .../docs}/images/AppServiceContainer.png | Bin .../docs}/images/Appregistrations.png | Bin .../docs}/images/Archimage.png | Bin .../docs}/images/AzureHomePage.png | Bin .../docs}/images/ContainerApp.png | Bin .../docs}/images/DeleteRG.png | Bin .../DocGen_Azure_AI_Foundry_Architecture.png | Bin .../docs}/images/GenerateDraft.png | Bin .../docs}/images/MicrosoftEntraID.png | Bin .../docs}/images/NewRegistration.png | Bin {docs => archive-doc-gen/docs}/images/Web.png | Bin .../docs}/images/WebAppURL.png | Bin .../docs}/images/architecture.png | Bin .../docs}/images/customerTruth.png | Bin .../docs}/images/deleteservices.png | Bin .../docs}/images/deployment_center.png | Bin .../docs}/images/git_bash.png | Bin .../docs}/images/keyfeatures.png | Bin .../docs}/images/landing_page.png | Bin .../docs}/images/logAnalytics.png | Bin .../docs}/images/logAnalyticsJson.png | Bin .../docs}/images/logAnalyticsList.png | Bin .../docs}/images/oneClickDeploy.png | Bin .../docs}/images/quota-check-output.png | Bin .../azure_ai_foundry_list.png | Bin .../navigate_to_projects.png | Bin .../project_resource_id.png | Bin .../docs}/images/re_use_log/logAnalytics.png | Bin .../images/re_use_log/logAnalyticsJson.png | Bin .../images/re_use_log/logAnalyticsList.png | Bin .../docs}/images/readme/business-scenario.png | Bin .../docs}/images/readme/quick-deploy.png | Bin .../docs}/images/readme/solution-overview.png | Bin .../readme/supporting-documentation.png | Bin .../docs}/images/resource-groups.png | Bin .../docs}/images/resource_menu.png | Bin .../docs}/images/resourcegroup.png | Bin .../docs}/images/resourcegroup1.png | Bin .../docs}/images/supportingDocuments.png | Bin .../docs}/images/userStory.png | Bin .../docs}/re-use-foundry-project.md | 0 .../docs}/re-use-log-analytics.md | 0 .../infra}/data/pdfdata.zip | Bin {infra => archive-doc-gen/infra}/main.bicep | 0 {infra => archive-doc-gen/infra}/main.json | 0 .../infra}/main.parameters.json | 0 .../infra}/main.waf.parameters.json | 0 .../infra}/modules/ai-project.bicep | 0 .../modules/ai-services-deployments.bicep | 0 .../deploy_aifp_aisearch_connection.bicep | 0 .../infra}/modules/role-assignment.bicep | 0 .../infra}/modules/virtualNetwork.bicep | 0 .../infra}/modules/web-sites.bicep | 0 .../infra}/modules/web-sites.config.bicep | 0 .../infra}/scripts/add_cosmosdb_access.sh | 0 .../infra}/scripts/copy_kb_files.sh | 0 .../index_scripts/01_create_search_index.py | 0 .../scripts/index_scripts/02_process_data.py | 0 .../scripts/index_scripts/requirements.txt | 0 .../infra}/scripts/process_sample_data.sh | 0 .../scripts/run_create_index_scripts.sh | 0 .../infra}/vscode_web/.gitignore | 0 .../infra}/vscode_web/LICENSE | 0 .../infra}/vscode_web/README-noazd.md | 0 .../infra}/vscode_web/README.md | 0 .../infra}/vscode_web/codeSample.py | 0 .../vscode_web/endpoint-requirements.txt | 0 .../infra}/vscode_web/endpointCodeSample.py | 0 .../infra}/vscode_web/index.json | 0 .../infra}/vscode_web/install.sh | 0 .../infra}/vscode_web/requirements.txt | 0 .../package-lock.json | 0 .../scripts}/SAMPLE_DATA.md | 0 .../scripts}/auth_init.ps1 | 0 .../scripts}/auth_init.py | 0 .../scripts}/auth_init.sh | 0 .../scripts}/auth_update.ps1 | 0 .../scripts}/auth_update.py | 0 .../scripts}/auth_update.sh | 0 .../scripts}/checkquota.sh | 0 .../scripts}/chunk_documents.py | 0 .../scripts}/config.json | 0 .../scripts}/data_preparation.py | 0 .../scripts}/data_utils.py | 0 .../scripts}/embed_documents.py | 0 .../scripts}/loadenv.ps1 | 0 .../scripts}/loadenv.sh | 0 .../scripts}/prepdocs.ps1 | 0 .../scripts}/prepdocs.py | 0 .../scripts}/prepdocs.sh | 0 .../scripts}/quota_check_params.sh | 0 .../scripts}/readme.md | 0 .../scripts}/role_assignment.sh | 0 {src => archive-doc-gen/src}/.dockerignore | 0 {src => archive-doc-gen/src}/.env.sample | 0 {src => archive-doc-gen/src}/.gitignore | 0 {src => archive-doc-gen/src}/SUPPORT.md | 0 .../src}/TEST_CASE_FLOWS.md | 0 .../src}/WebApp.Dockerfile | 0 {src => archive-doc-gen/src}/app.py | 0 .../src}/backend/__init__.py | 0 .../backend/api/agent/agent_factory_base.py | 0 .../backend/api/agent/browse_agent_factory.py | 0 .../api/agent/section_agent_factory.py | 0 .../api/agent/template_agent_factory.py | 0 .../src}/backend/auth/__init__.py | 0 .../src}/backend/auth/auth_utils.py | 0 .../src}/backend/auth/sample_user.py | 0 .../backend/helpers/azure_credential_utils.py | 0 .../src}/backend/history/cosmosdbservice.py | 0 .../src}/backend/security/__init__.py | 0 .../backend/security/ms_defender_utils.py | 0 .../src}/backend/settings.py | 0 {src => archive-doc-gen/src}/backend/utils.py | 0 {src => archive-doc-gen/src}/event_utils.py | 0 .../src}/frontend/.eslintignore | 0 .../src}/frontend/.eslintrc.json | 0 .../src}/frontend/.prettierignore | 0 .../src}/frontend/.prettierrc.json | 0 .../src}/frontend/__mocks__/dompurify.ts | 0 .../src}/frontend/__mocks__/fileMock.ts | 0 .../src}/frontend/__mocks__/mockAPIData.ts | 0 .../frontend/__mocks__/react-markdown.tsx | 0 .../src}/frontend/eslint.config.ts | 0 .../src}/frontend/index.html | 0 .../src}/frontend/jest.config.ts | 0 .../src}/frontend/jest.polyfills.js | 0 .../src}/frontend/package-lock.json | 0 .../src}/frontend/package.json | 0 .../src}/frontend/polyfills.js | 0 .../src}/frontend/public/favicon.ico | Bin .../src}/frontend/src/api/api.ts | 0 .../src}/frontend/src/api/index.ts | 0 .../src}/frontend/src/api/models.ts | 0 .../src}/frontend/src/assets/Azure.svg | 0 .../src}/frontend/src/assets/ClearChat.svg | 0 .../src}/frontend/src/assets/Contoso.svg | 0 .../src}/frontend/src/assets/Generate.svg | 0 .../src}/frontend/src/assets/Send.svg | 0 .../src/components/Answer/Answer.module.css | 0 .../src/components/Answer/Answer.test.tsx | 0 .../frontend/src/components/Answer/Answer.tsx | 0 .../components/Answer/AnswerParser.test.ts | 0 .../src/components/Answer/AnswerParser.tsx | 0 .../frontend/src/components/Answer/index.ts | 0 .../ChatHistory/ChatHistoryList.test.tsx | 0 .../ChatHistory/ChatHistoryList.tsx | 0 .../ChatHistory/ChatHistoryListItem.tsx | 0 .../ChatHistory/ChatHistoryPanel.module.css | 0 .../ChatHistory/ChatHistoryPanel.test.tsx | 0 .../ChatHistory/ChatHistoryPanel.tsx | 0 .../ChatHistory/chatHistoryListItem.test.tsx | 0 .../DraftCards/SectionCard.test.tsx | 0 .../src/components/DraftCards/SectionCard.tsx | 0 .../components/DraftCards/TitleCard.test.tsx | 0 .../src/components/DraftCards/TitleCard.tsx | 0 .../FeatureCard/FeatureCard.test.tsx | 0 .../components/FeatureCard/FeatureCard.tsx | 0 .../QuestionInput/QuestionInput.module.css | 0 .../QuestionInput/QuestionInput.test.tsx | 0 .../QuestionInput/QuestionInput.tsx | 0 .../src/components/QuestionInput/index.ts | 0 .../src/components/Sidebar/Sidebar.module.css | 0 .../src/components/Sidebar/Sidebar.test.tsx | 0 .../src/components/Sidebar/Sidebar.tsx | 0 .../src/components/common/Button.module.css | 0 .../src/components/common/Button.test.tsx | 0 .../frontend/src/components/common/Button.tsx | 0 .../src/constants/chatHistory.test.tsx | 0 .../frontend/src/constants/chatHistory.tsx | 0 .../frontend/src/constants/xssAllowTags.ts | 0 .../src}/frontend/src/helpers/helpers.ts | 0 .../src}/frontend/src/index.css | 0 .../src}/frontend/src/index.tsx | 0 .../src}/frontend/src/pages/NoPage.tsx | 0 .../frontend/src/pages/chat/Chat.module.css | 0 .../frontend/src/pages/chat/Chat.test.tsx | 0 .../src}/frontend/src/pages/chat/Chat.tsx | 0 .../chat/Components/AuthNotConfigure.test.tsx | 0 .../chat/Components/AuthNotConfigure.tsx | 0 .../Components/ChatMessageContainer.test.tsx | 0 .../chat/Components/ChatMessageContainer.tsx | 0 .../chat/Components/CitationPanel.test.tsx | 0 .../pages/chat/Components/CitationPanel.tsx | 0 .../src/pages/document/Document.module.css | 0 .../src/pages/document/Document.test.tsx | 0 .../frontend/src/pages/document/Document.tsx | 0 .../frontend/src/pages/draft/Draft.module.css | 0 .../frontend/src/pages/draft/Draft.test.tsx | 0 .../src}/frontend/src/pages/draft/Draft.tsx | 0 .../src/pages/landing/Landing.module.css | 0 .../src/pages/landing/Landing.test.tsx | 0 .../frontend/src/pages/landing/Landing.tsx | 0 .../src/pages/layout/Layout.module.css | 0 .../frontend/src/pages/layout/Layout.test.tsx | 0 .../src}/frontend/src/pages/layout/Layout.tsx | 0 .../src}/frontend/src/state/AppProvider.tsx | 0 .../src}/frontend/src/state/AppReducer.tsx | 0 .../src}/frontend/src/test/setupTests.ts | 0 .../src}/frontend/src/test/test.utils.tsx | 0 .../src}/frontend/src/vite-env.d.ts | 0 .../src}/frontend/tsconfig.json | 0 .../src}/frontend/tsconfig.node.json | 0 .../src}/frontend/vite.config.ts | 0 {src => archive-doc-gen/src}/gunicorn.conf.py | 0 .../src}/requirements-dev.txt | 0 {src => archive-doc-gen/src}/requirements.txt | 0 {src => archive-doc-gen/src}/start.cmd | 0 {src => archive-doc-gen/src}/start.sh | 0 {src => archive-doc-gen/src}/test.cmd | 0 .../src}/tests/conftest.py | 0 .../src}/tests/integration_tests/conftest.py | 0 .../dotenv_templates/dotenv.jinja2 | 0 .../integration_tests/test_datasources.py | 0 .../integration_tests/test_startup_scripts.py | 0 .../dotenv_data/dotenv_no_datasource_1 | 0 .../dotenv_data/dotenv_no_datasource_2 | 0 .../dotenv_with_azure_search_success | 0 .../dotenv_with_elasticsearch_success | 0 .../helpers/test_azure_credential_utils.py | 0 .../src}/tests/unit_tests/test_settings.py | 0 .../src}/tests/unit_tests/test_utils.py | 0 .../tests}/e2e-test/.gitignore | 0 .../tests}/e2e-test/README.md | 0 .../tests}/e2e-test/base/__init__.py | 0 .../tests}/e2e-test/base/base.py | 0 .../tests}/e2e-test/config/constants.py | 0 .../tests}/e2e-test/img.png | Bin .../tests}/e2e-test/img_1.png | Bin .../tests}/e2e-test/pages/__init__.py | 0 .../tests}/e2e-test/pages/browsePage.py | 0 .../tests}/e2e-test/pages/draftPage.py | 0 .../tests}/e2e-test/pages/generatePage.py | 0 .../tests}/e2e-test/pages/homePage.py | 0 .../tests}/e2e-test/pytest.ini | 0 .../tests}/e2e-test/requirements.txt | 0 .../tests}/e2e-test/sample_dotenv_file.txt | 0 .../tests}/e2e-test/tests/__init__.py | 0 .../tests}/e2e-test/tests/conftest.py | 0 .../tests}/e2e-test/tests/test_gp_docgen.py | 0 docs/images/readme/business_scenario.png | Bin 0 -> 14787 bytes docs/images/readme/quick_deploy.png | Bin 0 -> 19499 bytes docs/images/readme/solution_overview.png | Bin 0 -> 15891 bytes .../readme/supporting_documentation.png | Bin 0 -> 17402 bytes infra/vscode_web/.env | 7 - 306 files changed, 402 insertions(+), 91 deletions(-) rename {.azdo => archive-doc-gen/.azdo}/pipelines/azure-dev.yml (100%) rename {.devcontainer => archive-doc-gen/.devcontainer}/devcontainer.json (100%) rename {.devcontainer => archive-doc-gen/.devcontainer}/setup_env.sh (100%) rename .flake8 => archive-doc-gen/.flake8 (100%) create mode 100644 archive-doc-gen/.gitattributes rename {.github => archive-doc-gen/.github}/CODEOWNERS (100%) rename {.github => archive-doc-gen/.github}/ISSUE_TEMPLATE/bug_report.md (100%) rename {.github => archive-doc-gen/.github}/ISSUE_TEMPLATE/feature_request.md (100%) rename {.github => archive-doc-gen/.github}/ISSUE_TEMPLATE/subtask.md (100%) rename {.github => archive-doc-gen/.github}/dependabot.yml (100%) rename {.github => archive-doc-gen/.github}/pull_request_template.md (100%) rename {.github => archive-doc-gen/.github}/workflows/Scheduled-Dependabot-PRs-Auto-Merge.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/azure-dev.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/broken-links-checker.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/create-release.yml (97%) rename {.github => archive-doc-gen/.github}/workflows/deploy-v2.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/deploy.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/docker-build-and-push.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/node.js.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/pr-title-checker.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/pylint.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/python-app.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/stale-bot.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/telemetry-template-check.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/test-automation.yml (100%) rename {.github => archive-doc-gen/.github}/workflows/tests.yml (100%) create mode 100644 archive-doc-gen/.gitignore rename {.vscode => archive-doc-gen/.vscode}/launch.json (100%) rename {.vscode => archive-doc-gen/.vscode}/settings.json (100%) create mode 100644 archive-doc-gen/CODE_OF_CONDUCT.md create mode 100644 archive-doc-gen/CONTRIBUTING.md create mode 100644 archive-doc-gen/LICENSE create mode 100644 archive-doc-gen/README.md create mode 100644 archive-doc-gen/SECURITY.md rename app-azure.yaml => archive-doc-gen/app-azure.yaml (100%) rename azure.yaml => archive-doc-gen/azure.yaml (100%) rename {docs => archive-doc-gen/docs}/ACRBuildAndPushGuide.md (100%) rename {docs => archive-doc-gen/docs}/AppAuthentication.md (100%) rename {docs => archive-doc-gen/docs}/AzureAccountSetUp.md (100%) rename {docs => archive-doc-gen/docs}/AzureGPTQuotaSettings.md (100%) rename {docs => archive-doc-gen/docs}/AzureSemanticSearchRegion.md (100%) rename {docs => archive-doc-gen/docs}/CustomizingAzdParameters.md (100%) rename {docs => archive-doc-gen/docs}/DeleteResourceGroup.md (100%) rename {docs => archive-doc-gen/docs}/DeploymentGuide.md (100%) rename {docs => archive-doc-gen/docs}/PowershellSetup.md (100%) rename {docs => archive-doc-gen/docs}/QuotaCheck.md (100%) rename {docs => archive-doc-gen/docs}/README_LOCAL.md (100%) rename {docs => archive-doc-gen/docs}/SampleQuestions.md (100%) rename {docs => archive-doc-gen/docs}/TRANSPARENCY_FAQ.md (100%) rename {docs => archive-doc-gen/docs}/TroubleShootingSteps.md (100%) rename {docs => archive-doc-gen/docs}/container_registry_migration.md (100%) rename {docs => archive-doc-gen/docs}/create_new_app_registration.md (100%) rename {docs => archive-doc-gen/docs}/images/AddDetails.png (100%) rename {docs => archive-doc-gen/docs}/images/AddPlatform.png (100%) rename {docs => archive-doc-gen/docs}/images/AddRedirectURL.png (100%) rename {docs => archive-doc-gen/docs}/images/AppAuthIdentityProvider.png (100%) rename {docs => archive-doc-gen/docs}/images/AppAuthIdentityProviderAdd.png (100%) rename {docs => archive-doc-gen/docs}/images/AppAuthIdentityProviderAdded.png (100%) rename {docs => archive-doc-gen/docs}/images/AppAuthentication.png (100%) rename {docs => archive-doc-gen/docs}/images/AppAuthenticationIdentity.png (100%) rename {docs => archive-doc-gen/docs}/images/AppServiceContainer.png (100%) rename {docs => archive-doc-gen/docs}/images/Appregistrations.png (100%) rename {docs => archive-doc-gen/docs}/images/Archimage.png (100%) rename {docs => archive-doc-gen/docs}/images/AzureHomePage.png (100%) rename {docs => archive-doc-gen/docs}/images/ContainerApp.png (100%) rename {docs => archive-doc-gen/docs}/images/DeleteRG.png (100%) rename {docs => archive-doc-gen/docs}/images/DocGen_Azure_AI_Foundry_Architecture.png (100%) rename {docs => archive-doc-gen/docs}/images/GenerateDraft.png (100%) rename {docs => archive-doc-gen/docs}/images/MicrosoftEntraID.png (100%) rename {docs => archive-doc-gen/docs}/images/NewRegistration.png (100%) rename {docs => archive-doc-gen/docs}/images/Web.png (100%) rename {docs => archive-doc-gen/docs}/images/WebAppURL.png (100%) rename {docs => archive-doc-gen/docs}/images/architecture.png (100%) rename {docs => archive-doc-gen/docs}/images/customerTruth.png (100%) rename {docs => archive-doc-gen/docs}/images/deleteservices.png (100%) rename {docs => archive-doc-gen/docs}/images/deployment_center.png (100%) rename {docs => archive-doc-gen/docs}/images/git_bash.png (100%) rename {docs => archive-doc-gen/docs}/images/keyfeatures.png (100%) rename {docs => archive-doc-gen/docs}/images/landing_page.png (100%) rename {docs => archive-doc-gen/docs}/images/logAnalytics.png (100%) rename {docs => archive-doc-gen/docs}/images/logAnalyticsJson.png (100%) rename {docs => archive-doc-gen/docs}/images/logAnalyticsList.png (100%) rename {docs => archive-doc-gen/docs}/images/oneClickDeploy.png (100%) rename {docs => archive-doc-gen/docs}/images/quota-check-output.png (100%) rename {docs => archive-doc-gen/docs}/images/re_use_foundry_project/azure_ai_foundry_list.png (100%) rename {docs => archive-doc-gen/docs}/images/re_use_foundry_project/navigate_to_projects.png (100%) rename {docs => archive-doc-gen/docs}/images/re_use_foundry_project/project_resource_id.png (100%) rename {docs => archive-doc-gen/docs}/images/re_use_log/logAnalytics.png (100%) rename {docs => archive-doc-gen/docs}/images/re_use_log/logAnalyticsJson.png (100%) rename {docs => archive-doc-gen/docs}/images/re_use_log/logAnalyticsList.png (100%) rename {docs => archive-doc-gen/docs}/images/readme/business-scenario.png (100%) rename {docs => archive-doc-gen/docs}/images/readme/quick-deploy.png (100%) rename {docs => archive-doc-gen/docs}/images/readme/solution-overview.png (100%) rename {docs => archive-doc-gen/docs}/images/readme/supporting-documentation.png (100%) rename {docs => archive-doc-gen/docs}/images/resource-groups.png (100%) rename {docs => archive-doc-gen/docs}/images/resource_menu.png (100%) rename {docs => archive-doc-gen/docs}/images/resourcegroup.png (100%) rename {docs => archive-doc-gen/docs}/images/resourcegroup1.png (100%) rename {docs => archive-doc-gen/docs}/images/supportingDocuments.png (100%) rename {docs => archive-doc-gen/docs}/images/userStory.png (100%) rename {docs => archive-doc-gen/docs}/re-use-foundry-project.md (100%) rename {docs => archive-doc-gen/docs}/re-use-log-analytics.md (100%) rename {infra => archive-doc-gen/infra}/data/pdfdata.zip (100%) rename {infra => archive-doc-gen/infra}/main.bicep (100%) rename {infra => archive-doc-gen/infra}/main.json (100%) rename {infra => archive-doc-gen/infra}/main.parameters.json (100%) rename {infra => archive-doc-gen/infra}/main.waf.parameters.json (100%) rename {infra => archive-doc-gen/infra}/modules/ai-project.bicep (100%) rename {infra => archive-doc-gen/infra}/modules/ai-services-deployments.bicep (100%) rename {infra => archive-doc-gen/infra}/modules/deploy_aifp_aisearch_connection.bicep (100%) rename {infra => archive-doc-gen/infra}/modules/role-assignment.bicep (100%) rename {infra => archive-doc-gen/infra}/modules/virtualNetwork.bicep (100%) rename {infra => archive-doc-gen/infra}/modules/web-sites.bicep (100%) rename {infra => archive-doc-gen/infra}/modules/web-sites.config.bicep (100%) rename {infra => archive-doc-gen/infra}/scripts/add_cosmosdb_access.sh (100%) rename {infra => archive-doc-gen/infra}/scripts/copy_kb_files.sh (100%) rename {infra => archive-doc-gen/infra}/scripts/index_scripts/01_create_search_index.py (100%) rename {infra => archive-doc-gen/infra}/scripts/index_scripts/02_process_data.py (100%) rename {infra => archive-doc-gen/infra}/scripts/index_scripts/requirements.txt (100%) rename {infra => archive-doc-gen/infra}/scripts/process_sample_data.sh (100%) rename {infra => archive-doc-gen/infra}/scripts/run_create_index_scripts.sh (100%) rename {infra => archive-doc-gen/infra}/vscode_web/.gitignore (100%) rename {infra => archive-doc-gen/infra}/vscode_web/LICENSE (100%) rename {infra => archive-doc-gen/infra}/vscode_web/README-noazd.md (100%) rename {infra => archive-doc-gen/infra}/vscode_web/README.md (100%) rename {infra => archive-doc-gen/infra}/vscode_web/codeSample.py (100%) rename {infra => archive-doc-gen/infra}/vscode_web/endpoint-requirements.txt (100%) rename {infra => archive-doc-gen/infra}/vscode_web/endpointCodeSample.py (100%) rename {infra => archive-doc-gen/infra}/vscode_web/index.json (100%) rename {infra => archive-doc-gen/infra}/vscode_web/install.sh (100%) rename {infra => archive-doc-gen/infra}/vscode_web/requirements.txt (100%) rename package-lock.json => archive-doc-gen/package-lock.json (100%) rename {scripts => archive-doc-gen/scripts}/SAMPLE_DATA.md (100%) rename {scripts => archive-doc-gen/scripts}/auth_init.ps1 (100%) rename {scripts => archive-doc-gen/scripts}/auth_init.py (100%) rename {scripts => archive-doc-gen/scripts}/auth_init.sh (100%) rename {scripts => archive-doc-gen/scripts}/auth_update.ps1 (100%) rename {scripts => archive-doc-gen/scripts}/auth_update.py (100%) rename {scripts => archive-doc-gen/scripts}/auth_update.sh (100%) rename {scripts => archive-doc-gen/scripts}/checkquota.sh (100%) rename {scripts => archive-doc-gen/scripts}/chunk_documents.py (100%) rename {scripts => archive-doc-gen/scripts}/config.json (100%) rename {scripts => archive-doc-gen/scripts}/data_preparation.py (100%) rename {scripts => archive-doc-gen/scripts}/data_utils.py (100%) rename {scripts => archive-doc-gen/scripts}/embed_documents.py (100%) rename {scripts => archive-doc-gen/scripts}/loadenv.ps1 (100%) rename {scripts => archive-doc-gen/scripts}/loadenv.sh (100%) rename {scripts => archive-doc-gen/scripts}/prepdocs.ps1 (100%) rename {scripts => archive-doc-gen/scripts}/prepdocs.py (100%) rename {scripts => archive-doc-gen/scripts}/prepdocs.sh (100%) rename {scripts => archive-doc-gen/scripts}/quota_check_params.sh (100%) rename {scripts => archive-doc-gen/scripts}/readme.md (100%) rename {scripts => archive-doc-gen/scripts}/role_assignment.sh (100%) rename {src => archive-doc-gen/src}/.dockerignore (100%) rename {src => archive-doc-gen/src}/.env.sample (100%) rename {src => archive-doc-gen/src}/.gitignore (100%) rename {src => archive-doc-gen/src}/SUPPORT.md (100%) rename {src => archive-doc-gen/src}/TEST_CASE_FLOWS.md (100%) rename {src => archive-doc-gen/src}/WebApp.Dockerfile (100%) rename {src => archive-doc-gen/src}/app.py (100%) rename {src => archive-doc-gen/src}/backend/__init__.py (100%) rename {src => archive-doc-gen/src}/backend/api/agent/agent_factory_base.py (100%) rename {src => archive-doc-gen/src}/backend/api/agent/browse_agent_factory.py (100%) rename {src => archive-doc-gen/src}/backend/api/agent/section_agent_factory.py (100%) rename {src => archive-doc-gen/src}/backend/api/agent/template_agent_factory.py (100%) rename {src => archive-doc-gen/src}/backend/auth/__init__.py (100%) rename {src => archive-doc-gen/src}/backend/auth/auth_utils.py (100%) rename {src => archive-doc-gen/src}/backend/auth/sample_user.py (100%) rename {src => archive-doc-gen/src}/backend/helpers/azure_credential_utils.py (100%) rename {src => archive-doc-gen/src}/backend/history/cosmosdbservice.py (100%) rename {src => archive-doc-gen/src}/backend/security/__init__.py (100%) rename {src => archive-doc-gen/src}/backend/security/ms_defender_utils.py (100%) rename {src => archive-doc-gen/src}/backend/settings.py (100%) rename {src => archive-doc-gen/src}/backend/utils.py (100%) rename {src => archive-doc-gen/src}/event_utils.py (100%) rename {src => archive-doc-gen/src}/frontend/.eslintignore (100%) rename {src => archive-doc-gen/src}/frontend/.eslintrc.json (100%) rename {src => archive-doc-gen/src}/frontend/.prettierignore (100%) rename {src => archive-doc-gen/src}/frontend/.prettierrc.json (100%) rename {src => archive-doc-gen/src}/frontend/__mocks__/dompurify.ts (100%) rename {src => archive-doc-gen/src}/frontend/__mocks__/fileMock.ts (100%) rename {src => archive-doc-gen/src}/frontend/__mocks__/mockAPIData.ts (100%) rename {src => archive-doc-gen/src}/frontend/__mocks__/react-markdown.tsx (100%) rename {src => archive-doc-gen/src}/frontend/eslint.config.ts (100%) rename {src => archive-doc-gen/src}/frontend/index.html (100%) rename {src => archive-doc-gen/src}/frontend/jest.config.ts (100%) rename {src => archive-doc-gen/src}/frontend/jest.polyfills.js (100%) rename {src => archive-doc-gen/src}/frontend/package-lock.json (100%) rename {src => archive-doc-gen/src}/frontend/package.json (100%) rename {src => archive-doc-gen/src}/frontend/polyfills.js (100%) rename {src => archive-doc-gen/src}/frontend/public/favicon.ico (100%) rename {src => archive-doc-gen/src}/frontend/src/api/api.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/api/index.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/api/models.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/assets/Azure.svg (100%) rename {src => archive-doc-gen/src}/frontend/src/assets/ClearChat.svg (100%) rename {src => archive-doc-gen/src}/frontend/src/assets/Contoso.svg (100%) rename {src => archive-doc-gen/src}/frontend/src/assets/Generate.svg (100%) rename {src => archive-doc-gen/src}/frontend/src/assets/Send.svg (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Answer/Answer.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Answer/Answer.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Answer/Answer.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Answer/AnswerParser.test.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Answer/AnswerParser.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Answer/index.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/ChatHistoryList.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/ChatHistoryList.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/ChatHistoryPanel.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/ChatHistoryPanel.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/ChatHistoryPanel.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/DraftCards/SectionCard.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/DraftCards/SectionCard.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/DraftCards/TitleCard.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/DraftCards/TitleCard.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/FeatureCard/FeatureCard.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/FeatureCard/FeatureCard.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/QuestionInput/QuestionInput.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/components/QuestionInput/QuestionInput.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/QuestionInput/QuestionInput.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/QuestionInput/index.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Sidebar/Sidebar.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Sidebar/Sidebar.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/Sidebar/Sidebar.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/common/Button.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/components/common/Button.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/components/common/Button.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/constants/chatHistory.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/constants/chatHistory.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/constants/xssAllowTags.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/helpers/helpers.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/index.css (100%) rename {src => archive-doc-gen/src}/frontend/src/index.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/NoPage.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Chat.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Chat.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Chat.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Components/AuthNotConfigure.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Components/AuthNotConfigure.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Components/ChatMessageContainer.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Components/ChatMessageContainer.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Components/CitationPanel.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/chat/Components/CitationPanel.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/document/Document.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/document/Document.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/document/Document.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/draft/Draft.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/draft/Draft.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/draft/Draft.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/landing/Landing.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/landing/Landing.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/landing/Landing.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/layout/Layout.module.css (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/layout/Layout.test.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/pages/layout/Layout.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/state/AppProvider.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/state/AppReducer.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/test/setupTests.ts (100%) rename {src => archive-doc-gen/src}/frontend/src/test/test.utils.tsx (100%) rename {src => archive-doc-gen/src}/frontend/src/vite-env.d.ts (100%) rename {src => archive-doc-gen/src}/frontend/tsconfig.json (100%) rename {src => archive-doc-gen/src}/frontend/tsconfig.node.json (100%) rename {src => archive-doc-gen/src}/frontend/vite.config.ts (100%) rename {src => archive-doc-gen/src}/gunicorn.conf.py (100%) rename {src => archive-doc-gen/src}/requirements-dev.txt (100%) rename {src => archive-doc-gen/src}/requirements.txt (100%) rename {src => archive-doc-gen/src}/start.cmd (100%) rename {src => archive-doc-gen/src}/start.sh (100%) rename {src => archive-doc-gen/src}/test.cmd (100%) rename {src => archive-doc-gen/src}/tests/conftest.py (100%) rename {src => archive-doc-gen/src}/tests/integration_tests/conftest.py (100%) rename {src => archive-doc-gen/src}/tests/integration_tests/dotenv_templates/dotenv.jinja2 (100%) rename {src => archive-doc-gen/src}/tests/integration_tests/test_datasources.py (100%) rename {src => archive-doc-gen/src}/tests/integration_tests/test_startup_scripts.py (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/dotenv_data/dotenv_no_datasource_1 (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/dotenv_data/dotenv_no_datasource_2 (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/dotenv_data/dotenv_with_azure_search_success (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/dotenv_data/dotenv_with_elasticsearch_success (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/helpers/test_azure_credential_utils.py (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/test_settings.py (100%) rename {src => archive-doc-gen/src}/tests/unit_tests/test_utils.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/.gitignore (100%) rename {tests => archive-doc-gen/tests}/e2e-test/README.md (100%) rename {tests => archive-doc-gen/tests}/e2e-test/base/__init__.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/base/base.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/config/constants.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/img.png (100%) rename {tests => archive-doc-gen/tests}/e2e-test/img_1.png (100%) rename {tests => archive-doc-gen/tests}/e2e-test/pages/__init__.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/pages/browsePage.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/pages/draftPage.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/pages/generatePage.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/pages/homePage.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/pytest.ini (100%) rename {tests => archive-doc-gen/tests}/e2e-test/requirements.txt (100%) rename {tests => archive-doc-gen/tests}/e2e-test/sample_dotenv_file.txt (100%) rename {tests => archive-doc-gen/tests}/e2e-test/tests/__init__.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/tests/conftest.py (100%) rename {tests => archive-doc-gen/tests}/e2e-test/tests/test_gp_docgen.py (100%) create mode 100644 docs/images/readme/business_scenario.png create mode 100644 docs/images/readme/quick_deploy.png create mode 100644 docs/images/readme/solution_overview.png create mode 100644 docs/images/readme/supporting_documentation.png delete mode 100644 infra/vscode_web/.env diff --git a/README.md b/README.md index 4740ff878..88a0e5ece 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -# Document generation solution accelerator - -This solution accelerator is a powerful tool that helps you create your own AI assistant for document generation. The accelerator can be used by any customer looking for reusable architecture and code snippets to build an AI assistant to generate a sample template and content grounded on their own enterprise data. - -This example focuses on a generic use case - chat with your own data, generate a document template using your own data, and exporting the document in a docx format. +# Content generation solution accelerator +This solution accelerator is an internal chatbot that can interpret and +understand context and direction from a creative brief to create multi-modal text and image content for a marketing ad campaign.​
@@ -16,7 +14,7 @@ This example focuses on a generic use case - chat with your own data, generate a **Note:** With any AI solutions you create using these templates, you are responsible for assessing all associated risks and for complying with all applicable laws and safety standards. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md).
-

+

Solution overview

@@ -25,7 +23,7 @@ It leverages Azure OpenAI Service and Azure AI Search, to identify relevant docu The sample data is sourced from generic AI-generated promissory notes. The documents are intended for use as sample data only. ### Solution architecture -|![image](./docs/images/DocGen_Azure_AI_Foundry_Architecture.png)| +|TBD| |---| @@ -46,21 +44,13 @@ The sample data is sourced from generic AI-generated promissory notes. The docum
Click to learn more about the key features this solution enables - - **Semantic search**
- Azure AI Search to enable RAG and grounding of the application on the processed dataset.​ - - - **Summarization**
- Azure OpenAI Service and GPT models to help summarize the search content and answer questions.​ - - - **Content generation**
- Azure OpenAI Service and GPT models to help generate relevant content with Prompt Flow.​ - + - **TBD**
+ Azure...​ +
- -

-

+

Quick deploy

@@ -157,7 +147,7 @@ Put your data to work by reducing blank page anxiety, speeding up document draft

-

+

Supporting documentation

diff --git a/.azdo/pipelines/azure-dev.yml b/archive-doc-gen/.azdo/pipelines/azure-dev.yml similarity index 100% rename from .azdo/pipelines/azure-dev.yml rename to archive-doc-gen/.azdo/pipelines/azure-dev.yml diff --git a/.devcontainer/devcontainer.json b/archive-doc-gen/.devcontainer/devcontainer.json similarity index 100% rename from .devcontainer/devcontainer.json rename to archive-doc-gen/.devcontainer/devcontainer.json diff --git a/.devcontainer/setup_env.sh b/archive-doc-gen/.devcontainer/setup_env.sh similarity index 100% rename from .devcontainer/setup_env.sh rename to archive-doc-gen/.devcontainer/setup_env.sh diff --git a/.flake8 b/archive-doc-gen/.flake8 similarity index 100% rename from .flake8 rename to archive-doc-gen/.flake8 diff --git a/archive-doc-gen/.gitattributes b/archive-doc-gen/.gitattributes new file mode 100644 index 000000000..314766e91 --- /dev/null +++ b/archive-doc-gen/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf diff --git a/.github/CODEOWNERS b/archive-doc-gen/.github/CODEOWNERS similarity index 100% rename from .github/CODEOWNERS rename to archive-doc-gen/.github/CODEOWNERS diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/archive-doc-gen/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to archive-doc-gen/.github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/archive-doc-gen/.github/ISSUE_TEMPLATE/feature_request.md similarity index 100% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to archive-doc-gen/.github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/subtask.md b/archive-doc-gen/.github/ISSUE_TEMPLATE/subtask.md similarity index 100% rename from .github/ISSUE_TEMPLATE/subtask.md rename to archive-doc-gen/.github/ISSUE_TEMPLATE/subtask.md diff --git a/.github/dependabot.yml b/archive-doc-gen/.github/dependabot.yml similarity index 100% rename from .github/dependabot.yml rename to archive-doc-gen/.github/dependabot.yml diff --git a/.github/pull_request_template.md b/archive-doc-gen/.github/pull_request_template.md similarity index 100% rename from .github/pull_request_template.md rename to archive-doc-gen/.github/pull_request_template.md diff --git a/.github/workflows/Scheduled-Dependabot-PRs-Auto-Merge.yml b/archive-doc-gen/.github/workflows/Scheduled-Dependabot-PRs-Auto-Merge.yml similarity index 100% rename from .github/workflows/Scheduled-Dependabot-PRs-Auto-Merge.yml rename to archive-doc-gen/.github/workflows/Scheduled-Dependabot-PRs-Auto-Merge.yml diff --git a/.github/workflows/azure-dev.yml b/archive-doc-gen/.github/workflows/azure-dev.yml similarity index 100% rename from .github/workflows/azure-dev.yml rename to archive-doc-gen/.github/workflows/azure-dev.yml diff --git a/.github/workflows/broken-links-checker.yml b/archive-doc-gen/.github/workflows/broken-links-checker.yml similarity index 100% rename from .github/workflows/broken-links-checker.yml rename to archive-doc-gen/.github/workflows/broken-links-checker.yml diff --git a/.github/workflows/create-release.yml b/archive-doc-gen/.github/workflows/create-release.yml similarity index 97% rename from .github/workflows/create-release.yml rename to archive-doc-gen/.github/workflows/create-release.yml index 2a2cbb23d..836bb7fc1 100644 --- a/.github/workflows/create-release.yml +++ b/archive-doc-gen/.github/workflows/create-release.yml @@ -1,64 +1,64 @@ -on: - push: - branches: - - main - -permissions: - contents: write - pull-requests: write - -name: Create-Release - -jobs: - create-release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - ref: ${{ github.event.workflow_run.head_sha }} - - - uses: codfish/semantic-release-action@v4 - id: semantic - with: - tag-format: 'v${version}' - additional-packages: | - ['conventional-changelog-conventionalcommits@7'] - plugins: | - [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits" - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { type: 'feat', section: 'Features', hidden: false }, - { type: 'fix', section: 'Bug Fixes', hidden: false }, - { type: 'perf', section: 'Performance Improvements', hidden: false }, - { type: 'revert', section: 'Reverts', hidden: false }, - { type: 'docs', section: 'Other Updates', hidden: false }, - { type: 'style', section: 'Other Updates', hidden: false }, - { type: 'chore', section: 'Other Updates', hidden: false }, - { type: 'refactor', section: 'Other Updates', hidden: false }, - { type: 'test', section: 'Other Updates', hidden: false }, - { type: 'build', section: 'Other Updates', hidden: false }, - { type: 'ci', section: 'Other Updates', hidden: false } - ] - } - } - ], - '@semantic-release/github' - ] - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: echo ${{ steps.semantic.outputs.release-version }} - - - run: echo "$OUTPUTS" - env: - OUTPUTS: ${{ toJson(steps.semantic.outputs) }} +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +name: Create-Release + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: ${{ github.event.workflow_run.head_sha }} + + - uses: codfish/semantic-release-action@v4 + id: semantic + with: + tag-format: 'v${version}' + additional-packages: | + ['conventional-changelog-conventionalcommits@7'] + plugins: | + [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { type: 'feat', section: 'Features', hidden: false }, + { type: 'fix', section: 'Bug Fixes', hidden: false }, + { type: 'perf', section: 'Performance Improvements', hidden: false }, + { type: 'revert', section: 'Reverts', hidden: false }, + { type: 'docs', section: 'Other Updates', hidden: false }, + { type: 'style', section: 'Other Updates', hidden: false }, + { type: 'chore', section: 'Other Updates', hidden: false }, + { type: 'refactor', section: 'Other Updates', hidden: false }, + { type: 'test', section: 'Other Updates', hidden: false }, + { type: 'build', section: 'Other Updates', hidden: false }, + { type: 'ci', section: 'Other Updates', hidden: false } + ] + } + } + ], + '@semantic-release/github' + ] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: echo ${{ steps.semantic.outputs.release-version }} + + - run: echo "$OUTPUTS" + env: + OUTPUTS: ${{ toJson(steps.semantic.outputs) }} diff --git a/.github/workflows/deploy-v2.yml b/archive-doc-gen/.github/workflows/deploy-v2.yml similarity index 100% rename from .github/workflows/deploy-v2.yml rename to archive-doc-gen/.github/workflows/deploy-v2.yml diff --git a/.github/workflows/deploy.yml b/archive-doc-gen/.github/workflows/deploy.yml similarity index 100% rename from .github/workflows/deploy.yml rename to archive-doc-gen/.github/workflows/deploy.yml diff --git a/.github/workflows/docker-build-and-push.yml b/archive-doc-gen/.github/workflows/docker-build-and-push.yml similarity index 100% rename from .github/workflows/docker-build-and-push.yml rename to archive-doc-gen/.github/workflows/docker-build-and-push.yml diff --git a/.github/workflows/node.js.yml b/archive-doc-gen/.github/workflows/node.js.yml similarity index 100% rename from .github/workflows/node.js.yml rename to archive-doc-gen/.github/workflows/node.js.yml diff --git a/.github/workflows/pr-title-checker.yml b/archive-doc-gen/.github/workflows/pr-title-checker.yml similarity index 100% rename from .github/workflows/pr-title-checker.yml rename to archive-doc-gen/.github/workflows/pr-title-checker.yml diff --git a/.github/workflows/pylint.yml b/archive-doc-gen/.github/workflows/pylint.yml similarity index 100% rename from .github/workflows/pylint.yml rename to archive-doc-gen/.github/workflows/pylint.yml diff --git a/.github/workflows/python-app.yml b/archive-doc-gen/.github/workflows/python-app.yml similarity index 100% rename from .github/workflows/python-app.yml rename to archive-doc-gen/.github/workflows/python-app.yml diff --git a/.github/workflows/stale-bot.yml b/archive-doc-gen/.github/workflows/stale-bot.yml similarity index 100% rename from .github/workflows/stale-bot.yml rename to archive-doc-gen/.github/workflows/stale-bot.yml diff --git a/.github/workflows/telemetry-template-check.yml b/archive-doc-gen/.github/workflows/telemetry-template-check.yml similarity index 100% rename from .github/workflows/telemetry-template-check.yml rename to archive-doc-gen/.github/workflows/telemetry-template-check.yml diff --git a/.github/workflows/test-automation.yml b/archive-doc-gen/.github/workflows/test-automation.yml similarity index 100% rename from .github/workflows/test-automation.yml rename to archive-doc-gen/.github/workflows/test-automation.yml diff --git a/.github/workflows/tests.yml b/archive-doc-gen/.github/workflows/tests.yml similarity index 100% rename from .github/workflows/tests.yml rename to archive-doc-gen/.github/workflows/tests.yml diff --git a/archive-doc-gen/.gitignore b/archive-doc-gen/.gitignore new file mode 100644 index 000000000..0abb7a034 --- /dev/null +++ b/archive-doc-gen/.gitignore @@ -0,0 +1,15 @@ +.venv + +.env +.azure/ +__pycache__/ +.ipynb_checkpoints/ + + +venv +myenv + +scriptsenv/ + +scriptenv +pdf \ No newline at end of file diff --git a/.vscode/launch.json b/archive-doc-gen/.vscode/launch.json similarity index 100% rename from .vscode/launch.json rename to archive-doc-gen/.vscode/launch.json diff --git a/.vscode/settings.json b/archive-doc-gen/.vscode/settings.json similarity index 100% rename from .vscode/settings.json rename to archive-doc-gen/.vscode/settings.json diff --git a/archive-doc-gen/CODE_OF_CONDUCT.md b/archive-doc-gen/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..f9ba8cf65 --- /dev/null +++ b/archive-doc-gen/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/archive-doc-gen/CONTRIBUTING.md b/archive-doc-gen/CONTRIBUTING.md new file mode 100644 index 000000000..c282e9a1a --- /dev/null +++ b/archive-doc-gen/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file diff --git a/archive-doc-gen/LICENSE b/archive-doc-gen/LICENSE new file mode 100644 index 000000000..9e841e7a2 --- /dev/null +++ b/archive-doc-gen/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/archive-doc-gen/README.md b/archive-doc-gen/README.md new file mode 100644 index 000000000..4740ff878 --- /dev/null +++ b/archive-doc-gen/README.md @@ -0,0 +1,225 @@ +# Document generation solution accelerator + +This solution accelerator is a powerful tool that helps you create your own AI assistant for document generation. The accelerator can be used by any customer looking for reusable architecture and code snippets to build an AI assistant to generate a sample template and content grounded on their own enterprise data. + +This example focuses on a generic use case - chat with your own data, generate a document template using your own data, and exporting the document in a docx format. + +
+ +
+ +[**SOLUTION OVERVIEW**](#solution-overview) \| [**QUICK DEPLOY**](#quick-deploy) \| [**BUSINESS SCENARIO**](#business-scenario) \| [**SUPPORTING DOCUMENTATION**](#supporting-documentation) + +
+
+ +**Note:** With any AI solutions you create using these templates, you are responsible for assessing all associated risks and for complying with all applicable laws and safety standards. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). +
+ +

+Solution overview +

+ +It leverages Azure OpenAI Service and Azure AI Search, to identify relevant documents, summarize unstructured information, and generate document templates. + +The sample data is sourced from generic AI-generated promissory notes. The documents are intended for use as sample data only. + +### Solution architecture +|![image](./docs/images/DocGen_Azure_AI_Foundry_Architecture.png)| +|---| + + +
+ +### Additional resources + +[Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/ai-services/openai/) + +[Azure AI Search](https://learn.microsoft.com/en-us/azure/search/) + +[Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-studio/) + + +
+ +### Key features +
+ Click to learn more about the key features this solution enables + + - **Semantic search**
+ Azure AI Search to enable RAG and grounding of the application on the processed dataset.​ + + - **Summarization**
+ Azure OpenAI Service and GPT models to help summarize the search content and answer questions.​ + + - **Content generation**
+ Azure OpenAI Service and GPT models to help generate relevant content with Prompt Flow.​ + +
+ + + +

+

+Quick deploy +

+ +### How to install or deploy +Follow the quick deploy steps on the deployment guide to deploy this solution to your own Azure subscription. + +> **Note:** This solution accelerator requires **Azure Developer CLI (azd) version 1.18.0 or higher**. Please ensure you have the latest version installed before proceeding with deployment. [Download azd here](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). + +[Click here to launch the deployment guide](./docs/DeploymentGuide.md) +

+ +| [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/microsoft/document-generation-solution-accelerator) | [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/document-generation-solution-accelerator) | [![Open in Visual Studio Code Web](https://img.shields.io/static/v1?style=for-the-badge&label=Visual%20Studio%20Code%20(Web)&message=Open&color=blue&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/azure/?vscode-azure-exp=foundry&agentPayload=eyJiYXNlVXJsIjogImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9taWNyb3NvZnQvZG9jdW1lbnQtZ2VuZXJhdGlvbi1zb2x1dGlvbi1hY2NlbGVyYXRvci9yZWZzL2hlYWRzL21haW4vaW5mcmEvdnNjb2RlX3dlYiIsICJpbmRleFVybCI6ICIvaW5kZXguanNvbiIsICJ2YXJpYWJsZXMiOiB7ImFnZW50SWQiOiAiIiwgImNvbm5lY3Rpb25TdHJpbmciOiAiIiwgInRocmVhZElkIjogIiIsICJ1c2VyTWVzc2FnZSI6ICIiLCAicGxheWdyb3VuZE5hbWUiOiAiIiwgImxvY2F0aW9uIjogIiIsICJzdWJzY3JpcHRpb25JZCI6ICIiLCAicmVzb3VyY2VJZCI6ICIiLCAicHJvamVjdFJlc291cmNlSWQiOiAiIiwgImVuZHBvaW50IjogIiJ9LCAiY29kZVJvdXRlIjogWyJhaS1wcm9qZWN0cy1zZGsiLCAicHl0aG9uIiwgImRlZmF1bHQtYXp1cmUtYXV0aCIsICJlbmRwb2ludCJdfQ==) | +|---|---|---| + +
+ +> ⚠️ **Important: Check Azure OpenAI Quota Availability** +
To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./docs/QuotaCheck.md) before you deploy the solution. + +
+ +### Prerequisites and costs + +To deploy this solution accelerator, ensure you have access to an [Azure subscription](https://azure.microsoft.com/free/) with the necessary permissions to create **resource groups, resources, app registrations, and assign roles at the resource group level**. This should include Contributor role at the subscription level and Role Based Access Control role on the subscription and/or resource group level. Follow the steps in [Azure Account Set Up](./docs/AzureAccountSetUp.md). + +Check the [Azure Products by Region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=all®ions=all) page and select a **region** where the following services are available. + +Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage. The majority of the Azure resources used in this infrastructure are on usage-based pricing tiers. However, Azure Container Registry has a fixed cost per registry per day. + +Use the [Azure pricing calculator](https://azure.microsoft.com/en-us/pricing/calculator) to calculate the cost of this solution in your subscription. + +Review a [sample pricing sheet](https://azure.com/e/2402502429fc46429e395e0bb93d0711) in the event you want to customize and scale usage. + +_Note: This is not meant to outline all costs as selected SKUs, scaled use, customizations, and integrations into your own tenant can affect the total consumption of this sample solution. The sample pricing sheet is meant to give you a starting point to customize the estimate for your specific needs._ + +
+ +| Product | Description | Cost | +|---|---|---| +| [Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/) | Free tier. Build generative AI applications on an enterprise-grade platform. | [Pricing](https://azure.microsoft.com/pricing/details/ai-studio/) | +| [Azure AI Search](https://learn.microsoft.com/en-us/azure/search/) | Standard tier, S1. Pricing is based on the number of documents and operations. Information retrieval at scale for vector and text content in traditional or generative search scenarios. | [Pricing](https://azure.microsoft.com/pricing/details/search/) | +| [Azure Storage Account](https://learn.microsoft.com/en-us/azure/storage/blobs/) | Standard tier, LRS. Pricing is based on storage and operations. Blob storage in the clopud, optimized for storing massive amounts of unstructured data. | [Pricing](https://azure.microsoft.com/pricing/details/storage/blobs/) | +| [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/) | Standard tier. Pricing is based on the number of operations. Maintain keys that access and encrypt your cloud resources, apps, and solutions. | [Pricing](https://azure.microsoft.com/pricing/details/key-vault/) | +| [Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/) | S0 tier, defaults to gpt-4.1 and text-embedding-ada-002 models. Pricing is based on token count. | [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/) | +| [Azure Container App](https://learn.microsoft.com/en-us/azure/container-apps/) | Consumption tier with 0.5 CPU, 1GiB memory/storage. Pricing is based on resource allocation, and each month allows for a certain amount of free usage. Allows you to run containerized applications without worrying about orchestration or infrastructure. | [Pricing](https://azure.microsoft.com/pricing/details/container-apps/) | +| [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) | Basic tier. Build, store, and manage container images and artifacts in a private registry for all types of container deployments | [Pricing](https://azure.microsoft.com/pricing/details/container-registry/) | +| [Log analytics](https://learn.microsoft.com/en-us/azure/azure-monitor/) | Pay-as-you-go tier. Costs based on data ingested. Collect and analyze on telemetry data generated by Azure. | [Pricing](https://azure.microsoft.com/pricing/details/monitor/) | +| [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/) | Fully managed, distributed NoSQL, relational, and vector database for modern app development. | [Pricing](https://azure.microsoft.com/en-us/pricing/details/cosmos-db/autoscale-provisioned/) | + + + +
+ +>⚠️ **Important:** To avoid unnecessary costs, remember to take down your app if it's no longer in use, +either by deleting the resource group in the Portal or running `azd down`. + +

+

+Business Scenario +

+ + +|![image](./docs/images/landing_page.png)| +|---| + +
+ +Put your data to work by reducing blank page anxiety, speeding up document drafting, improving draft document quality, and reference information quickly - keeping experts in their expertise. Draft document templates for your organization including Invoices, End-user Contracts, Purchase Orders, Investment Proposals, and Grant Submissions. + +⚠️ The sample data used in this repository is synthetic and generated using Azure OpenAI Service. The data is intended for use as sample data only. + + +### Business value +
+ Click to learn more about what value this solution provides + + - **Draft templates quickly**
+ Put your data to work to create any kind of document that is supported by a large data library. + + - **Share**
+ Share with co-authors, contributors and approvers quickly. + + - **Contextualize information**
+ Provide context using natural language. Primary and secondary queries allow for access to supplemental detail – reducing cognitive load, increasing efficiency, and enabling focus on higher value work. + + - **Gain confidence in responses**
+ Trust responses to queries by customizing how data is referenced and returned to users, reducing the risk of hallucinated responses.

Access reference documents in the same chat window to get more detail and confirm accuracy. + + - **Secure data and responsible AI for innovation**
+ Improve data security to minimize breaches, fostering a culture of responsible AI adoption, maximize innovation opportunities, and sustain competitive edge. + + +
+ +

+ +

+Supporting documentation +

+ +### Security guidelines + +This template uses Azure Key Vault to store all connections to communicate between resources. + +This template also uses [Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) for local development and deployment. + +To ensure continued best practices in your own repository, we recommend that anyone creating solutions based on our templates ensure that the [Github secret scanning](https://docs.github.com/code-security/secret-scanning/about-secret-scanning) setting is enabled. + +You may want to consider additional security measures, such as: + +* Enabling Microsoft Defender for Cloud to [secure your Azure resources](https://learn.microsoft.com/azure/defender-for-cloud). +* Protecting the Azure Container Apps instance with a [firewall](https://learn.microsoft.com/azure/container-apps/waf-app-gateway) and/or [Virtual Network](https://learn.microsoft.com/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli). + +
+ +### Cross references +Check out similar solution accelerators + +| Solution Accelerator | Description | +|---|---| +| [Chat with your data](https://github.com/Azure-Samples/chat-with-your-data-solution-accelerator) | Chat with their own data by combining Azure Cognitive Search and Large Language Models (LLMs) to create a conversational search experience. It enables increased user efficiency by minimizing endpoints required to access internal company knowledgebases. | +| [Document knowledge mining](https://github.com/microsoft/Document-Knowledge-Mining-Solution-Accelerator) | Built on Azure OpenAI Service and Azure AI Document Intelligence to process and extract summaries, entities, and metadata from unstructured, multi-modal documents and enable searching and chatting over this data. | +| [Build your own copilot](https://github.com/microsoft/Build-your-own-copilot-Solution-Accelerator) | Helps client advisors to save time and prepare relevant discussion topics for scheduled meetings with overviews, client profile views, and chatting with structured data. | + + +
+ + +## Provide feedback + +Have questions, find a bug, or want to request a feature? [Submit a new issue](https://github.com/microsoft/document-generation-solution-accelerator/issues) on this repo and we'll connect. + +
+ +## Responsible AI Transparency FAQ +Please refer to [Transparency FAQ](./docs/TRANSPARENCY_FAQ.md) for responsible AI transparency details of this solution accelerator. + +
+ +## Disclaimers + +This release is an artificial intelligence (AI) system that generates text based on user input. The text generated by this system may include ungrounded content, meaning that it is not verified by any reliable source or based on any factual data. The data included in this release is synthetic, meaning that it is artificially created by the system and may contain factual errors or inconsistencies. Users of this release are responsible for determining the accuracy, validity, and suitability of any content generated by the system for their intended purposes. Users should not rely on the system output as a source of truth or as a substitute for human judgment or expertise. + +This release only supports English language input and output. Users should not attempt to use the system with any other language or format. The system output may not be compatible with any translation tools or services, and may lose its meaning or coherence if translated. + +This release does not reflect the opinions, views, or values of Microsoft Corporation or any of its affiliates, subsidiaries, or partners. The system output is solely based on the system's own logic and algorithms, and does not represent any endorsement, recommendation, or advice from Microsoft or any other entity. Microsoft disclaims any liability or responsibility for any damages, losses, or harms arising from the use of this release or its output by any user or third party. + +This release does not provide any financial advice, and is not designed to replace the role of qualified client advisors in appropriately advising clients. Users should not use the system output for any financial decisions or transactions, and should consult with a professional financial advisor before taking any action based on the system output. Microsoft is not a financial institution or a fiduciary, and does not offer any financial products or services through this release or its output. + +This release is intended as a proof of concept only, and is not a finished or polished product. It is not intended for commercial use or distribution, and is subject to change or discontinuation without notice. Any planned deployment of this release or its output should include comprehensive testing and evaluation to ensure it is fit for purpose and meets the user's requirements and expectations. Microsoft does not guarantee the quality, performance, reliability, or availability of this release or its output, and does not provide any warranty or support for it. + +This Software requires the use of third-party components which are governed by separate proprietary or open-source licenses as identified below, and you must comply with the terms of each applicable license in order to use the Software. You acknowledge and agree that this license does not grant you a license or other right to use any such third-party proprietary or open-source components. + +To the extent that the Software includes components or code used in or derived from Microsoft products or services, including without limitation Microsoft Azure Services (collectively, “Microsoft Products and Services”), you must also comply with the Product Terms applicable to such Microsoft Products and Services. You acknowledge and agree that the license governing the Software does not grant you a license or other right to use Microsoft Products and Services. Nothing in the license or this ReadMe file will serve to supersede, amend, terminate or modify any terms in the Product Terms for any Microsoft Products and Services. + +You must also comply with all domestic and international export laws and regulations that apply to the Software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit https://aka.ms/exporting. + +You acknowledge that the Software and Microsoft Products and Services (1) are not designed, intended or made available as a medical device(s), and (2) are not designed or intended to be a substitute for professional medical advice, diagnosis, treatment, or judgment and should not be used to replace or as a substitute for professional medical advice, diagnosis, treatment, or judgment. Customer is solely responsible for displaying and/or obtaining appropriate consents, warnings, disclaimers, and acknowledgements to end users of Customer’s implementation of the Online Services. + +You acknowledge the Software is not subject to SOC 1 and SOC 2 compliance audits. No Microsoft technology, nor any of its component technologies, including the Software, is intended or made available as a substitute for the professional advice, opinion, or judgment of a certified financial services professional. Do not use the Software to replace, substitute, or provide professional financial advice or judgment. + +BY ACCESSING OR USING THE SOFTWARE, YOU ACKNOWLEDGE THAT THE SOFTWARE IS NOT DESIGNED OR INTENDED TO SUPPORT ANY USE IN WHICH A SERVICE INTERRUPTION, DEFECT, ERROR, OR OTHER FAILURE OF THE SOFTWARE COULD RESULT IN THE DEATH OR SERIOUS BODILY INJURY OF ANY PERSON OR IN PHYSICAL OR ENVIRONMENTAL DAMAGE (COLLECTIVELY, “HIGH-RISK USE”), AND THAT YOU WILL ENSURE THAT, IN THE EVENT OF ANY INTERRUPTION, DEFECT, ERROR, OR OTHER FAILURE OF THE SOFTWARE, THE SAFETY OF PEOPLE, PROPERTY, AND THE ENVIRONMENT ARE NOT REDUCED BELOW A LEVEL THAT IS REASONABLY, APPROPRIATE, AND LEGAL, WHETHER IN GENERAL OR IN A SPECIFIC INDUSTRY. BY ACCESSING THE SOFTWARE, YOU FURTHER ACKNOWLEDGE THAT YOUR HIGH-RISK USE OF THE SOFTWARE IS AT YOUR OWN RISK. diff --git a/archive-doc-gen/SECURITY.md b/archive-doc-gen/SECURITY.md new file mode 100644 index 000000000..96d73bc27 --- /dev/null +++ b/archive-doc-gen/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +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). + +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. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +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). + +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). + +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). + +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: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +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. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + + \ No newline at end of file diff --git a/app-azure.yaml b/archive-doc-gen/app-azure.yaml similarity index 100% rename from app-azure.yaml rename to archive-doc-gen/app-azure.yaml diff --git a/azure.yaml b/archive-doc-gen/azure.yaml similarity index 100% rename from azure.yaml rename to archive-doc-gen/azure.yaml diff --git a/docs/ACRBuildAndPushGuide.md b/archive-doc-gen/docs/ACRBuildAndPushGuide.md similarity index 100% rename from docs/ACRBuildAndPushGuide.md rename to archive-doc-gen/docs/ACRBuildAndPushGuide.md diff --git a/docs/AppAuthentication.md b/archive-doc-gen/docs/AppAuthentication.md similarity index 100% rename from docs/AppAuthentication.md rename to archive-doc-gen/docs/AppAuthentication.md diff --git a/docs/AzureAccountSetUp.md b/archive-doc-gen/docs/AzureAccountSetUp.md similarity index 100% rename from docs/AzureAccountSetUp.md rename to archive-doc-gen/docs/AzureAccountSetUp.md diff --git a/docs/AzureGPTQuotaSettings.md b/archive-doc-gen/docs/AzureGPTQuotaSettings.md similarity index 100% rename from docs/AzureGPTQuotaSettings.md rename to archive-doc-gen/docs/AzureGPTQuotaSettings.md diff --git a/docs/AzureSemanticSearchRegion.md b/archive-doc-gen/docs/AzureSemanticSearchRegion.md similarity index 100% rename from docs/AzureSemanticSearchRegion.md rename to archive-doc-gen/docs/AzureSemanticSearchRegion.md diff --git a/docs/CustomizingAzdParameters.md b/archive-doc-gen/docs/CustomizingAzdParameters.md similarity index 100% rename from docs/CustomizingAzdParameters.md rename to archive-doc-gen/docs/CustomizingAzdParameters.md diff --git a/docs/DeleteResourceGroup.md b/archive-doc-gen/docs/DeleteResourceGroup.md similarity index 100% rename from docs/DeleteResourceGroup.md rename to archive-doc-gen/docs/DeleteResourceGroup.md diff --git a/docs/DeploymentGuide.md b/archive-doc-gen/docs/DeploymentGuide.md similarity index 100% rename from docs/DeploymentGuide.md rename to archive-doc-gen/docs/DeploymentGuide.md diff --git a/docs/PowershellSetup.md b/archive-doc-gen/docs/PowershellSetup.md similarity index 100% rename from docs/PowershellSetup.md rename to archive-doc-gen/docs/PowershellSetup.md diff --git a/docs/QuotaCheck.md b/archive-doc-gen/docs/QuotaCheck.md similarity index 100% rename from docs/QuotaCheck.md rename to archive-doc-gen/docs/QuotaCheck.md diff --git a/docs/README_LOCAL.md b/archive-doc-gen/docs/README_LOCAL.md similarity index 100% rename from docs/README_LOCAL.md rename to archive-doc-gen/docs/README_LOCAL.md diff --git a/docs/SampleQuestions.md b/archive-doc-gen/docs/SampleQuestions.md similarity index 100% rename from docs/SampleQuestions.md rename to archive-doc-gen/docs/SampleQuestions.md diff --git a/docs/TRANSPARENCY_FAQ.md b/archive-doc-gen/docs/TRANSPARENCY_FAQ.md similarity index 100% rename from docs/TRANSPARENCY_FAQ.md rename to archive-doc-gen/docs/TRANSPARENCY_FAQ.md diff --git a/docs/TroubleShootingSteps.md b/archive-doc-gen/docs/TroubleShootingSteps.md similarity index 100% rename from docs/TroubleShootingSteps.md rename to archive-doc-gen/docs/TroubleShootingSteps.md diff --git a/docs/container_registry_migration.md b/archive-doc-gen/docs/container_registry_migration.md similarity index 100% rename from docs/container_registry_migration.md rename to archive-doc-gen/docs/container_registry_migration.md diff --git a/docs/create_new_app_registration.md b/archive-doc-gen/docs/create_new_app_registration.md similarity index 100% rename from docs/create_new_app_registration.md rename to archive-doc-gen/docs/create_new_app_registration.md diff --git a/docs/images/AddDetails.png b/archive-doc-gen/docs/images/AddDetails.png similarity index 100% rename from docs/images/AddDetails.png rename to archive-doc-gen/docs/images/AddDetails.png diff --git a/docs/images/AddPlatform.png b/archive-doc-gen/docs/images/AddPlatform.png similarity index 100% rename from docs/images/AddPlatform.png rename to archive-doc-gen/docs/images/AddPlatform.png diff --git a/docs/images/AddRedirectURL.png b/archive-doc-gen/docs/images/AddRedirectURL.png similarity index 100% rename from docs/images/AddRedirectURL.png rename to archive-doc-gen/docs/images/AddRedirectURL.png diff --git a/docs/images/AppAuthIdentityProvider.png b/archive-doc-gen/docs/images/AppAuthIdentityProvider.png similarity index 100% rename from docs/images/AppAuthIdentityProvider.png rename to archive-doc-gen/docs/images/AppAuthIdentityProvider.png diff --git a/docs/images/AppAuthIdentityProviderAdd.png b/archive-doc-gen/docs/images/AppAuthIdentityProviderAdd.png similarity index 100% rename from docs/images/AppAuthIdentityProviderAdd.png rename to archive-doc-gen/docs/images/AppAuthIdentityProviderAdd.png diff --git a/docs/images/AppAuthIdentityProviderAdded.png b/archive-doc-gen/docs/images/AppAuthIdentityProviderAdded.png similarity index 100% rename from docs/images/AppAuthIdentityProviderAdded.png rename to archive-doc-gen/docs/images/AppAuthIdentityProviderAdded.png diff --git a/docs/images/AppAuthentication.png b/archive-doc-gen/docs/images/AppAuthentication.png similarity index 100% rename from docs/images/AppAuthentication.png rename to archive-doc-gen/docs/images/AppAuthentication.png diff --git a/docs/images/AppAuthenticationIdentity.png b/archive-doc-gen/docs/images/AppAuthenticationIdentity.png similarity index 100% rename from docs/images/AppAuthenticationIdentity.png rename to archive-doc-gen/docs/images/AppAuthenticationIdentity.png diff --git a/docs/images/AppServiceContainer.png b/archive-doc-gen/docs/images/AppServiceContainer.png similarity index 100% rename from docs/images/AppServiceContainer.png rename to archive-doc-gen/docs/images/AppServiceContainer.png diff --git a/docs/images/Appregistrations.png b/archive-doc-gen/docs/images/Appregistrations.png similarity index 100% rename from docs/images/Appregistrations.png rename to archive-doc-gen/docs/images/Appregistrations.png diff --git a/docs/images/Archimage.png b/archive-doc-gen/docs/images/Archimage.png similarity index 100% rename from docs/images/Archimage.png rename to archive-doc-gen/docs/images/Archimage.png diff --git a/docs/images/AzureHomePage.png b/archive-doc-gen/docs/images/AzureHomePage.png similarity index 100% rename from docs/images/AzureHomePage.png rename to archive-doc-gen/docs/images/AzureHomePage.png diff --git a/docs/images/ContainerApp.png b/archive-doc-gen/docs/images/ContainerApp.png similarity index 100% rename from docs/images/ContainerApp.png rename to archive-doc-gen/docs/images/ContainerApp.png diff --git a/docs/images/DeleteRG.png b/archive-doc-gen/docs/images/DeleteRG.png similarity index 100% rename from docs/images/DeleteRG.png rename to archive-doc-gen/docs/images/DeleteRG.png diff --git a/docs/images/DocGen_Azure_AI_Foundry_Architecture.png b/archive-doc-gen/docs/images/DocGen_Azure_AI_Foundry_Architecture.png similarity index 100% rename from docs/images/DocGen_Azure_AI_Foundry_Architecture.png rename to archive-doc-gen/docs/images/DocGen_Azure_AI_Foundry_Architecture.png diff --git a/docs/images/GenerateDraft.png b/archive-doc-gen/docs/images/GenerateDraft.png similarity index 100% rename from docs/images/GenerateDraft.png rename to archive-doc-gen/docs/images/GenerateDraft.png diff --git a/docs/images/MicrosoftEntraID.png b/archive-doc-gen/docs/images/MicrosoftEntraID.png similarity index 100% rename from docs/images/MicrosoftEntraID.png rename to archive-doc-gen/docs/images/MicrosoftEntraID.png diff --git a/docs/images/NewRegistration.png b/archive-doc-gen/docs/images/NewRegistration.png similarity index 100% rename from docs/images/NewRegistration.png rename to archive-doc-gen/docs/images/NewRegistration.png diff --git a/docs/images/Web.png b/archive-doc-gen/docs/images/Web.png similarity index 100% rename from docs/images/Web.png rename to archive-doc-gen/docs/images/Web.png diff --git a/docs/images/WebAppURL.png b/archive-doc-gen/docs/images/WebAppURL.png similarity index 100% rename from docs/images/WebAppURL.png rename to archive-doc-gen/docs/images/WebAppURL.png diff --git a/docs/images/architecture.png b/archive-doc-gen/docs/images/architecture.png similarity index 100% rename from docs/images/architecture.png rename to archive-doc-gen/docs/images/architecture.png diff --git a/docs/images/customerTruth.png b/archive-doc-gen/docs/images/customerTruth.png similarity index 100% rename from docs/images/customerTruth.png rename to archive-doc-gen/docs/images/customerTruth.png diff --git a/docs/images/deleteservices.png b/archive-doc-gen/docs/images/deleteservices.png similarity index 100% rename from docs/images/deleteservices.png rename to archive-doc-gen/docs/images/deleteservices.png diff --git a/docs/images/deployment_center.png b/archive-doc-gen/docs/images/deployment_center.png similarity index 100% rename from docs/images/deployment_center.png rename to archive-doc-gen/docs/images/deployment_center.png diff --git a/docs/images/git_bash.png b/archive-doc-gen/docs/images/git_bash.png similarity index 100% rename from docs/images/git_bash.png rename to archive-doc-gen/docs/images/git_bash.png diff --git a/docs/images/keyfeatures.png b/archive-doc-gen/docs/images/keyfeatures.png similarity index 100% rename from docs/images/keyfeatures.png rename to archive-doc-gen/docs/images/keyfeatures.png diff --git a/docs/images/landing_page.png b/archive-doc-gen/docs/images/landing_page.png similarity index 100% rename from docs/images/landing_page.png rename to archive-doc-gen/docs/images/landing_page.png diff --git a/docs/images/logAnalytics.png b/archive-doc-gen/docs/images/logAnalytics.png similarity index 100% rename from docs/images/logAnalytics.png rename to archive-doc-gen/docs/images/logAnalytics.png diff --git a/docs/images/logAnalyticsJson.png b/archive-doc-gen/docs/images/logAnalyticsJson.png similarity index 100% rename from docs/images/logAnalyticsJson.png rename to archive-doc-gen/docs/images/logAnalyticsJson.png diff --git a/docs/images/logAnalyticsList.png b/archive-doc-gen/docs/images/logAnalyticsList.png similarity index 100% rename from docs/images/logAnalyticsList.png rename to archive-doc-gen/docs/images/logAnalyticsList.png diff --git a/docs/images/oneClickDeploy.png b/archive-doc-gen/docs/images/oneClickDeploy.png similarity index 100% rename from docs/images/oneClickDeploy.png rename to archive-doc-gen/docs/images/oneClickDeploy.png diff --git a/docs/images/quota-check-output.png b/archive-doc-gen/docs/images/quota-check-output.png similarity index 100% rename from docs/images/quota-check-output.png rename to archive-doc-gen/docs/images/quota-check-output.png diff --git a/docs/images/re_use_foundry_project/azure_ai_foundry_list.png b/archive-doc-gen/docs/images/re_use_foundry_project/azure_ai_foundry_list.png similarity index 100% rename from docs/images/re_use_foundry_project/azure_ai_foundry_list.png rename to archive-doc-gen/docs/images/re_use_foundry_project/azure_ai_foundry_list.png diff --git a/docs/images/re_use_foundry_project/navigate_to_projects.png b/archive-doc-gen/docs/images/re_use_foundry_project/navigate_to_projects.png similarity index 100% rename from docs/images/re_use_foundry_project/navigate_to_projects.png rename to archive-doc-gen/docs/images/re_use_foundry_project/navigate_to_projects.png diff --git a/docs/images/re_use_foundry_project/project_resource_id.png b/archive-doc-gen/docs/images/re_use_foundry_project/project_resource_id.png similarity index 100% rename from docs/images/re_use_foundry_project/project_resource_id.png rename to archive-doc-gen/docs/images/re_use_foundry_project/project_resource_id.png diff --git a/docs/images/re_use_log/logAnalytics.png b/archive-doc-gen/docs/images/re_use_log/logAnalytics.png similarity index 100% rename from docs/images/re_use_log/logAnalytics.png rename to archive-doc-gen/docs/images/re_use_log/logAnalytics.png diff --git a/docs/images/re_use_log/logAnalyticsJson.png b/archive-doc-gen/docs/images/re_use_log/logAnalyticsJson.png similarity index 100% rename from docs/images/re_use_log/logAnalyticsJson.png rename to archive-doc-gen/docs/images/re_use_log/logAnalyticsJson.png diff --git a/docs/images/re_use_log/logAnalyticsList.png b/archive-doc-gen/docs/images/re_use_log/logAnalyticsList.png similarity index 100% rename from docs/images/re_use_log/logAnalyticsList.png rename to archive-doc-gen/docs/images/re_use_log/logAnalyticsList.png diff --git a/docs/images/readme/business-scenario.png b/archive-doc-gen/docs/images/readme/business-scenario.png similarity index 100% rename from docs/images/readme/business-scenario.png rename to archive-doc-gen/docs/images/readme/business-scenario.png diff --git a/docs/images/readme/quick-deploy.png b/archive-doc-gen/docs/images/readme/quick-deploy.png similarity index 100% rename from docs/images/readme/quick-deploy.png rename to archive-doc-gen/docs/images/readme/quick-deploy.png diff --git a/docs/images/readme/solution-overview.png b/archive-doc-gen/docs/images/readme/solution-overview.png similarity index 100% rename from docs/images/readme/solution-overview.png rename to archive-doc-gen/docs/images/readme/solution-overview.png diff --git a/docs/images/readme/supporting-documentation.png b/archive-doc-gen/docs/images/readme/supporting-documentation.png similarity index 100% rename from docs/images/readme/supporting-documentation.png rename to archive-doc-gen/docs/images/readme/supporting-documentation.png diff --git a/docs/images/resource-groups.png b/archive-doc-gen/docs/images/resource-groups.png similarity index 100% rename from docs/images/resource-groups.png rename to archive-doc-gen/docs/images/resource-groups.png diff --git a/docs/images/resource_menu.png b/archive-doc-gen/docs/images/resource_menu.png similarity index 100% rename from docs/images/resource_menu.png rename to archive-doc-gen/docs/images/resource_menu.png diff --git a/docs/images/resourcegroup.png b/archive-doc-gen/docs/images/resourcegroup.png similarity index 100% rename from docs/images/resourcegroup.png rename to archive-doc-gen/docs/images/resourcegroup.png diff --git a/docs/images/resourcegroup1.png b/archive-doc-gen/docs/images/resourcegroup1.png similarity index 100% rename from docs/images/resourcegroup1.png rename to archive-doc-gen/docs/images/resourcegroup1.png diff --git a/docs/images/supportingDocuments.png b/archive-doc-gen/docs/images/supportingDocuments.png similarity index 100% rename from docs/images/supportingDocuments.png rename to archive-doc-gen/docs/images/supportingDocuments.png diff --git a/docs/images/userStory.png b/archive-doc-gen/docs/images/userStory.png similarity index 100% rename from docs/images/userStory.png rename to archive-doc-gen/docs/images/userStory.png diff --git a/docs/re-use-foundry-project.md b/archive-doc-gen/docs/re-use-foundry-project.md similarity index 100% rename from docs/re-use-foundry-project.md rename to archive-doc-gen/docs/re-use-foundry-project.md diff --git a/docs/re-use-log-analytics.md b/archive-doc-gen/docs/re-use-log-analytics.md similarity index 100% rename from docs/re-use-log-analytics.md rename to archive-doc-gen/docs/re-use-log-analytics.md diff --git a/infra/data/pdfdata.zip b/archive-doc-gen/infra/data/pdfdata.zip similarity index 100% rename from infra/data/pdfdata.zip rename to archive-doc-gen/infra/data/pdfdata.zip diff --git a/infra/main.bicep b/archive-doc-gen/infra/main.bicep similarity index 100% rename from infra/main.bicep rename to archive-doc-gen/infra/main.bicep diff --git a/infra/main.json b/archive-doc-gen/infra/main.json similarity index 100% rename from infra/main.json rename to archive-doc-gen/infra/main.json diff --git a/infra/main.parameters.json b/archive-doc-gen/infra/main.parameters.json similarity index 100% rename from infra/main.parameters.json rename to archive-doc-gen/infra/main.parameters.json diff --git a/infra/main.waf.parameters.json b/archive-doc-gen/infra/main.waf.parameters.json similarity index 100% rename from infra/main.waf.parameters.json rename to archive-doc-gen/infra/main.waf.parameters.json diff --git a/infra/modules/ai-project.bicep b/archive-doc-gen/infra/modules/ai-project.bicep similarity index 100% rename from infra/modules/ai-project.bicep rename to archive-doc-gen/infra/modules/ai-project.bicep diff --git a/infra/modules/ai-services-deployments.bicep b/archive-doc-gen/infra/modules/ai-services-deployments.bicep similarity index 100% rename from infra/modules/ai-services-deployments.bicep rename to archive-doc-gen/infra/modules/ai-services-deployments.bicep diff --git a/infra/modules/deploy_aifp_aisearch_connection.bicep b/archive-doc-gen/infra/modules/deploy_aifp_aisearch_connection.bicep similarity index 100% rename from infra/modules/deploy_aifp_aisearch_connection.bicep rename to archive-doc-gen/infra/modules/deploy_aifp_aisearch_connection.bicep diff --git a/infra/modules/role-assignment.bicep b/archive-doc-gen/infra/modules/role-assignment.bicep similarity index 100% rename from infra/modules/role-assignment.bicep rename to archive-doc-gen/infra/modules/role-assignment.bicep diff --git a/infra/modules/virtualNetwork.bicep b/archive-doc-gen/infra/modules/virtualNetwork.bicep similarity index 100% rename from infra/modules/virtualNetwork.bicep rename to archive-doc-gen/infra/modules/virtualNetwork.bicep diff --git a/infra/modules/web-sites.bicep b/archive-doc-gen/infra/modules/web-sites.bicep similarity index 100% rename from infra/modules/web-sites.bicep rename to archive-doc-gen/infra/modules/web-sites.bicep diff --git a/infra/modules/web-sites.config.bicep b/archive-doc-gen/infra/modules/web-sites.config.bicep similarity index 100% rename from infra/modules/web-sites.config.bicep rename to archive-doc-gen/infra/modules/web-sites.config.bicep diff --git a/infra/scripts/add_cosmosdb_access.sh b/archive-doc-gen/infra/scripts/add_cosmosdb_access.sh similarity index 100% rename from infra/scripts/add_cosmosdb_access.sh rename to archive-doc-gen/infra/scripts/add_cosmosdb_access.sh diff --git a/infra/scripts/copy_kb_files.sh b/archive-doc-gen/infra/scripts/copy_kb_files.sh similarity index 100% rename from infra/scripts/copy_kb_files.sh rename to archive-doc-gen/infra/scripts/copy_kb_files.sh diff --git a/infra/scripts/index_scripts/01_create_search_index.py b/archive-doc-gen/infra/scripts/index_scripts/01_create_search_index.py similarity index 100% rename from infra/scripts/index_scripts/01_create_search_index.py rename to archive-doc-gen/infra/scripts/index_scripts/01_create_search_index.py diff --git a/infra/scripts/index_scripts/02_process_data.py b/archive-doc-gen/infra/scripts/index_scripts/02_process_data.py similarity index 100% rename from infra/scripts/index_scripts/02_process_data.py rename to archive-doc-gen/infra/scripts/index_scripts/02_process_data.py diff --git a/infra/scripts/index_scripts/requirements.txt b/archive-doc-gen/infra/scripts/index_scripts/requirements.txt similarity index 100% rename from infra/scripts/index_scripts/requirements.txt rename to archive-doc-gen/infra/scripts/index_scripts/requirements.txt diff --git a/infra/scripts/process_sample_data.sh b/archive-doc-gen/infra/scripts/process_sample_data.sh similarity index 100% rename from infra/scripts/process_sample_data.sh rename to archive-doc-gen/infra/scripts/process_sample_data.sh diff --git a/infra/scripts/run_create_index_scripts.sh b/archive-doc-gen/infra/scripts/run_create_index_scripts.sh similarity index 100% rename from infra/scripts/run_create_index_scripts.sh rename to archive-doc-gen/infra/scripts/run_create_index_scripts.sh diff --git a/infra/vscode_web/.gitignore b/archive-doc-gen/infra/vscode_web/.gitignore similarity index 100% rename from infra/vscode_web/.gitignore rename to archive-doc-gen/infra/vscode_web/.gitignore diff --git a/infra/vscode_web/LICENSE b/archive-doc-gen/infra/vscode_web/LICENSE similarity index 100% rename from infra/vscode_web/LICENSE rename to archive-doc-gen/infra/vscode_web/LICENSE diff --git a/infra/vscode_web/README-noazd.md b/archive-doc-gen/infra/vscode_web/README-noazd.md similarity index 100% rename from infra/vscode_web/README-noazd.md rename to archive-doc-gen/infra/vscode_web/README-noazd.md diff --git a/infra/vscode_web/README.md b/archive-doc-gen/infra/vscode_web/README.md similarity index 100% rename from infra/vscode_web/README.md rename to archive-doc-gen/infra/vscode_web/README.md diff --git a/infra/vscode_web/codeSample.py b/archive-doc-gen/infra/vscode_web/codeSample.py similarity index 100% rename from infra/vscode_web/codeSample.py rename to archive-doc-gen/infra/vscode_web/codeSample.py diff --git a/infra/vscode_web/endpoint-requirements.txt b/archive-doc-gen/infra/vscode_web/endpoint-requirements.txt similarity index 100% rename from infra/vscode_web/endpoint-requirements.txt rename to archive-doc-gen/infra/vscode_web/endpoint-requirements.txt diff --git a/infra/vscode_web/endpointCodeSample.py b/archive-doc-gen/infra/vscode_web/endpointCodeSample.py similarity index 100% rename from infra/vscode_web/endpointCodeSample.py rename to archive-doc-gen/infra/vscode_web/endpointCodeSample.py diff --git a/infra/vscode_web/index.json b/archive-doc-gen/infra/vscode_web/index.json similarity index 100% rename from infra/vscode_web/index.json rename to archive-doc-gen/infra/vscode_web/index.json diff --git a/infra/vscode_web/install.sh b/archive-doc-gen/infra/vscode_web/install.sh similarity index 100% rename from infra/vscode_web/install.sh rename to archive-doc-gen/infra/vscode_web/install.sh diff --git a/infra/vscode_web/requirements.txt b/archive-doc-gen/infra/vscode_web/requirements.txt similarity index 100% rename from infra/vscode_web/requirements.txt rename to archive-doc-gen/infra/vscode_web/requirements.txt diff --git a/package-lock.json b/archive-doc-gen/package-lock.json similarity index 100% rename from package-lock.json rename to archive-doc-gen/package-lock.json diff --git a/scripts/SAMPLE_DATA.md b/archive-doc-gen/scripts/SAMPLE_DATA.md similarity index 100% rename from scripts/SAMPLE_DATA.md rename to archive-doc-gen/scripts/SAMPLE_DATA.md diff --git a/scripts/auth_init.ps1 b/archive-doc-gen/scripts/auth_init.ps1 similarity index 100% rename from scripts/auth_init.ps1 rename to archive-doc-gen/scripts/auth_init.ps1 diff --git a/scripts/auth_init.py b/archive-doc-gen/scripts/auth_init.py similarity index 100% rename from scripts/auth_init.py rename to archive-doc-gen/scripts/auth_init.py diff --git a/scripts/auth_init.sh b/archive-doc-gen/scripts/auth_init.sh similarity index 100% rename from scripts/auth_init.sh rename to archive-doc-gen/scripts/auth_init.sh diff --git a/scripts/auth_update.ps1 b/archive-doc-gen/scripts/auth_update.ps1 similarity index 100% rename from scripts/auth_update.ps1 rename to archive-doc-gen/scripts/auth_update.ps1 diff --git a/scripts/auth_update.py b/archive-doc-gen/scripts/auth_update.py similarity index 100% rename from scripts/auth_update.py rename to archive-doc-gen/scripts/auth_update.py diff --git a/scripts/auth_update.sh b/archive-doc-gen/scripts/auth_update.sh similarity index 100% rename from scripts/auth_update.sh rename to archive-doc-gen/scripts/auth_update.sh diff --git a/scripts/checkquota.sh b/archive-doc-gen/scripts/checkquota.sh similarity index 100% rename from scripts/checkquota.sh rename to archive-doc-gen/scripts/checkquota.sh diff --git a/scripts/chunk_documents.py b/archive-doc-gen/scripts/chunk_documents.py similarity index 100% rename from scripts/chunk_documents.py rename to archive-doc-gen/scripts/chunk_documents.py diff --git a/scripts/config.json b/archive-doc-gen/scripts/config.json similarity index 100% rename from scripts/config.json rename to archive-doc-gen/scripts/config.json diff --git a/scripts/data_preparation.py b/archive-doc-gen/scripts/data_preparation.py similarity index 100% rename from scripts/data_preparation.py rename to archive-doc-gen/scripts/data_preparation.py diff --git a/scripts/data_utils.py b/archive-doc-gen/scripts/data_utils.py similarity index 100% rename from scripts/data_utils.py rename to archive-doc-gen/scripts/data_utils.py diff --git a/scripts/embed_documents.py b/archive-doc-gen/scripts/embed_documents.py similarity index 100% rename from scripts/embed_documents.py rename to archive-doc-gen/scripts/embed_documents.py diff --git a/scripts/loadenv.ps1 b/archive-doc-gen/scripts/loadenv.ps1 similarity index 100% rename from scripts/loadenv.ps1 rename to archive-doc-gen/scripts/loadenv.ps1 diff --git a/scripts/loadenv.sh b/archive-doc-gen/scripts/loadenv.sh similarity index 100% rename from scripts/loadenv.sh rename to archive-doc-gen/scripts/loadenv.sh diff --git a/scripts/prepdocs.ps1 b/archive-doc-gen/scripts/prepdocs.ps1 similarity index 100% rename from scripts/prepdocs.ps1 rename to archive-doc-gen/scripts/prepdocs.ps1 diff --git a/scripts/prepdocs.py b/archive-doc-gen/scripts/prepdocs.py similarity index 100% rename from scripts/prepdocs.py rename to archive-doc-gen/scripts/prepdocs.py diff --git a/scripts/prepdocs.sh b/archive-doc-gen/scripts/prepdocs.sh similarity index 100% rename from scripts/prepdocs.sh rename to archive-doc-gen/scripts/prepdocs.sh diff --git a/scripts/quota_check_params.sh b/archive-doc-gen/scripts/quota_check_params.sh similarity index 100% rename from scripts/quota_check_params.sh rename to archive-doc-gen/scripts/quota_check_params.sh diff --git a/scripts/readme.md b/archive-doc-gen/scripts/readme.md similarity index 100% rename from scripts/readme.md rename to archive-doc-gen/scripts/readme.md diff --git a/scripts/role_assignment.sh b/archive-doc-gen/scripts/role_assignment.sh similarity index 100% rename from scripts/role_assignment.sh rename to archive-doc-gen/scripts/role_assignment.sh diff --git a/src/.dockerignore b/archive-doc-gen/src/.dockerignore similarity index 100% rename from src/.dockerignore rename to archive-doc-gen/src/.dockerignore diff --git a/src/.env.sample b/archive-doc-gen/src/.env.sample similarity index 100% rename from src/.env.sample rename to archive-doc-gen/src/.env.sample diff --git a/src/.gitignore b/archive-doc-gen/src/.gitignore similarity index 100% rename from src/.gitignore rename to archive-doc-gen/src/.gitignore diff --git a/src/SUPPORT.md b/archive-doc-gen/src/SUPPORT.md similarity index 100% rename from src/SUPPORT.md rename to archive-doc-gen/src/SUPPORT.md diff --git a/src/TEST_CASE_FLOWS.md b/archive-doc-gen/src/TEST_CASE_FLOWS.md similarity index 100% rename from src/TEST_CASE_FLOWS.md rename to archive-doc-gen/src/TEST_CASE_FLOWS.md diff --git a/src/WebApp.Dockerfile b/archive-doc-gen/src/WebApp.Dockerfile similarity index 100% rename from src/WebApp.Dockerfile rename to archive-doc-gen/src/WebApp.Dockerfile diff --git a/src/app.py b/archive-doc-gen/src/app.py similarity index 100% rename from src/app.py rename to archive-doc-gen/src/app.py diff --git a/src/backend/__init__.py b/archive-doc-gen/src/backend/__init__.py similarity index 100% rename from src/backend/__init__.py rename to archive-doc-gen/src/backend/__init__.py diff --git a/src/backend/api/agent/agent_factory_base.py b/archive-doc-gen/src/backend/api/agent/agent_factory_base.py similarity index 100% rename from src/backend/api/agent/agent_factory_base.py rename to archive-doc-gen/src/backend/api/agent/agent_factory_base.py diff --git a/src/backend/api/agent/browse_agent_factory.py b/archive-doc-gen/src/backend/api/agent/browse_agent_factory.py similarity index 100% rename from src/backend/api/agent/browse_agent_factory.py rename to archive-doc-gen/src/backend/api/agent/browse_agent_factory.py diff --git a/src/backend/api/agent/section_agent_factory.py b/archive-doc-gen/src/backend/api/agent/section_agent_factory.py similarity index 100% rename from src/backend/api/agent/section_agent_factory.py rename to archive-doc-gen/src/backend/api/agent/section_agent_factory.py diff --git a/src/backend/api/agent/template_agent_factory.py b/archive-doc-gen/src/backend/api/agent/template_agent_factory.py similarity index 100% rename from src/backend/api/agent/template_agent_factory.py rename to archive-doc-gen/src/backend/api/agent/template_agent_factory.py diff --git a/src/backend/auth/__init__.py b/archive-doc-gen/src/backend/auth/__init__.py similarity index 100% rename from src/backend/auth/__init__.py rename to archive-doc-gen/src/backend/auth/__init__.py diff --git a/src/backend/auth/auth_utils.py b/archive-doc-gen/src/backend/auth/auth_utils.py similarity index 100% rename from src/backend/auth/auth_utils.py rename to archive-doc-gen/src/backend/auth/auth_utils.py diff --git a/src/backend/auth/sample_user.py b/archive-doc-gen/src/backend/auth/sample_user.py similarity index 100% rename from src/backend/auth/sample_user.py rename to archive-doc-gen/src/backend/auth/sample_user.py diff --git a/src/backend/helpers/azure_credential_utils.py b/archive-doc-gen/src/backend/helpers/azure_credential_utils.py similarity index 100% rename from src/backend/helpers/azure_credential_utils.py rename to archive-doc-gen/src/backend/helpers/azure_credential_utils.py diff --git a/src/backend/history/cosmosdbservice.py b/archive-doc-gen/src/backend/history/cosmosdbservice.py similarity index 100% rename from src/backend/history/cosmosdbservice.py rename to archive-doc-gen/src/backend/history/cosmosdbservice.py diff --git a/src/backend/security/__init__.py b/archive-doc-gen/src/backend/security/__init__.py similarity index 100% rename from src/backend/security/__init__.py rename to archive-doc-gen/src/backend/security/__init__.py diff --git a/src/backend/security/ms_defender_utils.py b/archive-doc-gen/src/backend/security/ms_defender_utils.py similarity index 100% rename from src/backend/security/ms_defender_utils.py rename to archive-doc-gen/src/backend/security/ms_defender_utils.py diff --git a/src/backend/settings.py b/archive-doc-gen/src/backend/settings.py similarity index 100% rename from src/backend/settings.py rename to archive-doc-gen/src/backend/settings.py diff --git a/src/backend/utils.py b/archive-doc-gen/src/backend/utils.py similarity index 100% rename from src/backend/utils.py rename to archive-doc-gen/src/backend/utils.py diff --git a/src/event_utils.py b/archive-doc-gen/src/event_utils.py similarity index 100% rename from src/event_utils.py rename to archive-doc-gen/src/event_utils.py diff --git a/src/frontend/.eslintignore b/archive-doc-gen/src/frontend/.eslintignore similarity index 100% rename from src/frontend/.eslintignore rename to archive-doc-gen/src/frontend/.eslintignore diff --git a/src/frontend/.eslintrc.json b/archive-doc-gen/src/frontend/.eslintrc.json similarity index 100% rename from src/frontend/.eslintrc.json rename to archive-doc-gen/src/frontend/.eslintrc.json diff --git a/src/frontend/.prettierignore b/archive-doc-gen/src/frontend/.prettierignore similarity index 100% rename from src/frontend/.prettierignore rename to archive-doc-gen/src/frontend/.prettierignore diff --git a/src/frontend/.prettierrc.json b/archive-doc-gen/src/frontend/.prettierrc.json similarity index 100% rename from src/frontend/.prettierrc.json rename to archive-doc-gen/src/frontend/.prettierrc.json diff --git a/src/frontend/__mocks__/dompurify.ts b/archive-doc-gen/src/frontend/__mocks__/dompurify.ts similarity index 100% rename from src/frontend/__mocks__/dompurify.ts rename to archive-doc-gen/src/frontend/__mocks__/dompurify.ts diff --git a/src/frontend/__mocks__/fileMock.ts b/archive-doc-gen/src/frontend/__mocks__/fileMock.ts similarity index 100% rename from src/frontend/__mocks__/fileMock.ts rename to archive-doc-gen/src/frontend/__mocks__/fileMock.ts diff --git a/src/frontend/__mocks__/mockAPIData.ts b/archive-doc-gen/src/frontend/__mocks__/mockAPIData.ts similarity index 100% rename from src/frontend/__mocks__/mockAPIData.ts rename to archive-doc-gen/src/frontend/__mocks__/mockAPIData.ts diff --git a/src/frontend/__mocks__/react-markdown.tsx b/archive-doc-gen/src/frontend/__mocks__/react-markdown.tsx similarity index 100% rename from src/frontend/__mocks__/react-markdown.tsx rename to archive-doc-gen/src/frontend/__mocks__/react-markdown.tsx diff --git a/src/frontend/eslint.config.ts b/archive-doc-gen/src/frontend/eslint.config.ts similarity index 100% rename from src/frontend/eslint.config.ts rename to archive-doc-gen/src/frontend/eslint.config.ts diff --git a/src/frontend/index.html b/archive-doc-gen/src/frontend/index.html similarity index 100% rename from src/frontend/index.html rename to archive-doc-gen/src/frontend/index.html diff --git a/src/frontend/jest.config.ts b/archive-doc-gen/src/frontend/jest.config.ts similarity index 100% rename from src/frontend/jest.config.ts rename to archive-doc-gen/src/frontend/jest.config.ts diff --git a/src/frontend/jest.polyfills.js b/archive-doc-gen/src/frontend/jest.polyfills.js similarity index 100% rename from src/frontend/jest.polyfills.js rename to archive-doc-gen/src/frontend/jest.polyfills.js diff --git a/src/frontend/package-lock.json b/archive-doc-gen/src/frontend/package-lock.json similarity index 100% rename from src/frontend/package-lock.json rename to archive-doc-gen/src/frontend/package-lock.json diff --git a/src/frontend/package.json b/archive-doc-gen/src/frontend/package.json similarity index 100% rename from src/frontend/package.json rename to archive-doc-gen/src/frontend/package.json diff --git a/src/frontend/polyfills.js b/archive-doc-gen/src/frontend/polyfills.js similarity index 100% rename from src/frontend/polyfills.js rename to archive-doc-gen/src/frontend/polyfills.js diff --git a/src/frontend/public/favicon.ico b/archive-doc-gen/src/frontend/public/favicon.ico similarity index 100% rename from src/frontend/public/favicon.ico rename to archive-doc-gen/src/frontend/public/favicon.ico diff --git a/src/frontend/src/api/api.ts b/archive-doc-gen/src/frontend/src/api/api.ts similarity index 100% rename from src/frontend/src/api/api.ts rename to archive-doc-gen/src/frontend/src/api/api.ts diff --git a/src/frontend/src/api/index.ts b/archive-doc-gen/src/frontend/src/api/index.ts similarity index 100% rename from src/frontend/src/api/index.ts rename to archive-doc-gen/src/frontend/src/api/index.ts diff --git a/src/frontend/src/api/models.ts b/archive-doc-gen/src/frontend/src/api/models.ts similarity index 100% rename from src/frontend/src/api/models.ts rename to archive-doc-gen/src/frontend/src/api/models.ts diff --git a/src/frontend/src/assets/Azure.svg b/archive-doc-gen/src/frontend/src/assets/Azure.svg similarity index 100% rename from src/frontend/src/assets/Azure.svg rename to archive-doc-gen/src/frontend/src/assets/Azure.svg diff --git a/src/frontend/src/assets/ClearChat.svg b/archive-doc-gen/src/frontend/src/assets/ClearChat.svg similarity index 100% rename from src/frontend/src/assets/ClearChat.svg rename to archive-doc-gen/src/frontend/src/assets/ClearChat.svg diff --git a/src/frontend/src/assets/Contoso.svg b/archive-doc-gen/src/frontend/src/assets/Contoso.svg similarity index 100% rename from src/frontend/src/assets/Contoso.svg rename to archive-doc-gen/src/frontend/src/assets/Contoso.svg diff --git a/src/frontend/src/assets/Generate.svg b/archive-doc-gen/src/frontend/src/assets/Generate.svg similarity index 100% rename from src/frontend/src/assets/Generate.svg rename to archive-doc-gen/src/frontend/src/assets/Generate.svg diff --git a/src/frontend/src/assets/Send.svg b/archive-doc-gen/src/frontend/src/assets/Send.svg similarity index 100% rename from src/frontend/src/assets/Send.svg rename to archive-doc-gen/src/frontend/src/assets/Send.svg diff --git a/src/frontend/src/components/Answer/Answer.module.css b/archive-doc-gen/src/frontend/src/components/Answer/Answer.module.css similarity index 100% rename from src/frontend/src/components/Answer/Answer.module.css rename to archive-doc-gen/src/frontend/src/components/Answer/Answer.module.css diff --git a/src/frontend/src/components/Answer/Answer.test.tsx b/archive-doc-gen/src/frontend/src/components/Answer/Answer.test.tsx similarity index 100% rename from src/frontend/src/components/Answer/Answer.test.tsx rename to archive-doc-gen/src/frontend/src/components/Answer/Answer.test.tsx diff --git a/src/frontend/src/components/Answer/Answer.tsx b/archive-doc-gen/src/frontend/src/components/Answer/Answer.tsx similarity index 100% rename from src/frontend/src/components/Answer/Answer.tsx rename to archive-doc-gen/src/frontend/src/components/Answer/Answer.tsx diff --git a/src/frontend/src/components/Answer/AnswerParser.test.ts b/archive-doc-gen/src/frontend/src/components/Answer/AnswerParser.test.ts similarity index 100% rename from src/frontend/src/components/Answer/AnswerParser.test.ts rename to archive-doc-gen/src/frontend/src/components/Answer/AnswerParser.test.ts diff --git a/src/frontend/src/components/Answer/AnswerParser.tsx b/archive-doc-gen/src/frontend/src/components/Answer/AnswerParser.tsx similarity index 100% rename from src/frontend/src/components/Answer/AnswerParser.tsx rename to archive-doc-gen/src/frontend/src/components/Answer/AnswerParser.tsx diff --git a/src/frontend/src/components/Answer/index.ts b/archive-doc-gen/src/frontend/src/components/Answer/index.ts similarity index 100% rename from src/frontend/src/components/Answer/index.ts rename to archive-doc-gen/src/frontend/src/components/Answer/index.ts diff --git a/src/frontend/src/components/ChatHistory/ChatHistoryList.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryList.test.tsx similarity index 100% rename from src/frontend/src/components/ChatHistory/ChatHistoryList.test.tsx rename to archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryList.test.tsx diff --git a/src/frontend/src/components/ChatHistory/ChatHistoryList.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryList.tsx similarity index 100% rename from src/frontend/src/components/ChatHistory/ChatHistoryList.tsx rename to archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryList.tsx diff --git a/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx similarity index 100% rename from src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx rename to archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryListItem.tsx diff --git a/src/frontend/src/components/ChatHistory/ChatHistoryPanel.module.css b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryPanel.module.css similarity index 100% rename from src/frontend/src/components/ChatHistory/ChatHistoryPanel.module.css rename to archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryPanel.module.css diff --git a/src/frontend/src/components/ChatHistory/ChatHistoryPanel.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryPanel.test.tsx similarity index 100% rename from src/frontend/src/components/ChatHistory/ChatHistoryPanel.test.tsx rename to archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryPanel.test.tsx diff --git a/src/frontend/src/components/ChatHistory/ChatHistoryPanel.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryPanel.tsx similarity index 100% rename from src/frontend/src/components/ChatHistory/ChatHistoryPanel.tsx rename to archive-doc-gen/src/frontend/src/components/ChatHistory/ChatHistoryPanel.tsx diff --git a/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx b/archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx similarity index 100% rename from src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx rename to archive-doc-gen/src/frontend/src/components/ChatHistory/chatHistoryListItem.test.tsx diff --git a/src/frontend/src/components/DraftCards/SectionCard.test.tsx b/archive-doc-gen/src/frontend/src/components/DraftCards/SectionCard.test.tsx similarity index 100% rename from src/frontend/src/components/DraftCards/SectionCard.test.tsx rename to archive-doc-gen/src/frontend/src/components/DraftCards/SectionCard.test.tsx diff --git a/src/frontend/src/components/DraftCards/SectionCard.tsx b/archive-doc-gen/src/frontend/src/components/DraftCards/SectionCard.tsx similarity index 100% rename from src/frontend/src/components/DraftCards/SectionCard.tsx rename to archive-doc-gen/src/frontend/src/components/DraftCards/SectionCard.tsx diff --git a/src/frontend/src/components/DraftCards/TitleCard.test.tsx b/archive-doc-gen/src/frontend/src/components/DraftCards/TitleCard.test.tsx similarity index 100% rename from src/frontend/src/components/DraftCards/TitleCard.test.tsx rename to archive-doc-gen/src/frontend/src/components/DraftCards/TitleCard.test.tsx diff --git a/src/frontend/src/components/DraftCards/TitleCard.tsx b/archive-doc-gen/src/frontend/src/components/DraftCards/TitleCard.tsx similarity index 100% rename from src/frontend/src/components/DraftCards/TitleCard.tsx rename to archive-doc-gen/src/frontend/src/components/DraftCards/TitleCard.tsx diff --git a/src/frontend/src/components/FeatureCard/FeatureCard.test.tsx b/archive-doc-gen/src/frontend/src/components/FeatureCard/FeatureCard.test.tsx similarity index 100% rename from src/frontend/src/components/FeatureCard/FeatureCard.test.tsx rename to archive-doc-gen/src/frontend/src/components/FeatureCard/FeatureCard.test.tsx diff --git a/src/frontend/src/components/FeatureCard/FeatureCard.tsx b/archive-doc-gen/src/frontend/src/components/FeatureCard/FeatureCard.tsx similarity index 100% rename from src/frontend/src/components/FeatureCard/FeatureCard.tsx rename to archive-doc-gen/src/frontend/src/components/FeatureCard/FeatureCard.tsx diff --git a/src/frontend/src/components/QuestionInput/QuestionInput.module.css b/archive-doc-gen/src/frontend/src/components/QuestionInput/QuestionInput.module.css similarity index 100% rename from src/frontend/src/components/QuestionInput/QuestionInput.module.css rename to archive-doc-gen/src/frontend/src/components/QuestionInput/QuestionInput.module.css diff --git a/src/frontend/src/components/QuestionInput/QuestionInput.test.tsx b/archive-doc-gen/src/frontend/src/components/QuestionInput/QuestionInput.test.tsx similarity index 100% rename from src/frontend/src/components/QuestionInput/QuestionInput.test.tsx rename to archive-doc-gen/src/frontend/src/components/QuestionInput/QuestionInput.test.tsx diff --git a/src/frontend/src/components/QuestionInput/QuestionInput.tsx b/archive-doc-gen/src/frontend/src/components/QuestionInput/QuestionInput.tsx similarity index 100% rename from src/frontend/src/components/QuestionInput/QuestionInput.tsx rename to archive-doc-gen/src/frontend/src/components/QuestionInput/QuestionInput.tsx diff --git a/src/frontend/src/components/QuestionInput/index.ts b/archive-doc-gen/src/frontend/src/components/QuestionInput/index.ts similarity index 100% rename from src/frontend/src/components/QuestionInput/index.ts rename to archive-doc-gen/src/frontend/src/components/QuestionInput/index.ts diff --git a/src/frontend/src/components/Sidebar/Sidebar.module.css b/archive-doc-gen/src/frontend/src/components/Sidebar/Sidebar.module.css similarity index 100% rename from src/frontend/src/components/Sidebar/Sidebar.module.css rename to archive-doc-gen/src/frontend/src/components/Sidebar/Sidebar.module.css diff --git a/src/frontend/src/components/Sidebar/Sidebar.test.tsx b/archive-doc-gen/src/frontend/src/components/Sidebar/Sidebar.test.tsx similarity index 100% rename from src/frontend/src/components/Sidebar/Sidebar.test.tsx rename to archive-doc-gen/src/frontend/src/components/Sidebar/Sidebar.test.tsx diff --git a/src/frontend/src/components/Sidebar/Sidebar.tsx b/archive-doc-gen/src/frontend/src/components/Sidebar/Sidebar.tsx similarity index 100% rename from src/frontend/src/components/Sidebar/Sidebar.tsx rename to archive-doc-gen/src/frontend/src/components/Sidebar/Sidebar.tsx diff --git a/src/frontend/src/components/common/Button.module.css b/archive-doc-gen/src/frontend/src/components/common/Button.module.css similarity index 100% rename from src/frontend/src/components/common/Button.module.css rename to archive-doc-gen/src/frontend/src/components/common/Button.module.css diff --git a/src/frontend/src/components/common/Button.test.tsx b/archive-doc-gen/src/frontend/src/components/common/Button.test.tsx similarity index 100% rename from src/frontend/src/components/common/Button.test.tsx rename to archive-doc-gen/src/frontend/src/components/common/Button.test.tsx diff --git a/src/frontend/src/components/common/Button.tsx b/archive-doc-gen/src/frontend/src/components/common/Button.tsx similarity index 100% rename from src/frontend/src/components/common/Button.tsx rename to archive-doc-gen/src/frontend/src/components/common/Button.tsx diff --git a/src/frontend/src/constants/chatHistory.test.tsx b/archive-doc-gen/src/frontend/src/constants/chatHistory.test.tsx similarity index 100% rename from src/frontend/src/constants/chatHistory.test.tsx rename to archive-doc-gen/src/frontend/src/constants/chatHistory.test.tsx diff --git a/src/frontend/src/constants/chatHistory.tsx b/archive-doc-gen/src/frontend/src/constants/chatHistory.tsx similarity index 100% rename from src/frontend/src/constants/chatHistory.tsx rename to archive-doc-gen/src/frontend/src/constants/chatHistory.tsx diff --git a/src/frontend/src/constants/xssAllowTags.ts b/archive-doc-gen/src/frontend/src/constants/xssAllowTags.ts similarity index 100% rename from src/frontend/src/constants/xssAllowTags.ts rename to archive-doc-gen/src/frontend/src/constants/xssAllowTags.ts diff --git a/src/frontend/src/helpers/helpers.ts b/archive-doc-gen/src/frontend/src/helpers/helpers.ts similarity index 100% rename from src/frontend/src/helpers/helpers.ts rename to archive-doc-gen/src/frontend/src/helpers/helpers.ts diff --git a/src/frontend/src/index.css b/archive-doc-gen/src/frontend/src/index.css similarity index 100% rename from src/frontend/src/index.css rename to archive-doc-gen/src/frontend/src/index.css diff --git a/src/frontend/src/index.tsx b/archive-doc-gen/src/frontend/src/index.tsx similarity index 100% rename from src/frontend/src/index.tsx rename to archive-doc-gen/src/frontend/src/index.tsx diff --git a/src/frontend/src/pages/NoPage.tsx b/archive-doc-gen/src/frontend/src/pages/NoPage.tsx similarity index 100% rename from src/frontend/src/pages/NoPage.tsx rename to archive-doc-gen/src/frontend/src/pages/NoPage.tsx diff --git a/src/frontend/src/pages/chat/Chat.module.css b/archive-doc-gen/src/frontend/src/pages/chat/Chat.module.css similarity index 100% rename from src/frontend/src/pages/chat/Chat.module.css rename to archive-doc-gen/src/frontend/src/pages/chat/Chat.module.css diff --git a/src/frontend/src/pages/chat/Chat.test.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Chat.test.tsx similarity index 100% rename from src/frontend/src/pages/chat/Chat.test.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Chat.test.tsx diff --git a/src/frontend/src/pages/chat/Chat.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx similarity index 100% rename from src/frontend/src/pages/chat/Chat.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Chat.tsx diff --git a/src/frontend/src/pages/chat/Components/AuthNotConfigure.test.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Components/AuthNotConfigure.test.tsx similarity index 100% rename from src/frontend/src/pages/chat/Components/AuthNotConfigure.test.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Components/AuthNotConfigure.test.tsx diff --git a/src/frontend/src/pages/chat/Components/AuthNotConfigure.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Components/AuthNotConfigure.tsx similarity index 100% rename from src/frontend/src/pages/chat/Components/AuthNotConfigure.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Components/AuthNotConfigure.tsx diff --git a/src/frontend/src/pages/chat/Components/ChatMessageContainer.test.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Components/ChatMessageContainer.test.tsx similarity index 100% rename from src/frontend/src/pages/chat/Components/ChatMessageContainer.test.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Components/ChatMessageContainer.test.tsx diff --git a/src/frontend/src/pages/chat/Components/ChatMessageContainer.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Components/ChatMessageContainer.tsx similarity index 100% rename from src/frontend/src/pages/chat/Components/ChatMessageContainer.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Components/ChatMessageContainer.tsx diff --git a/src/frontend/src/pages/chat/Components/CitationPanel.test.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Components/CitationPanel.test.tsx similarity index 100% rename from src/frontend/src/pages/chat/Components/CitationPanel.test.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Components/CitationPanel.test.tsx diff --git a/src/frontend/src/pages/chat/Components/CitationPanel.tsx b/archive-doc-gen/src/frontend/src/pages/chat/Components/CitationPanel.tsx similarity index 100% rename from src/frontend/src/pages/chat/Components/CitationPanel.tsx rename to archive-doc-gen/src/frontend/src/pages/chat/Components/CitationPanel.tsx diff --git a/src/frontend/src/pages/document/Document.module.css b/archive-doc-gen/src/frontend/src/pages/document/Document.module.css similarity index 100% rename from src/frontend/src/pages/document/Document.module.css rename to archive-doc-gen/src/frontend/src/pages/document/Document.module.css diff --git a/src/frontend/src/pages/document/Document.test.tsx b/archive-doc-gen/src/frontend/src/pages/document/Document.test.tsx similarity index 100% rename from src/frontend/src/pages/document/Document.test.tsx rename to archive-doc-gen/src/frontend/src/pages/document/Document.test.tsx diff --git a/src/frontend/src/pages/document/Document.tsx b/archive-doc-gen/src/frontend/src/pages/document/Document.tsx similarity index 100% rename from src/frontend/src/pages/document/Document.tsx rename to archive-doc-gen/src/frontend/src/pages/document/Document.tsx diff --git a/src/frontend/src/pages/draft/Draft.module.css b/archive-doc-gen/src/frontend/src/pages/draft/Draft.module.css similarity index 100% rename from src/frontend/src/pages/draft/Draft.module.css rename to archive-doc-gen/src/frontend/src/pages/draft/Draft.module.css diff --git a/src/frontend/src/pages/draft/Draft.test.tsx b/archive-doc-gen/src/frontend/src/pages/draft/Draft.test.tsx similarity index 100% rename from src/frontend/src/pages/draft/Draft.test.tsx rename to archive-doc-gen/src/frontend/src/pages/draft/Draft.test.tsx diff --git a/src/frontend/src/pages/draft/Draft.tsx b/archive-doc-gen/src/frontend/src/pages/draft/Draft.tsx similarity index 100% rename from src/frontend/src/pages/draft/Draft.tsx rename to archive-doc-gen/src/frontend/src/pages/draft/Draft.tsx diff --git a/src/frontend/src/pages/landing/Landing.module.css b/archive-doc-gen/src/frontend/src/pages/landing/Landing.module.css similarity index 100% rename from src/frontend/src/pages/landing/Landing.module.css rename to archive-doc-gen/src/frontend/src/pages/landing/Landing.module.css diff --git a/src/frontend/src/pages/landing/Landing.test.tsx b/archive-doc-gen/src/frontend/src/pages/landing/Landing.test.tsx similarity index 100% rename from src/frontend/src/pages/landing/Landing.test.tsx rename to archive-doc-gen/src/frontend/src/pages/landing/Landing.test.tsx diff --git a/src/frontend/src/pages/landing/Landing.tsx b/archive-doc-gen/src/frontend/src/pages/landing/Landing.tsx similarity index 100% rename from src/frontend/src/pages/landing/Landing.tsx rename to archive-doc-gen/src/frontend/src/pages/landing/Landing.tsx diff --git a/src/frontend/src/pages/layout/Layout.module.css b/archive-doc-gen/src/frontend/src/pages/layout/Layout.module.css similarity index 100% rename from src/frontend/src/pages/layout/Layout.module.css rename to archive-doc-gen/src/frontend/src/pages/layout/Layout.module.css diff --git a/src/frontend/src/pages/layout/Layout.test.tsx b/archive-doc-gen/src/frontend/src/pages/layout/Layout.test.tsx similarity index 100% rename from src/frontend/src/pages/layout/Layout.test.tsx rename to archive-doc-gen/src/frontend/src/pages/layout/Layout.test.tsx diff --git a/src/frontend/src/pages/layout/Layout.tsx b/archive-doc-gen/src/frontend/src/pages/layout/Layout.tsx similarity index 100% rename from src/frontend/src/pages/layout/Layout.tsx rename to archive-doc-gen/src/frontend/src/pages/layout/Layout.tsx diff --git a/src/frontend/src/state/AppProvider.tsx b/archive-doc-gen/src/frontend/src/state/AppProvider.tsx similarity index 100% rename from src/frontend/src/state/AppProvider.tsx rename to archive-doc-gen/src/frontend/src/state/AppProvider.tsx diff --git a/src/frontend/src/state/AppReducer.tsx b/archive-doc-gen/src/frontend/src/state/AppReducer.tsx similarity index 100% rename from src/frontend/src/state/AppReducer.tsx rename to archive-doc-gen/src/frontend/src/state/AppReducer.tsx diff --git a/src/frontend/src/test/setupTests.ts b/archive-doc-gen/src/frontend/src/test/setupTests.ts similarity index 100% rename from src/frontend/src/test/setupTests.ts rename to archive-doc-gen/src/frontend/src/test/setupTests.ts diff --git a/src/frontend/src/test/test.utils.tsx b/archive-doc-gen/src/frontend/src/test/test.utils.tsx similarity index 100% rename from src/frontend/src/test/test.utils.tsx rename to archive-doc-gen/src/frontend/src/test/test.utils.tsx diff --git a/src/frontend/src/vite-env.d.ts b/archive-doc-gen/src/frontend/src/vite-env.d.ts similarity index 100% rename from src/frontend/src/vite-env.d.ts rename to archive-doc-gen/src/frontend/src/vite-env.d.ts diff --git a/src/frontend/tsconfig.json b/archive-doc-gen/src/frontend/tsconfig.json similarity index 100% rename from src/frontend/tsconfig.json rename to archive-doc-gen/src/frontend/tsconfig.json diff --git a/src/frontend/tsconfig.node.json b/archive-doc-gen/src/frontend/tsconfig.node.json similarity index 100% rename from src/frontend/tsconfig.node.json rename to archive-doc-gen/src/frontend/tsconfig.node.json diff --git a/src/frontend/vite.config.ts b/archive-doc-gen/src/frontend/vite.config.ts similarity index 100% rename from src/frontend/vite.config.ts rename to archive-doc-gen/src/frontend/vite.config.ts diff --git a/src/gunicorn.conf.py b/archive-doc-gen/src/gunicorn.conf.py similarity index 100% rename from src/gunicorn.conf.py rename to archive-doc-gen/src/gunicorn.conf.py diff --git a/src/requirements-dev.txt b/archive-doc-gen/src/requirements-dev.txt similarity index 100% rename from src/requirements-dev.txt rename to archive-doc-gen/src/requirements-dev.txt diff --git a/src/requirements.txt b/archive-doc-gen/src/requirements.txt similarity index 100% rename from src/requirements.txt rename to archive-doc-gen/src/requirements.txt diff --git a/src/start.cmd b/archive-doc-gen/src/start.cmd similarity index 100% rename from src/start.cmd rename to archive-doc-gen/src/start.cmd diff --git a/src/start.sh b/archive-doc-gen/src/start.sh similarity index 100% rename from src/start.sh rename to archive-doc-gen/src/start.sh diff --git a/src/test.cmd b/archive-doc-gen/src/test.cmd similarity index 100% rename from src/test.cmd rename to archive-doc-gen/src/test.cmd diff --git a/src/tests/conftest.py b/archive-doc-gen/src/tests/conftest.py similarity index 100% rename from src/tests/conftest.py rename to archive-doc-gen/src/tests/conftest.py diff --git a/src/tests/integration_tests/conftest.py b/archive-doc-gen/src/tests/integration_tests/conftest.py similarity index 100% rename from src/tests/integration_tests/conftest.py rename to archive-doc-gen/src/tests/integration_tests/conftest.py diff --git a/src/tests/integration_tests/dotenv_templates/dotenv.jinja2 b/archive-doc-gen/src/tests/integration_tests/dotenv_templates/dotenv.jinja2 similarity index 100% rename from src/tests/integration_tests/dotenv_templates/dotenv.jinja2 rename to archive-doc-gen/src/tests/integration_tests/dotenv_templates/dotenv.jinja2 diff --git a/src/tests/integration_tests/test_datasources.py b/archive-doc-gen/src/tests/integration_tests/test_datasources.py similarity index 100% rename from src/tests/integration_tests/test_datasources.py rename to archive-doc-gen/src/tests/integration_tests/test_datasources.py diff --git a/src/tests/integration_tests/test_startup_scripts.py b/archive-doc-gen/src/tests/integration_tests/test_startup_scripts.py similarity index 100% rename from src/tests/integration_tests/test_startup_scripts.py rename to archive-doc-gen/src/tests/integration_tests/test_startup_scripts.py diff --git a/src/tests/unit_tests/dotenv_data/dotenv_no_datasource_1 b/archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_no_datasource_1 similarity index 100% rename from src/tests/unit_tests/dotenv_data/dotenv_no_datasource_1 rename to archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_no_datasource_1 diff --git a/src/tests/unit_tests/dotenv_data/dotenv_no_datasource_2 b/archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_no_datasource_2 similarity index 100% rename from src/tests/unit_tests/dotenv_data/dotenv_no_datasource_2 rename to archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_no_datasource_2 diff --git a/src/tests/unit_tests/dotenv_data/dotenv_with_azure_search_success b/archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_with_azure_search_success similarity index 100% rename from src/tests/unit_tests/dotenv_data/dotenv_with_azure_search_success rename to archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_with_azure_search_success diff --git a/src/tests/unit_tests/dotenv_data/dotenv_with_elasticsearch_success b/archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_with_elasticsearch_success similarity index 100% rename from src/tests/unit_tests/dotenv_data/dotenv_with_elasticsearch_success rename to archive-doc-gen/src/tests/unit_tests/dotenv_data/dotenv_with_elasticsearch_success diff --git a/src/tests/unit_tests/helpers/test_azure_credential_utils.py b/archive-doc-gen/src/tests/unit_tests/helpers/test_azure_credential_utils.py similarity index 100% rename from src/tests/unit_tests/helpers/test_azure_credential_utils.py rename to archive-doc-gen/src/tests/unit_tests/helpers/test_azure_credential_utils.py diff --git a/src/tests/unit_tests/test_settings.py b/archive-doc-gen/src/tests/unit_tests/test_settings.py similarity index 100% rename from src/tests/unit_tests/test_settings.py rename to archive-doc-gen/src/tests/unit_tests/test_settings.py diff --git a/src/tests/unit_tests/test_utils.py b/archive-doc-gen/src/tests/unit_tests/test_utils.py similarity index 100% rename from src/tests/unit_tests/test_utils.py rename to archive-doc-gen/src/tests/unit_tests/test_utils.py diff --git a/tests/e2e-test/.gitignore b/archive-doc-gen/tests/e2e-test/.gitignore similarity index 100% rename from tests/e2e-test/.gitignore rename to archive-doc-gen/tests/e2e-test/.gitignore diff --git a/tests/e2e-test/README.md b/archive-doc-gen/tests/e2e-test/README.md similarity index 100% rename from tests/e2e-test/README.md rename to archive-doc-gen/tests/e2e-test/README.md diff --git a/tests/e2e-test/base/__init__.py b/archive-doc-gen/tests/e2e-test/base/__init__.py similarity index 100% rename from tests/e2e-test/base/__init__.py rename to archive-doc-gen/tests/e2e-test/base/__init__.py diff --git a/tests/e2e-test/base/base.py b/archive-doc-gen/tests/e2e-test/base/base.py similarity index 100% rename from tests/e2e-test/base/base.py rename to archive-doc-gen/tests/e2e-test/base/base.py diff --git a/tests/e2e-test/config/constants.py b/archive-doc-gen/tests/e2e-test/config/constants.py similarity index 100% rename from tests/e2e-test/config/constants.py rename to archive-doc-gen/tests/e2e-test/config/constants.py diff --git a/tests/e2e-test/img.png b/archive-doc-gen/tests/e2e-test/img.png similarity index 100% rename from tests/e2e-test/img.png rename to archive-doc-gen/tests/e2e-test/img.png diff --git a/tests/e2e-test/img_1.png b/archive-doc-gen/tests/e2e-test/img_1.png similarity index 100% rename from tests/e2e-test/img_1.png rename to archive-doc-gen/tests/e2e-test/img_1.png diff --git a/tests/e2e-test/pages/__init__.py b/archive-doc-gen/tests/e2e-test/pages/__init__.py similarity index 100% rename from tests/e2e-test/pages/__init__.py rename to archive-doc-gen/tests/e2e-test/pages/__init__.py diff --git a/tests/e2e-test/pages/browsePage.py b/archive-doc-gen/tests/e2e-test/pages/browsePage.py similarity index 100% rename from tests/e2e-test/pages/browsePage.py rename to archive-doc-gen/tests/e2e-test/pages/browsePage.py diff --git a/tests/e2e-test/pages/draftPage.py b/archive-doc-gen/tests/e2e-test/pages/draftPage.py similarity index 100% rename from tests/e2e-test/pages/draftPage.py rename to archive-doc-gen/tests/e2e-test/pages/draftPage.py diff --git a/tests/e2e-test/pages/generatePage.py b/archive-doc-gen/tests/e2e-test/pages/generatePage.py similarity index 100% rename from tests/e2e-test/pages/generatePage.py rename to archive-doc-gen/tests/e2e-test/pages/generatePage.py diff --git a/tests/e2e-test/pages/homePage.py b/archive-doc-gen/tests/e2e-test/pages/homePage.py similarity index 100% rename from tests/e2e-test/pages/homePage.py rename to archive-doc-gen/tests/e2e-test/pages/homePage.py diff --git a/tests/e2e-test/pytest.ini b/archive-doc-gen/tests/e2e-test/pytest.ini similarity index 100% rename from tests/e2e-test/pytest.ini rename to archive-doc-gen/tests/e2e-test/pytest.ini diff --git a/tests/e2e-test/requirements.txt b/archive-doc-gen/tests/e2e-test/requirements.txt similarity index 100% rename from tests/e2e-test/requirements.txt rename to archive-doc-gen/tests/e2e-test/requirements.txt diff --git a/tests/e2e-test/sample_dotenv_file.txt b/archive-doc-gen/tests/e2e-test/sample_dotenv_file.txt similarity index 100% rename from tests/e2e-test/sample_dotenv_file.txt rename to archive-doc-gen/tests/e2e-test/sample_dotenv_file.txt diff --git a/tests/e2e-test/tests/__init__.py b/archive-doc-gen/tests/e2e-test/tests/__init__.py similarity index 100% rename from tests/e2e-test/tests/__init__.py rename to archive-doc-gen/tests/e2e-test/tests/__init__.py diff --git a/tests/e2e-test/tests/conftest.py b/archive-doc-gen/tests/e2e-test/tests/conftest.py similarity index 100% rename from tests/e2e-test/tests/conftest.py rename to archive-doc-gen/tests/e2e-test/tests/conftest.py diff --git a/tests/e2e-test/tests/test_gp_docgen.py b/archive-doc-gen/tests/e2e-test/tests/test_gp_docgen.py similarity index 100% rename from tests/e2e-test/tests/test_gp_docgen.py rename to archive-doc-gen/tests/e2e-test/tests/test_gp_docgen.py diff --git a/docs/images/readme/business_scenario.png b/docs/images/readme/business_scenario.png new file mode 100644 index 0000000000000000000000000000000000000000..017032ccec27e0a9192700eae30a9bd17f8987d5 GIT binary patch literal 14787 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�#lm3@I6LpLt?$E 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 VSOKv|bjllW?TWQ|*=5(L{{aU2b{qfz literal 0 HcmV?d00001 diff --git a/docs/images/readme/supporting_documentation.png b/docs/images/readme/supporting_documentation.png new file mode 100644 index 0000000000000000000000000000000000000000..b498805cde451eaa606d9c076f07632c5c2563b8 GIT binary patch literal 17402 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 a0{;&iF$uRmlOn Date: Mon, 1 Dec 2025 15:19:56 -0500 Subject: [PATCH 2/2] feat: Add Intelligent Content Generation Accelerator - Multi-agent architecture using Microsoft Agent Framework - React/TypeScript frontend with Fluent UI - Python/Quart backend with async streaming - Azure OpenAI GPT-5.1 for text generation - Azure OpenAI DALL-E 3 for image generation - Azure Cosmos DB for conversation persistence - Azure Blob Storage for image storage - Azure AI Search for product/image search - Comprehensive brand guidelines compliance checking - Creative brief parsing and confirmation flow - Real-time SSE streaming for chat responses --- .gitignore | 56 +- content-gen/.env.template | 119 + content-gen/README.md | 151 + content-gen/azure.yaml | 51 + content-gen/docs/IMAGE_GENERATION.md | 239 + content-gen/infra/main.bicep | 773 ++ content-gen/infra/modules/ai-project.bicep | 58 + content-gen/infra/modules/web-sites.bicep | 367 + .../infra/modules/web-sites.config.bicep | 122 + .../scripts/create_image_search_index.py | 612 ++ content-gen/scripts/deploy.ps1 | 101 + content-gen/scripts/deploy.sh | 98 + content-gen/scripts/images/BlueAsh.jpg | Bin 0 -> 10472 bytes content-gen/scripts/images/CloudDrift.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/FogHarbor.jpg | Bin 0 -> 10470 bytes content-gen/scripts/images/GlacierTint.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/GraphiteFade.jpg | Bin 0 -> 10471 bytes content-gen/scripts/images/ObsidianPearl.jpg | Bin 0 -> 10471 bytes content-gen/scripts/images/OliveStone.jpg | Bin 0 -> 10472 bytes content-gen/scripts/images/PineShadow.jpg | Bin 0 -> 10474 bytes content-gen/scripts/images/PorcelainMist.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/QuietMoss.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/SeafoamLight.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/SilverShore.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/SnowVeil.jpg | Bin 0 -> 10471 bytes content-gen/scripts/images/SteelSky.jpg | Bin 0 -> 10473 bytes content-gen/scripts/images/StoneDusk.jpg | Bin 0 -> 10472 bytes content-gen/scripts/images/VerdantHaze.jpg | Bin 0 -> 10472 bytes content-gen/scripts/index_products.py | 324 + content-gen/scripts/load_sample_data.py | 150 + .../scripts/test_content_generation.py | 314 + content-gen/scripts/upload_images.py | 138 + content-gen/src/Dockerfile | 31 + content-gen/src/app.py | 534 ++ content-gen/src/backend/__init__.py | 30 + content-gen/src/backend/agents/__init__.py | 7 + content-gen/src/backend/agents/base_agent.py | 95 + .../src/backend/agents/compliance_agent.py | 236 + .../src/backend/agents/image_content_agent.py | 249 + .../src/backend/agents/planning_agent.py | 102 + .../src/backend/agents/research_agent.py | 227 + .../src/backend/agents/simple_agent.py | 181 + .../src/backend/agents/text_content_agent.py | 174 + .../src/backend/agents/triage_agent.py | 79 + content-gen/src/backend/models.py | 169 + content-gen/src/backend/orchestrator.py | 748 ++ content-gen/src/backend/services/__init__.py | 11 + .../src/backend/services/blob_service.py | 284 + .../src/backend/services/cosmos_service.py | 371 + .../src/backend/services/search_service.py | 289 + content-gen/src/backend/settings.py | 326 + content-gen/src/frontend/index.html | 13 + content-gen/src/frontend/package-lock.json | 6365 +++++++++++++++++ content-gen/src/frontend/package.json | 34 + content-gen/src/frontend/src/App.tsx | 303 + content-gen/src/frontend/src/api/index.ts | 277 + .../src/components/BriefConfirmation.tsx | 154 + .../src/frontend/src/components/ChatPanel.tsx | 163 + .../src/components/ContentPreview.tsx | 234 + .../src/components/ProductSelector.tsx | 185 + content-gen/src/frontend/src/main.tsx | 13 + .../src/frontend/src/styles/global.css | 87 + content-gen/src/frontend/src/types/index.ts | 108 + content-gen/src/frontend/tsconfig.json | 25 + content-gen/src/frontend/tsconfig.node.json | 11 + content-gen/src/frontend/vite.config.ts | 29 + content-gen/src/hypercorn.conf.py | 21 + content-gen/src/requirements-dev.txt | 17 + content-gen/src/requirements.txt | 26 + content-gen/src/start.cmd | 13 + content-gen/src/start.sh | 15 + content-gen/tests/conftest.py | 105 + content-gen/tests/test_agents.py | 175 + 73 files changed, 16184 insertions(+), 5 deletions(-) create mode 100644 content-gen/.env.template create mode 100644 content-gen/README.md create mode 100644 content-gen/azure.yaml create mode 100644 content-gen/docs/IMAGE_GENERATION.md create mode 100644 content-gen/infra/main.bicep create mode 100644 content-gen/infra/modules/ai-project.bicep create mode 100644 content-gen/infra/modules/web-sites.bicep create mode 100644 content-gen/infra/modules/web-sites.config.bicep create mode 100644 content-gen/scripts/create_image_search_index.py create mode 100644 content-gen/scripts/deploy.ps1 create mode 100644 content-gen/scripts/deploy.sh create mode 100644 content-gen/scripts/images/BlueAsh.jpg create mode 100644 content-gen/scripts/images/CloudDrift.jpg create mode 100644 content-gen/scripts/images/FogHarbor.jpg create mode 100644 content-gen/scripts/images/GlacierTint.jpg create mode 100644 content-gen/scripts/images/GraphiteFade.jpg create mode 100644 content-gen/scripts/images/ObsidianPearl.jpg create mode 100644 content-gen/scripts/images/OliveStone.jpg create mode 100644 content-gen/scripts/images/PineShadow.jpg create mode 100644 content-gen/scripts/images/PorcelainMist.jpg create mode 100644 content-gen/scripts/images/QuietMoss.jpg create mode 100644 content-gen/scripts/images/SeafoamLight.jpg create mode 100644 content-gen/scripts/images/SilverShore.jpg create mode 100644 content-gen/scripts/images/SnowVeil.jpg create mode 100644 content-gen/scripts/images/SteelSky.jpg create mode 100644 content-gen/scripts/images/StoneDusk.jpg create mode 100644 content-gen/scripts/images/VerdantHaze.jpg create mode 100644 content-gen/scripts/index_products.py create mode 100644 content-gen/scripts/load_sample_data.py create mode 100644 content-gen/scripts/test_content_generation.py create mode 100644 content-gen/scripts/upload_images.py create mode 100644 content-gen/src/Dockerfile create mode 100644 content-gen/src/app.py create mode 100644 content-gen/src/backend/__init__.py create mode 100644 content-gen/src/backend/agents/__init__.py create mode 100644 content-gen/src/backend/agents/base_agent.py create mode 100644 content-gen/src/backend/agents/compliance_agent.py create mode 100644 content-gen/src/backend/agents/image_content_agent.py create mode 100644 content-gen/src/backend/agents/planning_agent.py create mode 100644 content-gen/src/backend/agents/research_agent.py create mode 100644 content-gen/src/backend/agents/simple_agent.py create mode 100644 content-gen/src/backend/agents/text_content_agent.py create mode 100644 content-gen/src/backend/agents/triage_agent.py create mode 100644 content-gen/src/backend/models.py create mode 100644 content-gen/src/backend/orchestrator.py create mode 100644 content-gen/src/backend/services/__init__.py create mode 100644 content-gen/src/backend/services/blob_service.py create mode 100644 content-gen/src/backend/services/cosmos_service.py create mode 100644 content-gen/src/backend/services/search_service.py create mode 100644 content-gen/src/backend/settings.py create mode 100644 content-gen/src/frontend/index.html create mode 100644 content-gen/src/frontend/package-lock.json create mode 100644 content-gen/src/frontend/package.json create mode 100644 content-gen/src/frontend/src/App.tsx create mode 100644 content-gen/src/frontend/src/api/index.ts create mode 100644 content-gen/src/frontend/src/components/BriefConfirmation.tsx create mode 100644 content-gen/src/frontend/src/components/ChatPanel.tsx create mode 100644 content-gen/src/frontend/src/components/ContentPreview.tsx create mode 100644 content-gen/src/frontend/src/components/ProductSelector.tsx create mode 100644 content-gen/src/frontend/src/main.tsx create mode 100644 content-gen/src/frontend/src/styles/global.css create mode 100644 content-gen/src/frontend/src/types/index.ts create mode 100644 content-gen/src/frontend/tsconfig.json create mode 100644 content-gen/src/frontend/tsconfig.node.json create mode 100644 content-gen/src/frontend/vite.config.ts create mode 100644 content-gen/src/hypercorn.conf.py create mode 100644 content-gen/src/requirements-dev.txt create mode 100644 content-gen/src/requirements.txt create mode 100644 content-gen/src/start.cmd create mode 100644 content-gen/src/start.sh create mode 100644 content-gen/tests/conftest.py create mode 100644 content-gen/tests/test_agents.py diff --git a/.gitignore b/.gitignore index 0abb7a034..e0af1f41e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,61 @@ +# Virtual environments .venv +venv +myenv +scriptsenv/ +scriptenv +# Environment files with secrets .env +.env.local +.env.*.local +*.env + +# Azure .azure/ + +# Python __pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +eggs/ +*.egg + +# Jupyter .ipynb_checkpoints/ +# IDE +.vscode/ +.idea/ +*.swp +*.swo -venv -myenv +# Node +/content-gen/src/frontend/node_modules/ +node_modules/ -scriptsenv/ +# Logs +*.log +logs/ -scriptenv -pdf \ No newline at end of file +# Keys and credentials +*.pem +*.key +*.pfx +*.p12 +*secret* +*credential* + +# OS files +.DS_Store +Thumbs.db + +# Misc +pdf +*.bak +*.tmp \ No newline at end of file diff --git a/content-gen/.env.template b/content-gen/.env.template new file mode 100644 index 000000000..c277db006 --- /dev/null +++ b/content-gen/.env.template @@ -0,0 +1,119 @@ +# Content Generation Solution Accelerator - Development Environment +# Copy this file to .env and fill in your values + +# ============================================================================= +# Azure Authentication +# ============================================================================= +AZURE_CLIENT_ID= +AZURE_TENANT_ID= +AZURE_SUBSCRIPTION_ID= + +# ============================================================================= +# Azure OpenAI Configuration +# ============================================================================= +# Your Azure OpenAI endpoint (e.g., https://your-resource.openai.azure.com/) +AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/ + +# Or use resource name instead of full endpoint +# AZURE_OPENAI_RESOURCE=your-openai-resource + +# Model deployments +AZURE_OPENAI_GPT_MODEL=gpt-5.1 +AZURE_OPENAI_DALLE_MODEL=dall-e-3 + +# Optional: Separate endpoint for DALL-E if deployed in a different resource +AZURE_OPENAI_DALLE_ENDPOINT= + +# API versions +AZURE_OPENAI_API_VERSION=2024-06-01 +AZURE_OPENAI_PREVIEW_API_VERSION=2024-02-01 + +# Generation parameters +AZURE_OPENAI_TEMPERATURE=0.7 +AZURE_OPENAI_MAX_TOKENS=2000 + +# ============================================================================= +# Azure AI Foundry (Agent Framework) +# ============================================================================= +AZURE_AI_AGENT_ENDPOINT=https://your-project.services.ai.azure.com/api/projects/your-project +AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-5.1 +AZURE_AI_AGENT_API_VERSION=2024-12-01-preview + +# ============================================================================= +# Azure Cosmos DB +# ============================================================================= +AZURE_COSMOS_ENDPOINT=https://your-cosmos.documents.azure.com:443/ +AZURE_COSMOS_DATABASE_NAME=content-generation +AZURE_COSMOS_PRODUCTS_CONTAINER=products +AZURE_COSMOS_CONVERSATIONS_CONTAINER=conversations + +# For local development with emulator (optional) +# AZURE_COSMOS_ENDPOINT=https://localhost:8081 +# AZURE_COSMOSDB_ACCOUNT_KEY=your-emulator-key + +# ============================================================================= +# Azure Blob Storage +# ============================================================================= +AZURE_BLOB_ACCOUNT_NAME=yourstorageaccount +AZURE_BLOB_PRODUCT_IMAGES_CONTAINER=product-images +AZURE_BLOB_GENERATED_IMAGES_CONTAINER=generated-images + +# ============================================================================= +# Azure AI Search +# ============================================================================= +AZURE_AI_SEARCH_ENDPOINT=https://your-search.search.windows.net +AZURE_AI_SEARCH_PRODUCTS_INDEX=products +AZURE_AI_SEARCH_IMAGE_INDEX=product-images +AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 + +# ============================================================================= +# UI Configuration +# ============================================================================= +UI_APP_NAME=Content Generation Accelerator +UI_TITLE=Content Generation +UI_CHAT_TITLE=Marketing Content Generator +UI_CHAT_DESCRIPTION=AI-powered multimodal content generation for marketing campaigns. + +# ============================================================================= +# Brand Guidelines +# ============================================================================= +# Voice and Tone +BRAND_TONE=Professional yet approachable +BRAND_VOICE=Innovative, trustworthy, customer-focused + +# Visual Guidelines +BRAND_PRIMARY_COLOR=#0078D4 +BRAND_SECONDARY_COLOR=#107C10 +BRAND_IMAGE_STYLE=Modern, clean, minimalist with bright lighting + +# Content Rules +BRAND_MAX_HEADLINE_LENGTH=60 +BRAND_MAX_BODY_LENGTH=500 +BRAND_REQUIRE_CTA=true + +# Comma-separated list of prohibited words +BRAND_PROHIBITED_WORDS=cheapest,guaranteed,best in class,#1,market leader + +# Comma-separated list of required disclosures (leave empty if none) +BRAND_REQUIRED_DISCLOSURES= + +# ============================================================================= +# Application Settings +# ============================================================================= +# Server configuration +PORT=5000 +WORKERS=4 +LOG_LEVEL=info + +# Feature flags +AUTH_ENABLED=false +SANITIZE_ANSWER=false + +# ============================================================================= +# Development Settings +# ============================================================================= +# Set to true for verbose logging +DEBUG=true + +# Python path (usually set automatically) +PYTHONPATH=. diff --git a/content-gen/README.md b/content-gen/README.md new file mode 100644 index 000000000..c3e81a365 --- /dev/null +++ b/content-gen/README.md @@ -0,0 +1,151 @@ +# Intelligent Content Generation Accelerator + +A multimodal content generation solution for retail marketing campaigns using Microsoft Agent Framework with HandoffBuilder orchestration. The system interprets creative briefs and generates compliant marketing content (text and images) grounded in enterprise product data, brand guidelines, and product images. + +## Overview + +This accelerator provides an internal chatbot that can: + +- **Interpret Creative Briefs**: Parse free-text creative briefs into structured fields (overview, objectives, target audience, key message, tone/style, deliverable, timelines, visual guidelines, CTA) +- **Generate Multimodal Content**: Create marketing copy and images using GPT-5 and DALL-E 3 +- **Ensure Brand Compliance**: Validate all content against brand guidelines with severity-categorized warnings +- **Ground in Enterprise Data**: Leverage product information, product images, and brand guidelines stored in Azure services + +## Architecture + +### Specialized Agents (Microsoft Agent Framework) + +The solution uses **HandoffBuilder** orchestration with 6 specialized agents: + +| Agent | Role | +|-------|------| +| **TriageAgent** | Coordinator that routes user requests to appropriate specialists | +| **PlanningAgent** | Parses creative briefs, develops content strategy, returns for user confirmation | +| **ResearchAgent** | Retrieves products from CosmosDB, fetches brand guidelines, assembles grounding data | +| **TextContentAgent** | Generates marketing copy (headlines, body, CTAs) using GPT-5 | +| **ImageContentAgent** | Creates marketing images via DALL-E 3 with product context | +| **ComplianceAgent** | Validates content against brand guidelines, categorizes violations | + +### Compliance Severity Levels + +| Level | Description | Action | +|-------|-------------|--------| +| **Error** | Legal/regulatory violations | Blocks acceptance until modified | +| **Warning** | Brand guideline deviations | Review recommended | +| **Info** | Style suggestions | Optional improvements | + +### Data Storage + +| Data | Storage | Purpose | +|------|---------|---------| +| Products | CosmosDB | Product catalog with auto-generated image descriptions | +| Chat History | CosmosDB | Conversation persistence | +| Product Images | Blob Storage | Seed images for DALL-E generation | +| Brand Guidelines | Solution Parameters | Injected into all agent instructions | + +## Creative Brief Fields + +The system extracts the following fields from free-text creative briefs: + +1. **Overview** - Campaign summary +2. **Objectives** - Goals and KPIs +3. **Target Audience** - Demographics and psychographics +4. **Key Message** - Core messaging +5. **Tone and Style** - Voice and manner +6. **Deliverable** - Expected outputs +7. **Timelines** - Due dates and milestones +8. **Visual Guidelines** - Image requirements +9. **CTA** - Call to action + +## Product Schema + +```json +{ + "product_name": "string", + "category": "string", + "sub_category": "string", + "marketing_description": "string", + "detailed_spec_description": "string", + "sku": "string", + "model": "string", + "image_description": "string (auto-generated via GPT-5 vision)", + "image_url": "string" +} +``` + +## Getting Started + +### Prerequisites + +- Azure subscription with access to: + - Azure AI Foundry (GPT-5 + DALL-E 3) + - Azure CosmosDB + - Azure Blob Storage + - Azure App Service +- Azure Developer CLI (azd) >= 1.18.0 +- Python 3.11+ +- Node.js 18+ + +### Deployment + +```bash +# Login to Azure +azd auth login + +# Deploy infrastructure and application +azd up + +# Upload product data (optional) +python ./scripts/product_ingestion.py +``` + +### Local Development + +```bash +# Backend +cd src +pip install -r requirements.txt +python app.py + +# Frontend +cd src/frontend +npm install +npm run dev +``` + +## Configuration + +### Environment Variables + +See `src/backend/settings.py` for all configuration options. Key settings: + +| Variable | Description | +|----------|-------------| +| `AZURE_AI_AGENT_ENDPOINT` | Azure AI Foundry project endpoint | +| `AZURE_OPENAI_MODEL` | GPT model deployment name (gpt-5) | +| `AZURE_DALLE_MODEL` | DALL-E model deployment name (dall-e-3) | +| `AZURE_COSMOSDB_ACCOUNT` | CosmosDB account name | +| `BRAND_*` | Brand guideline parameters | + +### Brand Guidelines + +Brand guidelines are configured via environment variables with the `BRAND_` prefix: + +```env +BRAND_TONE=Professional yet approachable +BRAND_VOICE=Innovative, trustworthy, customer-focused +BRAND_PROHIBITED_WORDS=guarantee,best,only,exclusive +BRAND_REQUIRED_DISCLOSURES=Terms apply,See details +BRAND_PRIMARY_COLOR=#0078D4 +BRAND_SECONDARY_COLOR=#107C10 +``` + +## Documentation + +- [DALL-E 3 Image Generation Limitations](docs/IMAGE_GENERATION.md) +- [Deployment Guide](docs/DEPLOYMENT.md) +- [API Reference](docs/API.md) + +## License + +MIT License - See [LICENSE](LICENSE) for details. diff --git a/content-gen/azure.yaml b/content-gen/azure.yaml new file mode 100644 index 000000000..228c237b9 --- /dev/null +++ b/content-gen/azure.yaml @@ -0,0 +1,51 @@ +environment: + name: content-generation + location: eastus + +name: content-generation +metadata: + template: content-generation@1.0 + +requiredVersions: + azd: '>= 1.18.0' + +parameters: + solutionPrefix: + type: string + default: contentgen + otherLocation: + type: string + default: eastus2 + baseUrl: + type: string + default: 'https://github.com/microsoft/content-generation-solution-accelerator' + +deployment: + mode: Incremental + template: ./infra/main.bicep + parameters: + solutionPrefix: ${parameters.solutionPrefix} + otherLocation: ${parameters.otherLocation} + baseUrl: ${parameters.baseUrl} + +hooks: + postprovision: + windows: + run: | + Write-Host "Web app URL: " + Write-Host "$env:WEB_APP_URL" -ForegroundColor Cyan + Write-Host "`nTo upload product data, run:" + Write-Host "python ./scripts/product_ingestion.py" -ForegroundColor Cyan + shell: pwsh + continueOnError: false + interactive: true + posix: + run: | + echo "Web app URL: " + echo $WEB_APP_URL + echo "" + echo "To upload product data, run:" + echo "python ./scripts/product_ingestion.py" + shell: sh + continueOnError: false + interactive: true diff --git a/content-gen/docs/IMAGE_GENERATION.md b/content-gen/docs/IMAGE_GENERATION.md new file mode 100644 index 000000000..23dfe21f2 --- /dev/null +++ b/content-gen/docs/IMAGE_GENERATION.md @@ -0,0 +1,239 @@ +# DALL-E 3 Image Generation: Limitations and Workarounds + +## Overview + +This document describes the limitations of DALL-E 3 for image generation in the Intelligent Content Generation Accelerator and the workarounds implemented to achieve product-seeded marketing image generation. + +## DALL-E 3 Limitations + +### Text-Only Input + +**DALL-E 3 only accepts text prompts**. Unlike newer models such as GPT-image-1, DALL-E 3 does not support: + +- Image-to-image generation +- Reference/seed images as input +- Image editing or inpainting with image inputs + +This means you cannot directly pass a product image to DALL-E 3 and ask it to create a marketing image featuring that product. + +### API Capabilities + +| Capability | DALL-E 3 | GPT-image-1 | +|------------|----------|-------------| +| Text prompts | ✅ | ✅ | +| Image input | ❌ | ✅ | +| Image editing | ❌ | ✅ | +| Inpainting | ❌ | ✅ | +| Multiple images per request | 1 only | 1-10 | +| Output format | URL or base64 | base64 only | + +## Implemented Workaround + +### GPT-5 Vision for Product Descriptions + +To work around DALL-E 3's text-only limitation, we use **GPT-5 Vision** to generate detailed text descriptions of product images during the product ingestion process. + +#### Workflow + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Product Image │────▶│ GPT-5 Vision │────▶│ Text Description│ +│ (Blob Storage) │ │ (Auto-analyze) │ │ (CosmosDB) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Marketing Image │◀────│ DALL-E 3 │◀────│ Combined Prompt │ +│ (Output) │ │ (Generate) │ │ (Desc + Brief) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +#### Step 1: Product Image Ingestion + +When a product image is uploaded to Blob Storage, the `ProductIngestionService` automatically: + +1. Sends the image to GPT-5 Vision +2. Generates a detailed text description including: + - Product appearance (colors, shapes, materials) + - Key visual features + - Composition and positioning + - Style and aesthetic qualities +3. Stores the description in CosmosDB alongside product metadata + +```python +async def generate_image_description(image_url: str) -> str: + """Generate detailed text description of product image using GPT-5 Vision.""" + response = await openai_client.chat.completions.create( + model="gpt-5", + messages=[ + { + "role": "user", + "content": [ + { + "type": "text", + "text": """Describe this product image in detail for use in marketing image generation. + Include: colors, materials, shape, key features, style, and positioning. + Be specific enough that an image generator could recreate a similar product.""" + }, + { + "type": "image_url", + "image_url": {"url": image_url} + } + ] + } + ], + max_tokens=500 + ) + return response.choices[0].message.content +``` + +#### Step 2: Marketing Image Generation + +The `ImageContentAgent` combines the stored product description with: + +- Creative brief visual guidelines +- Brand guidelines (colors, style, composition rules) +- Scene/context requirements + +```python +async def generate_marketing_image( + product: Product, + creative_brief: CreativeBrief, + brand_guidelines: BrandGuidelines +) -> bytes: + """Generate marketing image using DALL-E 3 with product context.""" + + prompt = f""" + Create a professional marketing image for a retail campaign. + + PRODUCT (maintain accuracy): + {product.image_description} + + SCENE: + {creative_brief.visual_guidelines} + + BRAND STYLE: + - Primary color: {brand_guidelines.primary_color} + - Style: {brand_guidelines.image_style} + - Composition: Product centered, 30% negative space + + REQUIREMENTS: + - Professional lighting + - Clean, modern aesthetic + - Suitable for {creative_brief.deliverable} + """ + + response = await openai_client.images.generate( + model="dall-e-3", + prompt=prompt, + size="1024x1024", + quality="hd", + n=1 + ) + + return response.data[0].url +``` + +## Limitations of the Workaround + +### Accuracy Trade-offs + +1. **Product Representation**: The generated product in the marketing image may not be an exact match to the original product. DALL-E 3 interprets the text description and creates its own version. + +2. **Brand-Specific Details**: Logos, specific patterns, or unique design elements may not be accurately reproduced. + +3. **Color Matching**: While we include color descriptions, exact color matching is not guaranteed. + +### Recommended Use Cases + +| Use Case | Suitability | +|----------|-------------| +| Lifestyle/contextual marketing images | ✅ Excellent | +| Social media campaign visuals | ✅ Excellent | +| Concept mockups | ✅ Good | +| Product-in-scene compositions | ✅ Good | +| Exact product photography replacement | ❌ Not recommended | +| Catalog/technical images | ❌ Not recommended | + +## Future Upgrade Path: GPT-image-1 + +### When Available + +GPT-image-1 (currently in limited access preview) will enable true image-to-image generation: + +```python +# Future implementation with GPT-image-1 +async def generate_marketing_image_with_seed( + product_image_path: str, + scene_description: str, + brand_style: str +) -> bytes: + """Generate marketing image seeded with actual product photo.""" + + response = await openai_client.images.edit( + model="gpt-image-1", + image=open(product_image_path, "rb"), # Actual product image as input + prompt=f""" + Create a marketing image featuring the product shown. + Scene: {scene_description} + Brand Style: {brand_style} + Maintain product accuracy. + """, + size="1024x1024", + quality="high", + input_fidelity="high" # Preserve product details + ) + + return base64.b64decode(response.data[0].b64_json) +``` + +### How to Request Access + +1. Visit [GPT-image-1 Access Request](https://aka.ms/oai/gptimage1access) +2. Complete the application form +3. Wait for approval (typically 1-2 weeks) +4. Update the `ImageContentAgent` to use the Image Edit API + +### Migration Steps + +When GPT-image-1 access is granted: + +1. Update `AZURE_DALLE_MODEL` environment variable to `gpt-image-1` +2. Modify `ImageContentAgent` to use `images.edit()` instead of `images.generate()` +3. Update Blob Storage retrieval to pass actual image bytes +4. Test with sample products before production deployment + +## Best Practices + +### Optimizing Product Descriptions + +For best results with the text-based workaround: + +1. **Be Specific**: Include exact colors, materials, and dimensions +2. **Describe Unique Features**: Highlight what makes the product distinctive +3. **Include Context**: Mention typical use cases or settings +4. **Avoid Ambiguity**: Use precise terminology + +### Example High-Quality Description + +``` +A sleek wireless Bluetooth headphone in matte black finish with +rose gold accents on the ear cup rims and headband adjustment +sliders. Over-ear cushions in premium memory foam covered with +soft protein leather. The headband features a padded top section +with subtle brand embossing. The left ear cup has touch-sensitive +controls visible as a circular touch pad. Cable port and power +button are positioned on the bottom edge of the right ear cup. +Overall aesthetic is premium, modern, and minimalist. +``` + +## Compliance Considerations + +All generated images are validated by the `ComplianceAgent` for: + +- Brand color adherence +- Prohibited visual elements +- Appropriate imagery for target audience +- Required disclaimers (added as text overlay if needed) + +Images with compliance violations are flagged with appropriate severity levels before user review. diff --git a/content-gen/infra/main.bicep b/content-gen/infra/main.bicep new file mode 100644 index 000000000..49a09dbbc --- /dev/null +++ b/content-gen/infra/main.bicep @@ -0,0 +1,773 @@ +// ========== main.bicep ========== // +targetScope = 'resourceGroup' + +metadata name = 'Intelligent Content Generation Accelerator' +metadata description = '''Solution Accelerator for multimodal marketing content generation using Microsoft Agent Framework. +''' + +@minLength(3) +@maxLength(15) +@description('Optional. A unique application/solution name for all resources in this deployment.') +param solutionName string = 'contentgen' + +@maxLength(5) +@description('Optional. A unique text value for the solution.') +param solutionUniqueText string = substring(uniqueString(subscription().id, resourceGroup().name, solutionName), 0, 5) + +@allowed([ + 'australiaeast' + 'centralus' + 'eastasia' + 'eastus2' + 'japaneast' + 'northeurope' + 'southeastasia' + 'uksouth' +]) +@metadata({ azd: { type: 'location' } }) +@description('Required. Azure region for all services.') +param location string + +@minLength(3) +@description('Optional. Secondary location for databases creation.') +param secondaryLocation string = 'uksouth' + +@allowed([ + 'australiaeast' + 'eastus' + 'eastus2' + 'francecentral' + 'japaneast' + 'koreacentral' + 'swedencentral' + 'switzerlandnorth' + 'uaenorth' + 'uksouth' + 'westus' + 'westus3' +]) +@description('Location for AI deployments.') +@metadata({ + azd: { + type: 'location' + usageName: [ + 'OpenAI.GlobalStandard.gpt-5, 150' + 'OpenAI.GlobalStandard.dall-e-3, 10' + 'OpenAI.GlobalStandard.text-embedding-ada-002, 80' + ] + } +}) +param azureAiServiceLocation string + +@minLength(1) +@allowed([ + 'Standard' + 'GlobalStandard' +]) +@description('Optional. GPT model deployment type.') +param gptModelDeploymentType string = 'GlobalStandard' + +@minLength(1) +@description('Optional. Name of the GPT model to deploy.') +param gptModelName string = 'gpt-5' + +@description('Optional. Version of the GPT model to deploy.') +param gptModelVersion string = '2025-04-14' + +@description('Optional. Name of the DALL-E model to deploy.') +param dalleModelName string = 'dall-e-3' + +@description('Optional. Version of the DALL-E model.') +param dalleModelVersion string = '3.0' + +@description('Optional. API version for Azure OpenAI service.') +param azureOpenaiAPIVersion string = '2025-01-01-preview' + +@description('Optional. API version for Azure AI Agent service.') +param azureAiAgentApiVersion string = '2025-05-01' + +@minValue(10) +@description('Optional. AI model deployment token capacity.') +param gptModelCapacity int = 150 + +@minValue(1) +@description('Optional. DALL-E model deployment capacity.') +param dalleModelCapacity int = 10 + +@minLength(1) +@description('Optional. Name of the Text Embedding model to deploy.') +param embeddingModel string = 'text-embedding-ada-002' + +@minValue(10) +@description('Optional. Capacity of the Embedding Model deployment.') +param embeddingDeploymentCapacity int = 80 + +@description('Optional. Existing Log Analytics Workspace Resource ID.') +param existingLogAnalyticsWorkspaceId string = '' + +@description('Optional. Resource ID of an existing Foundry project.') +param azureExistingAIProjectResourceId string = '' + +@description('Optional. The tags to apply to all deployed Azure resources.') +param tags resourceInput<'Microsoft.Resources/resourceGroups@2025-04-01'>.tags = {} + +@description('Optional. Enable monitoring for applicable resources.') +param enableMonitoring bool = false + +@description('Optional. Enable scalability for applicable resources.') +param enableScalability bool = false + +@description('Optional. Enable redundancy for applicable resources.') +param enableRedundancy bool = false + +@description('Optional. Enable private networking for applicable resources.') +param enablePrivateNetworking bool = false + +@description('Optional. The Container Registry hostname.') +param acrName string = 'contentgencontainerreg' + +@description('Optional. Image Tag.') +param imageTag string = 'latest' + +@description('Optional. Enable/Disable usage telemetry.') +param enableTelemetry bool = true + +@description('Optional. Enable purge protection for Key Vault.') +param enablePurgeProtection bool = false + +@description('Optional. Created by user name.') +param createdBy string = contains(deployer(), 'userPrincipalName')? split(deployer().userPrincipalName, '@')[0]: deployer().objectId + +// ============== // +// Variables // +// ============== // + +var solutionLocation = empty(location) ? resourceGroup().location : location +var solutionSuffix = toLower(trim(replace( + replace( + replace(replace(replace(replace('${solutionName}${solutionUniqueText}', '-', ''), '_', ''), '.', ''), '/', ''), + ' ', + '' + ), + '*', + '' +))) + +var cosmosDbZoneRedundantHaRegionPairs = { + australiaeast: 'uksouth' + centralus: 'eastus2' + eastasia: 'southeastasia' + eastus: 'centralus' + eastus2: 'centralus' + japaneast: 'australiaeast' + northeurope: 'westeurope' + southeastasia: 'eastasia' + uksouth: 'westeurope' +} +var cosmosDbHaLocation = cosmosDbZoneRedundantHaRegionPairs[resourceGroup().location] + +var replicaRegionPairs = { + australiaeast: 'australiasoutheast' + centralus: 'westus' + eastasia: 'japaneast' + eastus: 'centralus' + eastus2: 'centralus' + japaneast: 'eastasia' + northeurope: 'westeurope' + southeastasia: 'eastasia' + uksouth: 'westeurope' +} +var replicaLocation = replicaRegionPairs[resourceGroup().location] + +var appEnvironment = 'Prod' +var azureCosmosDbEnableFeedback = 'True' + +// Extracts subscription, resource group, and workspace name from the resource ID +var useExistingLogAnalytics = !empty(existingLogAnalyticsWorkspaceId) +var useExistingAiFoundryAiProject = !empty(azureExistingAIProjectResourceId) +var aiFoundryAiServicesResourceGroupName = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[4] + : 'rg-${solutionSuffix}' +var aiFoundryAiServicesSubscriptionId = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[2] + : subscription().id +var aiFoundryAiServicesResourceName = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[8] + : 'aif-${solutionSuffix}' +var aiFoundryAiProjectResourceName = useExistingAiFoundryAiProject + ? split(azureExistingAIProjectResourceId, '/')[10] + : 'proj-${solutionSuffix}' +var aiFoundryAiServicesModelDeployment = [ + { + format: 'OpenAI' + name: gptModelName + model: gptModelName + sku: { + name: gptModelDeploymentType + capacity: gptModelCapacity + } + version: gptModelVersion + raiPolicyName: 'Microsoft.Default' + } + { + format: 'OpenAI' + name: dalleModelName + model: dalleModelName + sku: { + name: 'Standard' + capacity: dalleModelCapacity + } + version: dalleModelVersion + raiPolicyName: 'Microsoft.Default' + } + { + format: 'OpenAI' + name: embeddingModel + model: embeddingModel + sku: { + name: 'GlobalStandard' + capacity: embeddingDeploymentCapacity + } + version: '2' + raiPolicyName: 'Microsoft.Default' + } +] +var aiFoundryAiProjectDescription = 'Content Generation AI Foundry Project' + +// ============== // +// Resources // +// ============== // + +#disable-next-line no-deployments-resources +resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) { + name: '46d3xbcp.ptn.sa-contentgen.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, solutionLocation), 0, 4)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + outputs: { + telemetry: { + type: 'String' + value: 'For more information, see https://aka.ms/avm/TelemetryInfo' + } + } + } + } +} + +// ========== Resource Group Tag ========== // +resource resourceGroupTags 'Microsoft.Resources/tags@2021-04-01' = { + name: 'default' + properties: { + tags: { + ...resourceGroup().tags + ... tags + TemplateName: 'ContentGen' + Type: enablePrivateNetworking ? 'WAF' : 'Non-WAF' + CreatedBy: createdBy + } + } +} + +// ========== Log Analytics Workspace ========== // +var logAnalyticsWorkspaceResourceName = 'log-${solutionSuffix}' +module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.12.0' = if (enableMonitoring && !useExistingLogAnalytics) { + name: take('avm.res.operational-insights.workspace.${logAnalyticsWorkspaceResourceName}', 64) + params: { + name: logAnalyticsWorkspaceResourceName + tags: tags + location: solutionLocation + enableTelemetry: enableTelemetry + skuName: 'PerGB2018' + dataRetention: 365 + features: { enableLogAccessUsingOnlyResourcePermissions: true } + diagnosticSettings: [{ useThisWorkspace: true }] + dailyQuotaGb: enableRedundancy ? 10 : null + replication: enableRedundancy + ? { + enabled: true + location: replicaLocation + } + : null + publicNetworkAccessForIngestion: enablePrivateNetworking ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } +} +var logAnalyticsWorkspaceResourceId = useExistingLogAnalytics ? existingLogAnalyticsWorkspaceId : logAnalyticsWorkspace!.outputs.resourceId + +// ========== Application Insights ========== // +var applicationInsightsResourceName = 'appi-${solutionSuffix}' +module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (enableMonitoring) { + name: take('avm.res.insights.component.${applicationInsightsResourceName}', 64) + params: { + name: applicationInsightsResourceName + tags: tags + location: solutionLocation + enableTelemetry: enableTelemetry + retentionInDays: 365 + kind: 'web' + disableIpMasking: false + flowType: 'Bluefield' + workspaceResourceId: logAnalyticsWorkspaceResourceId + diagnosticSettings: [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] + } +} + +// ========== User Assigned Identity ========== // +var userAssignedIdentityResourceName = 'id-${solutionSuffix}' +module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { + name: take('avm.res.managed-identity.user-assigned-identity.${userAssignedIdentityResourceName}', 64) + params: { + name: userAssignedIdentityResourceName + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + } +} + +// ========== AI Foundry: AI Services ========== // +module aiFoundryAiServices 'br:mcr.microsoft.com/bicep/avm/res/cognitive-services/account:0.13.2' = if (!useExistingAiFoundryAiProject) { + name: take('avm.res.cognitive-services.account.${aiFoundryAiServicesResourceName}', 64) + params: { + name: aiFoundryAiServicesResourceName + location: azureAiServiceLocation + tags: tags + sku: 'S0' + kind: 'AIServices' + disableLocalAuth: true + allowProjectManagement: true + customSubDomainName: aiFoundryAiServicesResourceName + restrictOutboundNetworkAccess: false + deployments: [ + for deployment in aiFoundryAiServicesModelDeployment: { + name: deployment.name + model: { + format: deployment.format + name: deployment.name + version: deployment.version + } + raiPolicyName: deployment.raiPolicyName + sku: { + name: deployment.sku.name + capacity: deployment.sku.capacity + } + } + ] + networkAcls: { + defaultAction: 'Allow' + virtualNetworkRules: [] + ipRules: [] + } + managedIdentities: { + userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] + } + roleAssignments: [ + { + roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } + { + roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } + { + roleDefinitionIdOrName: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' // Cognitive Services OpenAI User + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + } + ] + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } +} + +module aiFoundryAiServicesProject 'modules/ai-project.bicep' = if (!useExistingAiFoundryAiProject) { + name: take('module.ai-project.${aiFoundryAiProjectResourceName}', 64) + params: { + name: aiFoundryAiProjectResourceName + location: azureAiServiceLocation + tags: tags + desc: aiFoundryAiProjectDescription + aiServicesName: aiFoundryAiServicesResourceName + azureExistingAIProjectResourceId: azureExistingAIProjectResourceId + } + dependsOn: [ + aiFoundryAiServices + ] +} + +var aiFoundryAiProjectEndpoint = useExistingAiFoundryAiProject + ? 'https://${aiFoundryAiServicesResourceName}.services.ai.azure.com/api/projects/${aiFoundryAiProjectResourceName}' + : aiFoundryAiServicesProject!.outputs.apiEndpoint + +// ========== Storage Account ========== // +var storageAccountName = 'st${solutionSuffix}' +var productImagesContainer = 'product-images' +var generatedImagesContainer = 'generated-images' + +module storageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = { + name: take('avm.res.storage.storage-account.${storageAccountName}', 64) + params: { + name: storageAccountName + location: solutionLocation + skuName: 'Standard_LRS' + managedIdentities: { systemAssigned: true } + minimumTlsVersion: 'TLS1_2' + enableTelemetry: enableTelemetry + tags: tags + accessTier: 'Hot' + supportsHttpsTrafficOnly: true + blobServices: { + containerDeleteRetentionPolicyEnabled: false + containerDeleteRetentionPolicyDays: 7 + deleteRetentionPolicyEnabled: false + deleteRetentionPolicyDays: 6 + containers: [ + { + name: productImagesContainer + publicAccess: 'None' + denyEncryptionScopeOverride: false + defaultEncryptionScope: '$account-encryption-key' + } + { + name: generatedImagesContainer + publicAccess: 'None' + denyEncryptionScopeOverride: false + defaultEncryptionScope: '$account-encryption-key' + } + ] + } + roleAssignments: [ + { + principalId: userAssignedIdentity.outputs.principalId + roleDefinitionIdOrName: 'Storage Blob Data Contributor' + principalType: 'ServicePrincipal' + } + ] + networkAcls: { + bypass: 'AzureServices' + defaultAction: enablePrivateNetworking ? 'Deny' : 'Allow' + } + allowBlobPublicAccess: enablePrivateNetworking ? true : false + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } +} + +// ========== Cosmos DB ========== // +var cosmosDBResourceName = 'cosmos-${solutionSuffix}' +var cosmosDBDatabaseName = 'content_generation_db' +var cosmosDBConversationsContainer = 'conversations' +var cosmosDBProductsContainer = 'products' + +module cosmosDB 'br/public:avm/res/document-db/database-account:0.15.0' = { + name: take('avm.res.document-db.database-account.${cosmosDBResourceName}', 64) + params: { + name: 'cosmos-${solutionSuffix}' + location: secondaryLocation + tags: tags + enableTelemetry: enableTelemetry + sqlDatabases: [ + { + name: cosmosDBDatabaseName + containers: [ + { + name: cosmosDBConversationsContainer + paths: [ + '/userId' + ] + } + { + name: cosmosDBProductsContainer + paths: [ + '/category' + ] + } + ] + } + ] + dataPlaneRoleDefinitions: [ + { + roleName: 'Cosmos DB SQL Data Contributor' + dataActions: [ + 'Microsoft.DocumentDB/databaseAccounts/readMetadata' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' + ] + assignments: [{ principalId: userAssignedIdentity.outputs.principalId }] + } + ] + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + networkRestrictions: { + networkAclBypass: 'None' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + } + zoneRedundant: enableRedundancy ? true : false + capabilitiesToAdd: enableRedundancy ? null : ['EnableServerless'] + automaticFailover: enableRedundancy ? true : false + failoverLocations: enableRedundancy + ? [ + { + failoverPriority: 0 + isZoneRedundant: true + locationName: secondaryLocation + } + { + failoverPriority: 1 + isZoneRedundant: true + locationName: cosmosDbHaLocation + } + ] + : [ + { + locationName: secondaryLocation + failoverPriority: 0 + isZoneRedundant: enableRedundancy + } + ] + } +} + +// ========== Key Vault ========== // +var keyVaultName = 'kv-${solutionSuffix}' +module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = { + name: take('avm.res.key-vault.vault.${keyVaultName}', 64) + params: { + name: keyVaultName + location: solutionLocation + tags: tags + sku: 'standard' + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + networkAcls: { + defaultAction: 'Allow' + } + enableVaultForDeployment: true + enableVaultForDiskEncryption: true + enableVaultForTemplateDeployment: true + enableRbacAuthorization: true + enableSoftDelete: true + enablePurgeProtection: enablePurgeProtection + softDeleteRetentionInDays: 7 + diagnosticSettings: enableMonitoring + ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] + : [] + roleAssignments: [ + { + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Key Vault Administrator' + } + ] + enableTelemetry: enableTelemetry + secrets: [ + { + name: 'STORAGE-ACCOUNT-NAME' + value: storageAccountName + } + { + name: 'STORAGE-ACCOUNT-KEY' + value: storageAccount.outputs.primaryAccessKey + } + { + name: 'AZURE-COSMOSDB-ACCOUNT' + value: cosmosDB.outputs.name + } + { + name: 'AZURE-COSMOSDB-ACCOUNT-KEY' + value: cosmosDB.outputs.primaryReadWriteKey + } + { + name: 'AZURE-COSMOSDB-DATABASE' + value: cosmosDBDatabaseName + } + { + name: 'AZURE-COSMOSDB-CONVERSATIONS-CONTAINER' + value: cosmosDBConversationsContainer + } + { + name: 'AZURE-COSMOSDB-PRODUCTS-CONTAINER' + value: cosmosDBProductsContainer + } + {name: 'AZURE-LOCATION', value: azureAiServiceLocation } + {name: 'AZURE-RESOURCE-GROUP', value: resourceGroup().name} + {name: 'AZURE-SUBSCRIPTION-ID', value: subscription().subscriptionId} + { + name: 'COG-SERVICES-NAME' + value: aiFoundryAiServicesResourceName + } + { + name: 'COG-SERVICES-ENDPOINT' + value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' + } + {name: 'AZURE-OPENAI-MODEL', value: gptModelName} + {name: 'AZURE-DALLE-MODEL', value: dalleModelName} + {name: 'AZURE-OPENAI-EMBEDDING-MODEL', value: embeddingModel} + { + name: 'AZURE-OPENAI-ENDPOINT' + value: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' + } + {name: 'AZURE-OPENAI-PREVIEW-API-VERSION', value: azureOpenaiAPIVersion} + {name: 'TENANT-ID', value: subscription().tenantId} + ] + } +} + +// ========== App Service Plan ========== // +var webServerFarmResourceName = 'asp-${solutionSuffix}' +module webServerFarm 'br/public:avm/res/web/serverfarm:0.5.0' = { + name: take('avm.res.web.serverfarm.${webServerFarmResourceName}', 64) + params: { + name: webServerFarmResourceName + tags: tags + enableTelemetry: enableTelemetry + location: solutionLocation + reserved: true + kind: 'linux' + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + skuName: enableScalability || enableRedundancy ? 'P1v3' : 'B3' + skuCapacity: 1 + zoneRedundant: enableRedundancy ? true : false + } + scope: resourceGroup(resourceGroup().name) +} + +// ========== Web App ========== // +var webSiteResourceName = 'app-${solutionSuffix}' +module webSite 'modules/web-sites.bicep' = { + name: take('module.web-sites.${webSiteResourceName}', 64) + params: { + name: webSiteResourceName + tags: tags + location: solutionLocation + kind: 'app,linux,container' + serverFarmResourceId: webServerFarm.outputs.resourceId + managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] } + siteConfig: { + linuxFxVersion: 'DOCKER|${acrName}.azurecr.io/webapp:${imageTag}' + minTlsVersion: '1.2' + } + configs: concat([ + { + name: 'appsettings' + properties: { + SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' + DOCKER_REGISTRY_SERVER_URL: 'https://${acrName}.azurecr.io' + AUTH_ENABLED: 'false' + // Azure OpenAI Settings + AZURE_OPENAI_API_VERSION: azureOpenaiAPIVersion + AZURE_OPENAI_MODEL: gptModelName + AZURE_DALLE_MODEL: dalleModelName + AZURE_OPENAI_ENDPOINT: 'https://${aiFoundryAiServicesResourceName}.openai.azure.com/' + AZURE_OPENAI_RESOURCE: aiFoundryAiServicesResourceName + AZURE_OPENAI_PREVIEW_API_VERSION: azureOpenaiAPIVersion + // AI Agent Settings + AZURE_AI_AGENT_ENDPOINT: aiFoundryAiProjectEndpoint + AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME: gptModelName + AZURE_AI_AGENT_API_VERSION: azureAiAgentApiVersion + // Storage Settings + AZURE_STORAGE_ACCOUNT: storageAccountName + AZURE_STORAGE_PRODUCT_IMAGES_CONTAINER: productImagesContainer + AZURE_STORAGE_GENERATED_IMAGES_CONTAINER: generatedImagesContainer + // CosmosDB Settings + SOLUTION_NAME: solutionName + USE_CHAT_HISTORY_ENABLED: 'True' + AZURE_COSMOSDB_ACCOUNT: cosmosDB.outputs.name + AZURE_COSMOSDB_ACCOUNT_KEY: '' + AZURE_COSMOSDB_CONVERSATIONS_CONTAINER: cosmosDBConversationsContainer + AZURE_COSMOSDB_PRODUCTS_CONTAINER: cosmosDBProductsContainer + AZURE_COSMOSDB_DATABASE: cosmosDBDatabaseName + AZURE_COSMOSDB_ENABLE_FEEDBACK: azureCosmosDbEnableFeedback + // Brand Guidelines (configured via environment) + BRAND_TONE: 'Professional yet approachable' + BRAND_VOICE: 'Innovative, trustworthy, customer-focused' + BRAND_PROHIBITED_WORDS: 'guarantee,best,only,exclusive,cheapest' + BRAND_REQUIRED_DISCLOSURES: 'Terms apply,See details for eligibility' + BRAND_PRIMARY_COLOR: '#0078D4' + BRAND_SECONDARY_COLOR: '#107C10' + BRAND_IMAGE_STYLE: 'Modern, clean, minimalist with bright lighting' + BRAND_TYPOGRAPHY: 'Sans-serif, bold headlines, readable body text' + // App Settings + UWSGI_PROCESSES: '2' + UWSGI_THREADS: '2' + APP_ENV: appEnvironment + AZURE_CLIENT_ID: userAssignedIdentity.outputs.clientId + } + applicationInsightResourceId: enableMonitoring ? applicationInsights!.outputs.resourceId : null + } + ], enableMonitoring ? [ + { + name: 'logs' + properties: {} + } + ] : []) + enableMonitoring: enableMonitoring + diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null + vnetRouteAllEnabled: enablePrivateNetworking ? true : false + vnetImagePullEnabled: enablePrivateNetworking ? true : false + publicNetworkAccess: 'Enabled' + } +} + +// ========== Outputs ========== // +@description('Contains WebApp URL') +output WEB_APP_URL string = 'https://${webSite.outputs.name}.azurewebsites.net' + +@description('Contains Storage Account Name') +output STORAGE_ACCOUNT_NAME string = storageAccount.outputs.name + +@description('Contains Product Images Container') +output STORAGE_PRODUCT_IMAGES_CONTAINER string = productImagesContainer + +@description('Contains Generated Images Container') +output STORAGE_GENERATED_IMAGES_CONTAINER string = generatedImagesContainer + +@description('Contains KeyVault Name') +output KEY_VAULT_NAME string = keyvault.outputs.name + +@description('Contains CosmosDB Account Name') +output COSMOSDB_ACCOUNT_NAME string = cosmosDB.outputs.name + +@description('Contains CosmosDB Database Name') +output COSMOSDB_DATABASE_NAME string = cosmosDBDatabaseName + +@description('Contains CosmosDB Products Container') +output COSMOSDB_PRODUCTS_CONTAINER string = cosmosDBProductsContainer + +@description('Contains CosmosDB Conversations Container') +output COSMOSDB_CONVERSATIONS_CONTAINER string = cosmosDBConversationsContainer + +@description('Contains Resource Group Name') +output RESOURCE_GROUP_NAME string = resourceGroup().name + +@description('Contains AI Foundry Name') +output AI_FOUNDRY_NAME string = aiFoundryAiProjectResourceName + +@description('Contains AI Foundry RG Name') +output AI_FOUNDRY_RG_NAME string = aiFoundryAiServicesResourceGroupName + +@description('Contains AI Foundry Resource ID') +output AI_FOUNDRY_RESOURCE_ID string = useExistingAiFoundryAiProject ? '' : aiFoundryAiServices!.outputs.resourceId + +@description('Contains GPT Model') +output AZURE_OPENAI_MODEL string = gptModelName + +@description('Contains DALL-E Model') +output AZURE_DALLE_MODEL string = dalleModelName + +@description('Contains OpenAI Resource') +output AZURE_OPENAI_RESOURCE string = aiFoundryAiServicesResourceName + +@description('Contains AI Agent Endpoint') +output AZURE_AI_AGENT_ENDPOINT string = aiFoundryAiProjectEndpoint + +@description('Contains AI Agent API Version') +output AZURE_AI_AGENT_API_VERSION string = azureAiAgentApiVersion + +@description('Contains Application Insights Connection String') +output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = (enableMonitoring && !useExistingLogAnalytics) ? applicationInsights!.outputs.connectionString : '' + +@description('Contains Application Environment') +output APP_ENV string = appEnvironment diff --git a/content-gen/infra/modules/ai-project.bicep b/content-gen/infra/modules/ai-project.bicep new file mode 100644 index 000000000..6809b0aac --- /dev/null +++ b/content-gen/infra/modules/ai-project.bicep @@ -0,0 +1,58 @@ +@description('Required. Name of the AI Services project.') +param name string + +@description('Required. The location of the Project resource.') +param location string = resourceGroup().location + +@description('Optional. The description of the AI Foundry project to create. Defaults to the project name.') +param desc string = name + +@description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.') +param aiServicesName string + +@description('Required. Azure Existing AI Project ResourceID.') +param azureExistingAIProjectResourceId string = '' + +@description('Optional. Tags to be applied to the resources.') +param tags object = {} + +var useExistingAiFoundryAiProject = !empty(azureExistingAIProjectResourceId) +var existingOpenAIEndpoint = useExistingAiFoundryAiProject + ? format('https://{0}.openai.azure.com/', split(azureExistingAIProjectResourceId, '/')[8]) + : '' + +// Reference to cognitive service in current resource group for new projects +resource cogServiceReference 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = { + name: aiServicesName +} + +resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-06-01' = { + parent: cogServiceReference + name: name + tags: tags + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + description: desc + displayName: name + } +} + +@description('Required. Name of the AI project.') +output name string = aiProject.name + +@description('Required. Resource ID of the AI project.') +output resourceId string = aiProject.id + +@description('Required. API endpoint for the AI project.') +output apiEndpoint string = aiProject!.properties.endpoints['AI Foundry API'] + +@description('Contains AI Endpoint.') +output aoaiEndpoint string = !empty(existingOpenAIEndpoint) + ? existingOpenAIEndpoint + : cogServiceReference.properties.endpoints['OpenAI Language Model Instance API'] + +@description('Required. Principal ID of the AI project system-assigned managed identity.') +output systemAssignedMIPrincipalId string = aiProject.identity.principalId diff --git a/content-gen/infra/modules/web-sites.bicep b/content-gen/infra/modules/web-sites.bicep new file mode 100644 index 000000000..9f78af838 --- /dev/null +++ b/content-gen/infra/modules/web-sites.bicep @@ -0,0 +1,367 @@ +@description('Required. Name of the site.') +param name string + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +@description('Required. Type of site to deploy.') +@allowed([ + 'functionapp' + 'functionapp,linux' + 'functionapp,workflowapp' + 'functionapp,workflowapp,linux' + 'functionapp,linux,container' + 'functionapp,linux,container,azurecontainerapps' + 'app,linux' + 'app' + 'linux,api' + 'api' + 'app,linux,container' + 'app,container,windows' +]) +param kind string + +@description('Required. The resource ID of the app service plan to use for the site.') +param serverFarmResourceId string + +@description('Optional. Azure Resource Manager ID of the customers selected Managed Environment on which to host this app.') +param managedEnvironmentId string? + +@description('Optional. Configures a site to accept only HTTPS requests.') +param httpsOnly bool = true + +@description('Optional. If client affinity is enabled.') +param clientAffinityEnabled bool = true + +@description('Optional. The resource ID of the app service environment to use for this resource.') +param appServiceEnvironmentResourceId string? + +import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The managed identity definition for this resource.') +param managedIdentities managedIdentityAllType? + +@description('Optional. The resource ID of the assigned identity to be used to access a key vault with.') +param keyVaultAccessIdentityResourceId string? + +@description('Optional. Checks if Customer provided storage account is required.') +param storageAccountRequired bool = false + +@description('Optional. Enable monitoring and logging configuration.') +param enableMonitoring bool = false + +@description('Optional. Azure Resource Manager ID of the Virtual network and subnet to be joined by Regional VNET Integration.') +param virtualNetworkSubnetId string? + +@description('Optional. To enable accessing content over virtual network.') +param vnetContentShareEnabled bool = false + +@description('Optional. To enable pulling image over Virtual Network.') +param vnetImagePullEnabled bool = false + +@description('Optional. Virtual Network Route All enabled.') +param vnetRouteAllEnabled bool = false + +@description('Optional. Stop SCM (KUDU) site when the app is stopped.') +param scmSiteAlsoStopped bool = false + +@description('Optional. The site config object.') +param siteConfig resourceInput<'Microsoft.Web/sites@2024-04-01'>.properties.siteConfig = { + alwaysOn: true + minTlsVersion: '1.2' + ftpsState: 'FtpsOnly' +} + +@description('Optional. The web site config.') +param configs appSettingsConfigType[]? + +@description('Optional. The Function App configuration object.') +param functionAppConfig resourceInput<'Microsoft.Web/sites@2024-04-01'>.properties.functionAppConfig? + +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints.') +param privateEndpoints privateEndpointSingleServiceType[]? + +@description('Optional. Tags of the resource.') +param tags object? + +import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. The diagnostic settings of the service.') +param diagnosticSettings diagnosticSettingFullType[]? + +@description('Optional. To enable client certificate authentication (TLS mutual authentication).') +param clientCertEnabled bool = false + +@description('Optional. Client certificate authentication comma-separated exclusion paths.') +param clientCertExclusionPaths string? + +@description('Optional. Client certificate mode.') +@allowed([ + 'Optional' + 'OptionalInteractiveUser' + 'Required' +]) +param clientCertMode string = 'Optional' + +@description('Optional. If specified during app creation, the app is cloned from a source app.') +param cloningInfo resourceInput<'Microsoft.Web/sites@2024-04-01'>.properties.cloningInfo? + +@description('Optional. Size of the function container.') +param containerSize int? + +@description('Optional. Maximum allowed daily memory-time quota (applicable on dynamic apps only).') +param dailyMemoryTimeQuota int? + +@description('Optional. Setting this value to false disables the app (takes the app offline).') +param enabled bool = true + +@description('Optional. Hostname SSL states are used to manage the SSL bindings for app\'s hostnames.') +param hostNameSslStates resourceInput<'Microsoft.Web/sites@2024-04-01'>.properties.hostNameSslStates? + +@description('Optional. Hyper-V sandbox.') +param hyperV bool = false + +@description('Optional. Site redundancy mode.') +@allowed([ + 'ActiveActive' + 'Failover' + 'GeoRedundant' + 'Manual' + 'None' +]) +param redundancyMode string = 'None' + +@description('Optional. Whether or not public network access is allowed for this resource.') +@allowed([ + 'Enabled' + 'Disabled' +]) +param publicNetworkAccess string? + +@description('Optional. End to End Encryption Setting.') +param e2eEncryptionEnabled bool? + +@description('Optional. Property to configure various DNS related settings for a site.') +param dnsConfiguration resourceInput<'Microsoft.Web/sites@2024-04-01'>.properties.dnsConfiguration? + +@description('Optional. Specifies the scope of uniqueness for the default hostname during resource creation.') +@allowed([ + 'NoReuse' + 'ResourceGroupReuse' + 'SubscriptionReuse' + 'TenantReuse' +]) +param autoGeneratedDomainNameLabelScope string? + +var formattedUserAssignedIdentities = reduce( + map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), + {}, + (cur, next) => union(cur, next) +) + +var identity = !empty(managedIdentities) + ? { + type: (managedIdentities.?systemAssigned ?? false) + ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') + : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : 'None') + userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null + } + : null + +resource app 'Microsoft.Web/sites@2024-04-01' = { + name: name + location: location + kind: kind + tags: tags + identity: identity + properties: { + managedEnvironmentId: !empty(managedEnvironmentId) ? managedEnvironmentId : null + serverFarmId: serverFarmResourceId + clientAffinityEnabled: clientAffinityEnabled + httpsOnly: httpsOnly + hostingEnvironmentProfile: !empty(appServiceEnvironmentResourceId) + ? { + id: appServiceEnvironmentResourceId + } + : null + storageAccountRequired: storageAccountRequired + keyVaultReferenceIdentity: keyVaultAccessIdentityResourceId + virtualNetworkSubnetId: virtualNetworkSubnetId + siteConfig: siteConfig + functionAppConfig: functionAppConfig + clientCertEnabled: clientCertEnabled + clientCertExclusionPaths: clientCertExclusionPaths + clientCertMode: clientCertMode + cloningInfo: cloningInfo + containerSize: containerSize + dailyMemoryTimeQuota: dailyMemoryTimeQuota + enabled: enabled + hostNameSslStates: hostNameSslStates + hyperV: hyperV + redundancyMode: redundancyMode + publicNetworkAccess: !empty(publicNetworkAccess) + ? any(publicNetworkAccess) + : (!empty(privateEndpoints) ? 'Disabled' : 'Enabled') + vnetContentShareEnabled: vnetContentShareEnabled + vnetImagePullEnabled: vnetImagePullEnabled + vnetRouteAllEnabled: vnetRouteAllEnabled + scmSiteAlsoStopped: scmSiteAlsoStopped + endToEndEncryptionEnabled: e2eEncryptionEnabled + dnsConfiguration: dnsConfiguration + autoGeneratedDomainNameLabelScope: autoGeneratedDomainNameLabelScope + } +} + +module app_config 'web-sites.config.bicep' = [ + for (config, index) in (configs ?? []): { + name: '${uniqueString(deployment().name, location)}-Site-Config-${index}' + params: { + appName: app.name + name: config.name + applicationInsightResourceId: config.?applicationInsightResourceId + storageAccountResourceId: config.?storageAccountResourceId + storageAccountUseIdentityAuthentication: config.?storageAccountUseIdentityAuthentication + properties: config.?properties + currentAppSettings: config.?retainCurrentAppSettings ?? true && config.name == 'appsettings' + ? list('${app.id}/config/appsettings', '2023-12-01').properties + : {} + enableMonitoring: enableMonitoring + } + } +] + +#disable-next-line use-recent-api-versions +resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [ + for (diagnosticSetting, index) in (diagnosticSettings ?? []): { + name: diagnosticSetting.?name ?? '${name}-diagnosticSettings' + properties: { + storageAccountId: diagnosticSetting.?storageAccountResourceId + workspaceId: diagnosticSetting.?workspaceResourceId + eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId + eventHubName: diagnosticSetting.?eventHubName + metrics: [ + for group in (diagnosticSetting.?metricCategories ?? [{ category: 'AllMetrics' }]): { + category: group.category + enabled: group.?enabled ?? true + timeGrain: null + } + ] + logs: [ + for group in (diagnosticSetting.?logCategoriesAndGroups ?? [{ categoryGroup: 'allLogs' }]): { + categoryGroup: group.?categoryGroup + category: group.?category + enabled: group.?enabled ?? true + } + ] + marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId + logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType + } + scope: app + } +] + +module app_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [ + for (privateEndpoint, index) in (privateEndpoints ?? []): { + name: '${uniqueString(deployment().name, location)}-app-PrivateEndpoint-${index}' + scope: resourceGroup( + split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2], + split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4] + ) + params: { + name: privateEndpoint.?name ?? 'pep-${last(split(app.id, '/'))}-${privateEndpoint.?service ?? 'sites'}-${index}' + privateLinkServiceConnections: privateEndpoint.?isManualConnection != true + ? [ + { + name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(app.id, '/'))}-${privateEndpoint.?service ?? 'sites'}-${index}' + properties: { + privateLinkServiceId: app.id + groupIds: [ + privateEndpoint.?service ?? 'sites' + ] + } + } + ] + : null + manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true + ? [ + { + name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(app.id, '/'))}-${privateEndpoint.?service ?? 'sites'}-${index}' + properties: { + privateLinkServiceId: app.id + groupIds: [ + privateEndpoint.?service ?? 'sites' + ] + requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.' + } + } + ] + : null + subnetResourceId: privateEndpoint.subnetResourceId + enableTelemetry: false + location: privateEndpoint.?location ?? reference( + split(privateEndpoint.subnetResourceId, '/subnets/')[0], + '2020-06-01', + 'Full' + ).location + lock: privateEndpoint.?lock ?? null + privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup + roleAssignments: privateEndpoint.?roleAssignments + tags: privateEndpoint.?tags ?? tags + customDnsConfigs: privateEndpoint.?customDnsConfigs + ipConfigurations: privateEndpoint.?ipConfigurations + applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds + customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName + } + } +] + +@description('The name of the site.') +output name string = app.name + +@description('The resource ID of the site.') +output resourceId string = app.id + +@description('The resource group the site was deployed into.') +output resourceGroupName string = resourceGroup().name + +@description('The principal ID of the system assigned identity.') +output systemAssignedMIPrincipalId string? = app.?identity.?principalId + +@description('The location the resource was deployed into.') +output location string = app.location + +@description('Default hostname of the app.') +output defaultHostname string = 'https://${name}.azurewebsites.net' + +@description('Unique identifier that verifies the custom domains assigned to the app.') +output customDomainVerificationId string = app.properties.customDomainVerificationId + +@description('The outbound IP addresses of the app.') +output outboundIpAddresses string = app.properties.outboundIpAddresses + +// ================ // +// Definitions // +// ================ // +@export() +@description('The type of an app settings configuration.') +type appSettingsConfigType = { + @description('Required. The type of config.') + name: 'appsettings' | 'logs' + + @description('Optional. If the provided storage account requires Identity based authentication.') + storageAccountUseIdentityAuthentication: bool? + + @description('Optional. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions.') + storageAccountResourceId: string? + + @description('Optional. Resource ID of the application insight to leverage for this resource.') + applicationInsightResourceId: string? + + @description('Optional. The retain the current app settings. Defaults to true.') + retainCurrentAppSettings: bool? + + @description('Optional. The app settings key-value pairs.') + properties: { + @description('Required. An app settings key-value pair.') + *: string + }? +} diff --git a/content-gen/infra/modules/web-sites.config.bicep b/content-gen/infra/modules/web-sites.config.bicep new file mode 100644 index 000000000..34ff9e4c9 --- /dev/null +++ b/content-gen/infra/modules/web-sites.config.bicep @@ -0,0 +1,122 @@ +metadata name = 'Site App Settings' +metadata description = 'This module deploys a Site App Setting.' + +@description('Conditional. The name of the parent site resource. Required if the template is used in a standalone deployment.') +param appName string + +@description('Required. The name of the config.') +@allowed([ + 'appsettings' + 'authsettings' + 'authsettingsV2' + 'azurestorageaccounts' + 'backup' + 'connectionstrings' + 'logs' + 'metadata' + 'pushsettings' + 'slotConfigNames' + 'web' +]) +param name string + +@description('Optional. The properties of the config.') +param properties object = {} + +@description('Optional. If the provided storage account requires Identity based authentication.') +param storageAccountUseIdentityAuthentication bool = false + +@description('Optional. Required if app of kind functionapp. Resource ID of the storage account.') +param storageAccountResourceId string? + +@description('Optional. Resource ID of the application insight to leverage for this resource.') +param applicationInsightResourceId string? + +@description('Optional. The current app settings.') +param currentAppSettings { + @description('Required. The key-values pairs of the current app settings.') + *: string +} = {} + +@description('Optional. Enable monitoring and logging configuration.') +param enableMonitoring bool = false + +var azureWebJobsValues = !empty(storageAccountResourceId) && !storageAccountUseIdentityAuthentication + ? { + AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount!.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' + } + : !empty(storageAccountResourceId) && storageAccountUseIdentityAuthentication + ? { + AzureWebJobsStorage__accountName: storageAccount.name + AzureWebJobsStorage__blobServiceUri: storageAccount!.properties.primaryEndpoints.blob + AzureWebJobsStorage__queueServiceUri: storageAccount!.properties.primaryEndpoints.queue + AzureWebJobsStorage__tableServiceUri: storageAccount!.properties.primaryEndpoints.table + } + : {} + +var appInsightsValues = !empty(applicationInsightResourceId) + ? { + APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights!.properties.ConnectionString + } + : {} + +var loggingProperties = enableMonitoring && name == 'logs' + ? { + applicationLogs: { + fileSystem: { + level: 'Verbose' + } + } + httpLogs: { + fileSystem: { + enabled: true + retentionInDays: 3 + retentionInMb: 100 + } + } + detailedErrorMessages: { + enabled: true + } + failedRequestsTracing: { + enabled: true + } + } + : {} + +var expandedProperties = union( + properties, + currentAppSettings, + azureWebJobsValues, + appInsightsValues, + loggingProperties +) + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightResourceId)) { + name: last(split(applicationInsightResourceId!, '/')) + scope: resourceGroup(split(applicationInsightResourceId!, '/')[2], split(applicationInsightResourceId!, '/')[4]) +} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' existing = if (!empty(storageAccountResourceId)) { + name: last(split(storageAccountResourceId!, '/')) + scope: resourceGroup(split(storageAccountResourceId!, '/')[2], split(storageAccountResourceId!, '/')[4]) +} + +resource app 'Microsoft.Web/sites@2023-12-01' existing = { + name: appName +} + +resource config 'Microsoft.Web/sites/config@2024-04-01' = { + parent: app + #disable-next-line BCP225 + name: name + properties: expandedProperties +} + +@description('The name of the site config.') +output name string = config.name + +@description('The resource ID of the site config.') +output resourceId string = config.id + +@description('The resource group the site config was deployed into.') +output resourceGroupName string = resourceGroup().name diff --git a/content-gen/scripts/create_image_search_index.py b/content-gen/scripts/create_image_search_index.py new file mode 100644 index 000000000..c67a613e8 --- /dev/null +++ b/content-gen/scripts/create_image_search_index.py @@ -0,0 +1,612 @@ +""" +Create Azure AI Search Index for Image Grounding. + +This script creates a search index for product images with: +- Image metadata (name, description, colors, style) +- Vector embeddings for semantic search +- Blob storage integration for image retrieval + +Uses DefaultAzureCredential for authentication (RBAC). +""" + +import asyncio +import json +import os +import sys +from pathlib import Path +from typing import List, Dict, Any + +from azure.identity import DefaultAzureCredential +from azure.search.documents import SearchClient +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + SearchIndex, + SearchField, + SearchFieldDataType, + SimpleField, + SearchableField, + VectorSearch, + VectorSearchProfile, + HnswAlgorithmConfiguration, + SemanticConfiguration, + SemanticField, + SemanticPrioritizedFields, + SemanticSearch, +) +from azure.core.credentials import AzureKeyCredential +from azure.storage.blob.aio import BlobServiceClient +from dotenv import load_dotenv + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +# Load environment variables +env_path = Path(__file__).parent.parent / ".env" +load_dotenv(env_path) + +# Configuration +SEARCH_ENDPOINT = os.getenv("AZURE_AI_SEARCH_ENDPOINT", "https://search-contentgen-jh.search.windows.net") +SEARCH_INDEX_NAME = os.getenv("AZURE_AI_SEARCH_IMAGE_INDEX", "product-images") +STORAGE_ACCOUNT_NAME = os.getenv("AZURE_BLOB_ACCOUNT_NAME", "storagecontentgenjh") +CONTAINER_NAME = os.getenv("AZURE_BLOB_PRODUCT_IMAGES_CONTAINER", "product-images") + +AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT", "") +AZURE_OPENAI_EMBEDDING_MODEL = os.getenv("AZURE_OPENAI_EMBEDDING_MODEL", "text-embedding-ada-002") +AZURE_SEARCH_ADMIN_KEY = os.getenv("AZURE_AI_SEARCH_ADMIN_KEY", "") + +# Image metadata - derived from filenames with color/style info +IMAGE_METADATA = { + "BlueAsh.jpg": { + "name": "BlueAsh", + "primary_color": "Blue", + "secondary_color": "Gray", + "color_family": "Cool", + "mood": "Calm, Professional", + "style": "Modern, Minimalist", + "description": "A sophisticated blue-ash tone with subtle gray undertones. Perfect for professional and calming aesthetics.", + "use_cases": "Corporate branding, tech products, wellness, professional services", + "keywords": ["blue", "ash", "gray", "calm", "professional", "modern", "cool tones"] + }, + "CloudDrift.jpg": { + "name": "CloudDrift", + "primary_color": "White", + "secondary_color": "Light Gray", + "color_family": "Neutral", + "mood": "Ethereal, Peaceful", + "style": "Soft, Dreamy", + "description": "A soft, cloudy white with gentle drifting patterns. Evokes serenity and openness.", + "use_cases": "Spa, wellness, bedding, clean beauty, minimalist design", + "keywords": ["white", "cloud", "soft", "peaceful", "ethereal", "clean", "neutral"] + }, + "FogHarbor.jpg": { + "name": "FogHarbor", + "primary_color": "Gray", + "secondary_color": "Blue-Gray", + "color_family": "Cool", + "mood": "Mysterious, Coastal", + "style": "Moody, Atmospheric", + "description": "A misty gray reminiscent of coastal fog rolling into harbor. Mysterious yet inviting.", + "use_cases": "Nautical themes, outdoor gear, photography, artisanal products", + "keywords": ["fog", "gray", "harbor", "coastal", "misty", "moody", "atmospheric"] + }, + "GlacierTint.jpg": { + "name": "GlacierTint", + "primary_color": "Ice Blue", + "secondary_color": "White", + "color_family": "Cool", + "mood": "Fresh, Crisp", + "style": "Clean, Nordic", + "description": "A crisp ice-blue tint inspired by glacial formations. Fresh and invigorating.", + "use_cases": "Skincare, beverages, winter sports, Scandinavian design", + "keywords": ["glacier", "ice", "blue", "fresh", "crisp", "nordic", "winter"] + }, + "GraphiteFade.jpg": { + "name": "GraphiteFade", + "primary_color": "Dark Gray", + "secondary_color": "Charcoal", + "color_family": "Dark", + "mood": "Sophisticated, Bold", + "style": "Industrial, Premium", + "description": "A deep graphite with gradient fade effect. Sophisticated and premium feel.", + "use_cases": "Luxury goods, electronics, automotive, high-end fashion", + "keywords": ["graphite", "dark", "gray", "sophisticated", "premium", "industrial", "bold"] + }, + "ObsidianPearl.jpg": { + "name": "ObsidianPearl", + "primary_color": "Black", + "secondary_color": "Pearl White", + "color_family": "Contrast", + "mood": "Elegant, Luxurious", + "style": "High-contrast, Dramatic", + "description": "A striking contrast of deep obsidian black with pearlescent highlights. Ultimate elegance.", + "use_cases": "Jewelry, luxury accessories, evening wear, premium packaging", + "keywords": ["obsidian", "pearl", "black", "white", "elegant", "luxurious", "contrast"] + }, + "OliveStone.jpg": { + "name": "OliveStone", + "primary_color": "Olive Green", + "secondary_color": "Brown", + "color_family": "Earth", + "mood": "Natural, Grounded", + "style": "Organic, Rustic", + "description": "An earthy olive green with stone-like undertones. Natural and grounded aesthetic.", + "use_cases": "Organic products, outdoor brands, home decor, sustainable fashion", + "keywords": ["olive", "green", "stone", "earth", "natural", "grounded", "organic"] + }, + "PineShadow.jpg": { + "name": "PineShadow", + "primary_color": "Dark Green", + "secondary_color": "Forest Green", + "color_family": "Nature", + "mood": "Deep, Mysterious", + "style": "Natural, Rich", + "description": "A deep pine green with shadowy depth. Evokes dense forests and natural mystery.", + "use_cases": "Outdoor recreation, eco brands, luxury camping, artisan products", + "keywords": ["pine", "forest", "green", "shadow", "deep", "natural", "mysterious"] + }, + "PorcelainMist.jpg": { + "name": "PorcelainMist", + "primary_color": "Cream", + "secondary_color": "Soft White", + "color_family": "Warm Neutral", + "mood": "Delicate, Refined", + "style": "Classic, Elegant", + "description": "A delicate porcelain cream with misty softness. Classic elegance and refinement.", + "use_cases": "Ceramics, fine dining, bridal, luxury stationery", + "keywords": ["porcelain", "cream", "mist", "delicate", "refined", "classic", "elegant"] + }, + "QuietMoss.jpg": { + "name": "QuietMoss", + "primary_color": "Moss Green", + "secondary_color": "Sage", + "color_family": "Nature", + "mood": "Tranquil, Peaceful", + "style": "Organic, Calming", + "description": "A quiet moss green that brings tranquility and connection to nature.", + "use_cases": "Wellness brands, botanical products, sustainable goods, meditation spaces", + "keywords": ["moss", "green", "quiet", "tranquil", "peaceful", "organic", "calming"] + }, + "SeafoamLight.jpg": { + "name": "SeafoamLight", + "primary_color": "Seafoam", + "secondary_color": "Mint", + "color_family": "Cool", + "mood": "Fresh, Playful", + "style": "Coastal, Light", + "description": "A light seafoam with minty freshness. Playful yet sophisticated coastal vibe.", + "use_cases": "Summer collections, beach products, refreshing beverages, youth brands", + "keywords": ["seafoam", "mint", "light", "fresh", "playful", "coastal", "summer"] + }, + "SilverShore.jpg": { + "name": "SilverShore", + "primary_color": "Silver", + "secondary_color": "Sand", + "color_family": "Metallic Neutral", + "mood": "Sophisticated, Modern", + "style": "Sleek, Contemporary", + "description": "A sleek silver with sandy shore warmth. Modern sophistication meets coastal warmth.", + "use_cases": "Tech accessories, modern home, jewelry, premium retail", + "keywords": ["silver", "shore", "sand", "sophisticated", "modern", "sleek", "metallic"] + }, + "SnowVeil.jpg": { + "name": "SnowVeil", + "primary_color": "Pure White", + "secondary_color": "Ice", + "color_family": "Cool Neutral", + "mood": "Pure, Serene", + "style": "Minimal, Clean", + "description": "A pure snow white with delicate veil-like softness. Ultimate purity and serenity.", + "use_cases": "Bridal, skincare, clean tech, medical, minimalist design", + "keywords": ["snow", "white", "pure", "serene", "minimal", "clean", "veil"] + }, + "SteelSky.jpg": { + "name": "SteelSky", + "primary_color": "Steel Blue", + "secondary_color": "Gray", + "color_family": "Cool", + "mood": "Strong, Reliable", + "style": "Industrial, Modern", + "description": "A strong steel blue with sky-like openness. Combines strength with aspiration.", + "use_cases": "Automotive, aerospace, industrial design, corporate, sports gear", + "keywords": ["steel", "blue", "sky", "strong", "reliable", "industrial", "modern"] + }, + "StoneDusk.jpg": { + "name": "StoneDusk", + "primary_color": "Warm Gray", + "secondary_color": "Taupe", + "color_family": "Warm Neutral", + "mood": "Warm, Inviting", + "style": "Rustic, Cozy", + "description": "A warm stone gray with dusk-like golden undertones. Inviting and comfortable.", + "use_cases": "Home decor, hospitality, artisan goods, coffee shops", + "keywords": ["stone", "dusk", "warm", "gray", "taupe", "inviting", "cozy"] + }, + "VerdantHaze.jpg": { + "name": "VerdantHaze", + "primary_color": "Green", + "secondary_color": "Teal", + "color_family": "Nature", + "mood": "Lush, Vibrant", + "style": "Tropical, Fresh", + "description": "A lush verdant green with hazy tropical depth. Vibrant and full of life.", + "use_cases": "Tropical products, plant-based brands, health foods, spa resorts", + "keywords": ["verdant", "green", "haze", "lush", "vibrant", "tropical", "fresh"] + } +} + + +def create_search_index(index_client: SearchIndexClient) -> SearchIndex: + """Create the search index schema for product images.""" + + fields = [ + # Key field + SimpleField( + name="id", + type=SearchFieldDataType.String, + key=True, + filterable=True + ), + # Image identification + SearchableField( + name="filename", + type=SearchFieldDataType.String, + filterable=True, + sortable=True + ), + SearchableField( + name="name", + type=SearchFieldDataType.String, + filterable=True, + sortable=True + ), + # Color fields + SearchableField( + name="primary_color", + type=SearchFieldDataType.String, + filterable=True, + facetable=True + ), + SearchableField( + name="secondary_color", + type=SearchFieldDataType.String, + filterable=True, + facetable=True + ), + SearchableField( + name="color_family", + type=SearchFieldDataType.String, + filterable=True, + facetable=True + ), + # Style and mood + SearchableField( + name="mood", + type=SearchFieldDataType.String, + filterable=True + ), + SearchableField( + name="style", + type=SearchFieldDataType.String, + filterable=True + ), + # Description and use cases + SearchableField( + name="description", + type=SearchFieldDataType.String + ), + SearchableField( + name="use_cases", + type=SearchFieldDataType.String + ), + # Keywords for search + SimpleField( + name="keywords", + type=SearchFieldDataType.Collection(SearchFieldDataType.String), + filterable=True, + facetable=True + ), + # Storage info + SimpleField( + name="blob_url", + type=SearchFieldDataType.String + ), + SimpleField( + name="blob_container", + type=SearchFieldDataType.String, + filterable=True + ), + # Combined text for embedding + SearchableField( + name="combined_text", + type=SearchFieldDataType.String + ), + # Vector field for semantic search + SearchField( + name="content_vector", + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + searchable=True, + vector_search_dimensions=1536, # text-embedding-ada-002 dimensions + vector_search_profile_name="image-vector-profile" + ) + ] + + # Configure vector search + vector_search = VectorSearch( + algorithms=[ + HnswAlgorithmConfiguration( + name="hnsw-algorithm", + parameters={ + "m": 4, + "efConstruction": 400, + "efSearch": 500, + "metric": "cosine" + } + ) + ], + profiles=[ + VectorSearchProfile( + name="image-vector-profile", + algorithm_configuration_name="hnsw-algorithm" + ) + ] + ) + + # Configure semantic search + semantic_config = SemanticConfiguration( + name="image-semantic-config", + prioritized_fields=SemanticPrioritizedFields( + title_field=SemanticField(field_name="name"), + content_fields=[ + SemanticField(field_name="description"), + SemanticField(field_name="use_cases"), + SemanticField(field_name="combined_text") + ], + keywords_fields=[ + SemanticField(field_name="primary_color"), + SemanticField(field_name="style"), + SemanticField(field_name="mood") + ] + ) + ) + + semantic_search = SemanticSearch(configurations=[semantic_config]) + + # Create the index + index = SearchIndex( + name=SEARCH_INDEX_NAME, + fields=fields, + vector_search=vector_search, + semantic_search=semantic_search + ) + + return index + + +async def get_embedding(text: str) -> List[float]: + """Get embedding vector for text using Azure OpenAI.""" + from openai import AzureOpenAI + + client = AzureOpenAI( + azure_endpoint=AZURE_OPENAI_ENDPOINT, + azure_ad_token_provider=lambda: DefaultAzureCredential().get_token( + "https://cognitiveservices.azure.com/.default" + ).token, + api_version="2024-06-01" + ) + + try: + response = client.embeddings.create( + input=text, + model=AZURE_OPENAI_EMBEDDING_MODEL + ) + return response.data[0].embedding + except Exception as e: + print(f"Warning: Could not generate embedding: {e}") + # Return zero vector as fallback + return [0.0] * 1536 + + +async def get_blob_images() -> List[Dict[str, Any]]: + """Get list of images from blob storage.""" + account_url = f"https://{STORAGE_ACCOUNT_NAME}.blob.core.windows.net" + credential = DefaultAzureCredential() + + images = [] + + async with BlobServiceClient(account_url=account_url, credential=credential) as blob_service: + container_client = blob_service.get_container_client(CONTAINER_NAME) + + async for blob in container_client.list_blobs(): + if blob.name.lower().endswith(('.jpg', '.jpeg')): + images.append({ + "filename": blob.name, + "url": f"{account_url}/{CONTAINER_NAME}/{blob.name}", + "size": blob.size + }) + + return images + + +def prepare_document(filename: str, blob_url: str) -> Dict[str, Any]: + """Prepare a document for indexing.""" + + # Get metadata for this image + metadata = IMAGE_METADATA.get(filename, { + "name": filename.replace(".jpg", "").replace(".JPG", ""), + "primary_color": "Unknown", + "secondary_color": "Unknown", + "color_family": "Unknown", + "mood": "Unknown", + "style": "Unknown", + "description": f"Image file: {filename}", + "use_cases": "General marketing", + "keywords": [filename.lower().replace(".jpg", "")] + }) + + # Create combined text for embedding + combined_text = f""" + {metadata['name']} - {metadata['description']} + Colors: {metadata['primary_color']}, {metadata['secondary_color']} ({metadata['color_family']}) + Mood: {metadata['mood']} + Style: {metadata['style']} + Use Cases: {metadata['use_cases']} + Keywords: {', '.join(metadata['keywords'])} + """ + + # Create document ID from filename + doc_id = filename.lower().replace(".jpg", "").replace(" ", "-").replace(".", "-") + + return { + "id": doc_id, + "filename": filename, + "name": metadata["name"], + "primary_color": metadata["primary_color"], + "secondary_color": metadata["secondary_color"], + "color_family": metadata["color_family"], + "mood": metadata["mood"], + "style": metadata["style"], + "description": metadata["description"], + "use_cases": metadata["use_cases"], + "keywords": metadata["keywords"], + "blob_url": blob_url, + "blob_container": CONTAINER_NAME, + "combined_text": combined_text.strip() + } + + +async def index_images(search_client: SearchClient, images: List[Dict[str, Any]], use_vectors: bool = True): + """Index images into the search index.""" + + documents = [] + + for img in images: + doc = prepare_document(img["filename"], img["url"]) + + if use_vectors: + try: + # Generate embedding for the combined text + embedding = await get_embedding(doc["combined_text"]) + doc["content_vector"] = embedding + print(f" ✓ Generated embedding for: {img['filename']}") + except Exception as e: + print(f" ⚠ Embedding failed for {img['filename']}: {e}") + doc["content_vector"] = [0.0] * 1536 + else: + doc["content_vector"] = [0.0] * 1536 + + documents.append(doc) + + # Upload documents to the index + result = search_client.upload_documents(documents) + + succeeded = sum(1 for r in result if r.succeeded) + failed = sum(1 for r in result if not r.succeeded) + + return succeeded, failed + + +async def main(): + """Main entry point.""" + print("=" * 60) + print("Azure AI Search Index Creation for Product Images") + print("=" * 60) + print() + + # Check for embedding model deployment + use_vectors = bool(AZURE_OPENAI_ENDPOINT) and bool(AZURE_OPENAI_EMBEDDING_MODEL) + if not use_vectors: + print("⚠ Warning: Azure OpenAI not configured. Creating index without embeddings.") + print(" Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_EMBEDDING_MODEL in .env") + print() + + # Prefer RBAC (DefaultAzureCredential), fall back to API key if RBAC fails + try: + search_credential = DefaultAzureCredential() + # Test the credential + test_client = SearchIndexClient(endpoint=SEARCH_ENDPOINT, credential=search_credential) + list(test_client.list_indexes()) # Quick test + print("Using RBAC authentication for search (DefaultAzureCredential)") + except Exception as e: + if AZURE_SEARCH_ADMIN_KEY: + search_credential = AzureKeyCredential(AZURE_SEARCH_ADMIN_KEY) + print("Using API key authentication for search (RBAC failed)") + else: + print(f"⚠ RBAC failed and no API key configured: {e}") + raise + + # Create index client + index_client = SearchIndexClient( + endpoint=SEARCH_ENDPOINT, + credential=search_credential + ) + + # Create or update the index + print(f"Creating search index: {SEARCH_INDEX_NAME}") + print(f"Search endpoint: {SEARCH_ENDPOINT}") + print() + + try: + index = create_search_index(index_client) + result = index_client.create_or_update_index(index) + print(f"✓ Index '{result.name}' created/updated successfully") + except Exception as e: + print(f"✗ Failed to create index: {e}") + raise + + # Get images from blob storage + print() + print("Fetching images from blob storage...") + + try: + images = await get_blob_images() + print(f"Found {len(images)} images in blob storage") + except Exception as e: + print(f"✗ Failed to list blob images: {e}") + print(" Make sure images are uploaded to blob storage first.") + print(" Run: python upload_images.py") + return + + if not images: + print("No images found. Please upload images first using upload_images.py") + return + + # Index the images + print() + print("Indexing images...") + print("-" * 50) + + search_client = SearchClient( + endpoint=SEARCH_ENDPOINT, + index_name=SEARCH_INDEX_NAME, + credential=search_credential + ) + + try: + succeeded, failed = await index_images(search_client, images, use_vectors=use_vectors) + print("-" * 50) + print(f"\n✓ Indexed {succeeded} images successfully") + if failed > 0: + print(f"✗ Failed to index {failed} images") + except Exception as e: + print(f"✗ Failed to index images: {e}") + raise + + # Summary + print() + print("=" * 60) + print("Index Creation Complete!") + print("=" * 60) + print(f"Index name: {SEARCH_INDEX_NAME}") + print(f"Endpoint: {SEARCH_ENDPOINT}") + print(f"Documents indexed: {succeeded}") + print() + print("You can now use this index for grounding AI content generation.") + print() + print("Example search queries:") + print(" - Search by color: 'blue', 'green', 'warm tones'") + print(" - Search by mood: 'calm', 'energetic', 'professional'") + print(" - Search by use case: 'luxury products', 'outdoor brands'") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/content-gen/scripts/deploy.ps1 b/content-gen/scripts/deploy.ps1 new file mode 100644 index 000000000..869a8255e --- /dev/null +++ b/content-gen/scripts/deploy.ps1 @@ -0,0 +1,101 @@ +# PowerShell deployment script for Content Generation Solution Accelerator + +$ErrorActionPreference = "Stop" + +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "Content Generation Solution Accelerator" -ForegroundColor Cyan +Write-Host "Deployment Script" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan + +# Check prerequisites +Write-Host "Checking prerequisites..." -ForegroundColor Yellow + +$azCli = Get-Command az -ErrorAction SilentlyContinue +if (-not $azCli) { + Write-Host "Error: Azure CLI is not installed. Please install it first." -ForegroundColor Red + exit 1 +} + +$azdCli = Get-Command azd -ErrorAction SilentlyContinue +if (-not $azdCli) { + Write-Host "Error: Azure Developer CLI (azd) is not installed. Please install it first." -ForegroundColor Red + exit 1 +} + +# Check Azure login +Write-Host "Checking Azure login status..." -ForegroundColor Yellow +try { + az account show | Out-Null +} catch { + Write-Host "Not logged in to Azure. Running 'az login'..." -ForegroundColor Yellow + az login +} + +# Get current directory +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ProjectDir = Split-Path -Parent $ScriptDir + +Set-Location $ProjectDir + +# Check if environment is initialized +if (-not (Test-Path ".azure/config.json")) { + Write-Host "Initializing Azure Developer CLI environment..." -ForegroundColor Yellow + azd init +} + +# Deploy infrastructure and application +Write-Host "" +Write-Host "Starting deployment..." -ForegroundColor Green +Write-Host "This will deploy the following resources:" -ForegroundColor White +Write-Host " - Azure AI Foundry (GPT-5, DALL-E 3)" -ForegroundColor White +Write-Host " - Azure Cosmos DB (products, conversations)" -ForegroundColor White +Write-Host " - Azure Blob Storage (images)" -ForegroundColor White +Write-Host " - Azure AI Search" -ForegroundColor White +Write-Host " - Azure App Service" -ForegroundColor White +Write-Host " - Azure Key Vault" -ForegroundColor White +Write-Host "" + +$continue = Read-Host "Continue with deployment? (y/n)" + +if ($continue -eq "y" -or $continue -eq "Y") { + # Run azd up + azd up + + Write-Host "" + Write-Host "============================================" -ForegroundColor Green + Write-Host "Deployment complete!" -ForegroundColor Green + Write-Host "============================================" -ForegroundColor Green + + # Get deployment outputs + Write-Host "" + Write-Host "Getting deployment information..." -ForegroundColor Yellow + + # Show the app URL + $envValues = azd env get-values + $webappUrl = ($envValues | Where-Object { $_ -match "WEBAPP_URL" }) -replace "WEBAPP_URL=", "" -replace '"', "" + + if ($webappUrl) { + Write-Host "" + Write-Host "Application URL: $webappUrl" -ForegroundColor Cyan + Write-Host "" + } + + # Load sample data + $loadData = Read-Host "Load sample product data? (y/n)" + if ($loadData -eq "y" -or $loadData -eq "Y") { + Write-Host "Loading sample data..." -ForegroundColor Yellow + Set-Location src + python ../scripts/load_sample_data.py + Set-Location .. + Write-Host "Sample data loaded successfully!" -ForegroundColor Green + } + + Write-Host "" + Write-Host "Next steps:" -ForegroundColor White + Write-Host "1. Visit the application URL to start using the Content Generation Accelerator" -ForegroundColor White + Write-Host "2. Configure brand guidelines in the Azure Portal or .env file" -ForegroundColor White + Write-Host "3. Add your product catalog via the API or CosmosDB" -ForegroundColor White + Write-Host "" +} else { + Write-Host "Deployment cancelled." -ForegroundColor Yellow +} diff --git a/content-gen/scripts/deploy.sh b/content-gen/scripts/deploy.sh new file mode 100644 index 000000000..678111f36 --- /dev/null +++ b/content-gen/scripts/deploy.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Deployment script for Content Generation Solution Accelerator + +set -e + +echo "============================================" +echo "Content Generation Solution Accelerator" +echo "Deployment Script" +echo "============================================" + +# Check prerequisites +echo "Checking prerequisites..." + +if ! command -v az &> /dev/null; then + echo "Error: Azure CLI is not installed. Please install it first." + exit 1 +fi + +if ! command -v azd &> /dev/null; then + echo "Error: Azure Developer CLI (azd) is not installed. Please install it first." + exit 1 +fi + +# Check Azure login +echo "Checking Azure login status..." +az account show &> /dev/null || { + echo "Not logged in to Azure. Running 'az login'..." + az login +} + +# Get current directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +cd "$PROJECT_DIR" + +# Check if environment is initialized +if [ ! -f ".azure/config.json" ]; then + echo "Initializing Azure Developer CLI environment..." + azd init +fi + +# Deploy infrastructure and application +echo "" +echo "Starting deployment..." +echo "This will deploy the following resources:" +echo " - Azure AI Foundry (GPT-5, DALL-E 3)" +echo " - Azure Cosmos DB (products, conversations)" +echo " - Azure Blob Storage (images)" +echo " - Azure AI Search" +echo " - Azure App Service" +echo " - Azure Key Vault" +echo "" + +read -p "Continue with deployment? (y/n) " -n 1 -r +echo "" + +if [[ $REPLY =~ ^[Yy]$ ]]; then + # Run azd up + azd up + + echo "" + echo "============================================" + echo "Deployment complete!" + echo "============================================" + + # Get deployment outputs + echo "" + echo "Getting deployment information..." + + # Show the app URL + WEBAPP_URL=$(azd env get-values | grep WEBAPP_URL | cut -d'=' -f2 | tr -d '"') + if [ -n "$WEBAPP_URL" ]; then + echo "" + echo "Application URL: $WEBAPP_URL" + echo "" + fi + + # Load sample data + read -p "Load sample product data? (y/n) " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Loading sample data..." + cd src + python ../scripts/load_sample_data.py + cd .. + echo "Sample data loaded successfully!" + fi + + echo "" + echo "Next steps:" + echo "1. Visit the application URL to start using the Content Generation Accelerator" + echo "2. Configure brand guidelines in the Azure Portal or .env file" + echo "3. Add your product catalog via the API or CosmosDB" + echo "" +else + echo "Deployment cancelled." +fi diff --git a/content-gen/scripts/images/BlueAsh.jpg b/content-gen/scripts/images/BlueAsh.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fb852bad59f64ce9c38864ffd734302a63a5f97a GIT binary patch literal 10472 zcmex=Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2nfr~+dVZkUh8U&-MU^F9)mIb55;b@I8S{shmhNHFNXl*!J8;;h7qqX5^ zZ8%yRj@E{wwc%)OI9eNy)`p|C;b?6*S{shmhNHFNXl*!J8;;h7qqX5^Z8%yRj@E{w Xwc%)OI9eNy)`p|CA%WVE;r~ql`*I1A literal 0 HcmV?d00001 diff --git a/content-gen/scripts/images/GraphiteFade.jpg b/content-gen/scripts/images/GraphiteFade.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f585f9a000a0b9316a4b155525e262f4ba9909eb GIT binary patch literal 10471 zcmex=Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2plh)MyZlrh?IoFj^Lj7KfuX!f0(cS{shmhNHFNXl*!J8;;h7 zqqX5^Z8%yRj@E{wwc%)OI9eNy)`p|C;b?6*S{shmhNHFNXl*!J8;;h7qqX5^Z8%yR Xj@E{wwc%)OI9eNy)`kRX!~Zt{a~%qg literal 0 HcmV?d00001 diff --git a/content-gen/scripts/images/SnowVeil.jpg b/content-gen/scripts/images/SnowVeil.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3a63aa9cd0fb92094ba482f6608b0dfe3b7e2969 GIT binary patch literal 10471 zcmex=Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2T^ literal 0 HcmV?d00001 diff --git a/content-gen/scripts/images/StoneDusk.jpg b/content-gen/scripts/images/StoneDusk.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3504cea7e2ebdf454742794aeb5b7f67ef8f4082 GIT binary patch literal 10472 zcmex=Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2Jq?U}9uuW@2GxWo2Ojs;&jfGq4D< z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2!T5!vdxTE{0KRGzdmh!DvPpEel4A!_gXHv^E^A4M%Ik(b{mdHXN-DM{C2; z+HkZs9IXvUYs1mnaI`iYtqn(O!_nGsv^E^A4M%Ik(b{mdHXN-DM{C2;+HkZs9IXvU WYs1mnaI`iYtqn(OLjtwo|C<0QG6|Ug literal 0 HcmV?d00001 diff --git a/content-gen/scripts/index_products.py b/content-gen/scripts/index_products.py new file mode 100644 index 000000000..5b1c23eab --- /dev/null +++ b/content-gen/scripts/index_products.py @@ -0,0 +1,324 @@ +""" +Index CosmosDB Products into Azure AI Search. + +This script reads products from CosmosDB and indexes them into Azure AI Search +for use in grounding AI content generation. +""" + +import asyncio +import os +import sys +from pathlib import Path +from typing import List, Dict, Any + +from azure.identity import DefaultAzureCredential +from azure.search.documents import SearchClient +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + SearchIndex, + SearchField, + SearchFieldDataType, + SimpleField, + SearchableField, + VectorSearch, + VectorSearchProfile, + HnswAlgorithmConfiguration, + SemanticConfiguration, + SemanticField, + SemanticPrioritizedFields, + SemanticSearch, +) +from azure.core.credentials import AzureKeyCredential +from dotenv import load_dotenv + +# Add src to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +# Load environment variables +env_path = Path(__file__).parent.parent / ".env" +load_dotenv(env_path) + +# Configuration +SEARCH_ENDPOINT = os.getenv("AZURE_AI_SEARCH_ENDPOINT", "https://search-contentgen-jh.search.windows.net") +SEARCH_INDEX_NAME = os.getenv("AZURE_AI_SEARCH_PRODUCTS_INDEX", "products") +AZURE_SEARCH_ADMIN_KEY = os.getenv("AZURE_AI_SEARCH_ADMIN_KEY", "") + + +def create_products_index(index_client: SearchIndexClient) -> SearchIndex: + """Create the search index schema for products.""" + + fields = [ + # Key field - use SKU as unique identifier + SimpleField( + name="id", + type=SearchFieldDataType.String, + key=True, + filterable=True + ), + # Product identification + SearchableField( + name="product_name", + type=SearchFieldDataType.String, + filterable=True, + sortable=True + ), + SearchableField( + name="sku", + type=SearchFieldDataType.String, + filterable=True + ), + SearchableField( + name="model", + type=SearchFieldDataType.String, + filterable=True + ), + # Categories + SearchableField( + name="category", + type=SearchFieldDataType.String, + filterable=True, + facetable=True + ), + SearchableField( + name="sub_category", + type=SearchFieldDataType.String, + filterable=True, + facetable=True + ), + # Descriptions + SearchableField( + name="marketing_description", + type=SearchFieldDataType.String + ), + SearchableField( + name="detailed_spec_description", + type=SearchFieldDataType.String + ), + SearchableField( + name="image_description", + type=SearchFieldDataType.String + ), + # Combined text for search + SearchableField( + name="combined_text", + type=SearchFieldDataType.String + ), + # Vector field for semantic search (optional - requires embedding model) + SearchField( + name="content_vector", + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + searchable=True, + vector_search_dimensions=1536, + vector_search_profile_name="product-vector-profile" + ) + ] + + # Configure vector search + vector_search = VectorSearch( + algorithms=[ + HnswAlgorithmConfiguration( + name="hnsw-algorithm" + ) + ], + profiles=[ + VectorSearchProfile( + name="product-vector-profile", + algorithm_configuration_name="hnsw-algorithm" + ) + ] + ) + + # Configure semantic search + semantic_config = SemanticConfiguration( + name="product-semantic-config", + prioritized_fields=SemanticPrioritizedFields( + title_field=SemanticField(field_name="product_name"), + content_fields=[ + SemanticField(field_name="marketing_description"), + SemanticField(field_name="detailed_spec_description"), + SemanticField(field_name="image_description"), + SemanticField(field_name="combined_text") + ], + keywords_fields=[ + SemanticField(field_name="category"), + SemanticField(field_name="sub_category"), + SemanticField(field_name="sku") + ] + ) + ) + + semantic_search = SemanticSearch(configurations=[semantic_config]) + + # Create the index + index = SearchIndex( + name=SEARCH_INDEX_NAME, + fields=fields, + vector_search=vector_search, + semantic_search=semantic_search + ) + + return index + + +def prepare_product_document(product: Dict[str, Any]) -> Dict[str, Any]: + """Prepare a product document for indexing.""" + + # Create combined text for search + combined_text = f""" + {product.get('product_name', '')} + Category: {product.get('category', '')} - {product.get('sub_category', '')} + SKU: {product.get('sku', '')} | Model: {product.get('model', '')} + + Marketing: {product.get('marketing_description', '')} + + Specifications: {product.get('detailed_spec_description', '')} + + Visual: {product.get('image_description', '')} + """ + + # Create document ID from SKU + doc_id = product.get('sku', '').lower().replace("-", "_").replace(" ", "_") + if not doc_id: + doc_id = product.get('id', 'unknown') + + return { + "id": doc_id, + "product_name": product.get("product_name", ""), + "sku": product.get("sku", ""), + "model": product.get("model", ""), + "category": product.get("category", ""), + "sub_category": product.get("sub_category", ""), + "marketing_description": product.get("marketing_description", ""), + "detailed_spec_description": product.get("detailed_spec_description", ""), + "image_description": product.get("image_description", ""), + "combined_text": combined_text.strip(), + "content_vector": [0.0] * 1536 # Placeholder - would need embedding model + } + + +async def get_products_from_cosmos() -> List[Dict[str, Any]]: + """Fetch all products from CosmosDB.""" + from backend.services.cosmos_service import get_cosmos_service + + cosmos_service = await get_cosmos_service() + products = await cosmos_service.get_all_products() + + return [p.model_dump() for p in products] + + +async def index_products(search_client: SearchClient, products: List[Dict[str, Any]]) -> tuple: + """Index products into the search index.""" + + documents = [] + + for product in products: + doc = prepare_product_document(product) + documents.append(doc) + print(f" ✓ Prepared: {product.get('product_name', 'Unknown')} ({product.get('sku', 'N/A')})") + + # Upload documents to the index + result = search_client.upload_documents(documents) + + succeeded = sum(1 for r in result if r.succeeded) + failed = sum(1 for r in result if not r.succeeded) + + return succeeded, failed + + +def get_search_credential(): + """Get search credential - prefer RBAC, fall back to API key.""" + try: + credential = DefaultAzureCredential() + # Test the credential + test_client = SearchIndexClient(endpoint=SEARCH_ENDPOINT, credential=credential) + list(test_client.list_indexes()) + print("Using RBAC authentication for search") + return credential + except Exception: + if AZURE_SEARCH_ADMIN_KEY: + print("Using API key authentication for search") + return AzureKeyCredential(AZURE_SEARCH_ADMIN_KEY) + raise + + +async def main(): + """Main entry point.""" + print("=" * 60) + print("Index CosmosDB Products into Azure AI Search") + print("=" * 60) + print() + + search_credential = get_search_credential() + + # Create index client + index_client = SearchIndexClient( + endpoint=SEARCH_ENDPOINT, + credential=search_credential + ) + + # Create or update the index + print(f"Creating/updating search index: {SEARCH_INDEX_NAME}") + print(f"Search endpoint: {SEARCH_ENDPOINT}") + print() + + try: + index = create_products_index(index_client) + result = index_client.create_or_update_index(index) + print(f"✓ Index '{result.name}' created/updated successfully") + except Exception as e: + print(f"✗ Failed to create index: {e}") + raise + + # Get products from CosmosDB + print() + print("Fetching products from CosmosDB...") + + try: + products = await get_products_from_cosmos() + print(f"Found {len(products)} products in CosmosDB") + except Exception as e: + print(f"✗ Failed to fetch products from CosmosDB: {e}") + raise + + if not products: + print("No products found in CosmosDB. Run load_sample_data.py first.") + return + + # Index the products + print() + print("Indexing products...") + print("-" * 50) + + search_client = SearchClient( + endpoint=SEARCH_ENDPOINT, + index_name=SEARCH_INDEX_NAME, + credential=search_credential + ) + + try: + succeeded, failed = await index_products(search_client, products) + print("-" * 50) + print(f"\n✓ Indexed {succeeded} products successfully") + if failed > 0: + print(f"✗ Failed to index {failed} products") + except Exception as e: + print(f"✗ Failed to index products: {e}") + raise + + # Summary + print() + print("=" * 60) + print("Product Indexing Complete!") + print("=" * 60) + print(f"Index name: {SEARCH_INDEX_NAME}") + print(f"Endpoint: {SEARCH_ENDPOINT}") + print(f"Documents indexed: {succeeded}") + print() + print("Example search queries:") + print(" - Search by category: 'electronics', 'footwear'") + print(" - Search by product: 'wireless headphones', 'yoga mat'") + print(" - Search by feature: 'noise cancellation', 'titanium'") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/content-gen/scripts/load_sample_data.py b/content-gen/scripts/load_sample_data.py new file mode 100644 index 000000000..fc2b224bd --- /dev/null +++ b/content-gen/scripts/load_sample_data.py @@ -0,0 +1,150 @@ +""" +Sample data loader for Content Generation Solution Accelerator. + +This script loads sample product data into CosmosDB for testing and demos. +""" + +import asyncio +import json +import os +import sys + +# Add the src directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from backend.services.cosmos_service import get_cosmos_service +from backend.models import Product + + +SAMPLE_PRODUCTS = [ + { + "product_name": "ProMax Wireless Headphones", + "category": "Electronics", + "sub_category": "Audio", + "marketing_description": "Immerse yourself in crystal-clear sound with our flagship wireless headphones featuring industry-leading noise cancellation.", + "detailed_spec_description": "40mm custom-designed drivers deliver rich, balanced audio. Advanced Active Noise Cancellation blocks outside noise. 30-hour battery life with quick charge (10 min = 5 hours). Bluetooth 5.2 with multipoint connection. Premium memory foam ear cushions. Foldable design with travel case included.", + "sku": "PM-WH-2024-001", + "model": "ProMax-Elite", + "image_description": "Sleek over-ear headphones in premium matte black finish with rose gold accent rings around the ear cups. Features thick memory foam cushions covered in breathable protein leather. Adjustable stainless steel headband with soft padding. Touch controls visible on the right ear cup." + }, + { + "product_name": "UltraFit Wireless Earbuds", + "category": "Electronics", + "sub_category": "Audio", + "marketing_description": "Stay active with earbuds designed for movement. Secure fit, powerful sound, all-day comfort.", + "detailed_spec_description": "10mm dynamic drivers with deep bass. IPX5 water and sweat resistant. 8 hours playback, 32 hours with case. Touch controls and voice assistant support. Wireless charging compatible. Three sizes of silicone and foam tips included.", + "sku": "UF-EB-2024-002", + "model": "UltraFit-Pro", + "image_description": "Compact true wireless earbuds in pearl white with a sleek charging case. Earbuds feature an ergonomic wingtip design for secure fit. The case has a matte finish with LED indicator lights and USB-C port visible on the bottom." + }, + { + "product_name": "SmartHome Hub Pro", + "category": "Electronics", + "sub_category": "Smart Home", + "marketing_description": "Control your entire smart home from one elegant hub. Voice control, automation, and seamless integration.", + "detailed_spec_description": "7-inch HD touchscreen display. Built-in premium speakers with 360° sound. Works with Alexa, Google Assistant, and HomeKit. Zigbee and Z-Wave built-in. Matter compatible. Privacy shutter for camera. Wall mount or tabletop stand included.", + "sku": "SH-HUB-2024-003", + "model": "SmartHub-7", + "image_description": "Modern smart home hub with a 7-inch display showing a home dashboard. Sleek charcoal gray frame with fabric-covered speaker grille at the bottom. Sits on a minimalist aluminum stand. The screen displays room controls and weather widgets." + }, + { + "product_name": "AeroLight Running Shoes", + "category": "Footwear", + "sub_category": "Athletic", + "marketing_description": "Engineered for speed. Our lightest running shoe ever delivers responsive cushioning and breathable comfort.", + "detailed_spec_description": "Weight: 7.2 oz (men's size 9). Nitrogen-infused foam midsole for 15% more energy return. Engineered mesh upper with targeted ventilation zones. Carbon fiber plate for propulsion. Rubber outsole with multi-directional traction pattern. Reflective elements for visibility.", + "sku": "AL-RUN-2024-004", + "model": "AeroLight-X", + "image_description": "Dynamic running shoe in electric blue with neon green accents. Features a lightweight mesh upper with visible ventilation holes. The midsole shows a distinctive curved carbon plate design. Translucent rubber outsole reveals the foam construction beneath." + }, + { + "product_name": "Executive Leather Briefcase", + "category": "Accessories", + "sub_category": "Bags", + "marketing_description": "Timeless elegance meets modern functionality. Handcrafted from premium full-grain leather.", + "detailed_spec_description": "Full-grain vegetable-tanned leather. Padded compartment fits laptops up to 15.6 inches. Organizer pocket with RFID-blocking liner. YKK zippers with custom-designed pulls. Adjustable, removable shoulder strap. Dimensions: 16\" x 12\" x 4\". Dust bag included.", + "sku": "EL-BRIEF-2024-005", + "model": "Executive-Classic", + "image_description": "Sophisticated brown leather briefcase with rich patina and subtle grain texture. Features brass-toned hardware and buckle closures. Front pocket with a sleek zipper. Top carry handles are reinforced with stitching. The leather shows natural variations indicating genuine premium quality." + }, + { + "product_name": "Ceramic Pour-Over Coffee Maker", + "category": "Home & Kitchen", + "sub_category": "Coffee", + "marketing_description": "Artisan coffee at home. Handmade ceramic dripper delivers a perfectly balanced, flavorful cup every time.", + "detailed_spec_description": "Handcrafted ceramic with food-safe glaze. Unique spiral ridges for optimal extraction. Fits standard #2 paper filters. Serves 1-2 cups. Dishwasher safe. Heat resistant cork sleeve. Includes 30 unbleached paper filters.", + "sku": "CP-COFFEE-2024-006", + "model": "ArtisanBrew", + "image_description": "Elegant ceramic pour-over coffee dripper in creamy white with subtle blue-gray spiral ridges inside. Sits on a natural cork base. The ceramic has a smooth, hand-finished quality with a gentle sheen. A glass carafe is positioned beneath to catch the brew." + }, + { + "product_name": "Performance Yoga Mat", + "category": "Sports & Fitness", + "sub_category": "Yoga", + "marketing_description": "Elevate your practice. Superior grip, cushioning, and eco-friendly materials for yogis who demand the best.", + "detailed_spec_description": "6mm thick natural rubber with microfiber surface. Dimensions: 72\" x 26\". Non-slip grip improves with moisture. Alignment markers laser-etched. Antimicrobial treatment. Free from PVC, latex, and toxic materials. Includes cotton carrying strap.", + "sku": "PY-MAT-2024-007", + "model": "ZenGrip-Pro", + "image_description": "Premium yoga mat rolled partially open, showing a deep forest green surface with subtle alignment lines. The underside visible at the roll shows a natural rubber texture in black. A cotton strap in matching green wraps around the rolled mat." + }, + { + "product_name": "Titanium Travel Watch", + "category": "Accessories", + "sub_category": "Watches", + "marketing_description": "Adventure-ready timekeeping. Lightweight titanium construction with world-class precision.", + "detailed_spec_description": "Grade 2 titanium case, 42mm diameter. Swiss automatic movement with 72-hour power reserve. Sapphire crystal with anti-reflective coating. Water resistant to 200 meters. Dual time zone display. Interchangeable NATO and titanium bracelet included.", + "sku": "TT-WATCH-2024-008", + "model": "Explorer-Ti", + "image_description": "Rugged titanium watch with a brushed silver-gray case showing subtle tool marks from the finishing process. Black dial with luminescent hands and hour markers. A rotating bezel with minute markings surrounds the sapphire crystal. The watch is shown on a gray NATO strap." + }, + { + "product_name": "Organic Cotton Bedding Set", + "category": "Home & Living", + "sub_category": "Bedding", + "marketing_description": "Sleep in sustainable luxury. GOTS-certified organic cotton that gets softer with every wash.", + "detailed_spec_description": "100% GOTS-certified organic cotton, 400 thread count sateen weave. Set includes: 1 duvet cover, 2 pillowcases. Available in Queen and King sizes. Hidden button closure. Oeko-Tex certified for safety. Pre-washed for extra softness. Machine washable.", + "sku": "OC-BED-2024-009", + "model": "PureRest-Sateen", + "image_description": "Luxurious bedding set displayed on a neatly made bed. The soft white cotton has a subtle sheen characteristic of sateen weave. Crisp, clean lines with perfectly tucked corners. The pillowcases feature a simple envelope closure. Natural light highlights the fabric's smooth texture." + }, + { + "product_name": "Smart Fitness Tracker", + "category": "Electronics", + "sub_category": "Wearables", + "marketing_description": "Your health companion. Advanced sensors, beautiful display, and insights that help you live better.", + "detailed_spec_description": "1.4\" AMOLED display, always-on option. Heart rate, SpO2, stress, and sleep monitoring. Built-in GPS and 20+ sport modes. 7-day battery life. 5 ATM water resistance. Contactless payments. Compatible with iOS and Android.", + "sku": "SF-TRACK-2024-010", + "model": "VitalBand-Pro", + "image_description": "Sleek fitness tracker with a curved rectangular display showing colorful health metrics. The case is polished black aluminum with a subtle graphite tone. Attached is a soft silicone band in midnight blue with a secure buckle clasp. The screen displays heart rate, steps, and time." + } +] + + +async def load_sample_data(): + """Load sample products into CosmosDB.""" + print("Loading sample product data...") + + cosmos_service = await get_cosmos_service() + + for product_data in SAMPLE_PRODUCTS: + try: + product = Product(**product_data) + await cosmos_service.upsert_product(product) + print(f" ✓ Loaded: {product.product_name} ({product.sku})") + except Exception as e: + print(f" ✗ Failed to load {product_data.get('product_name', 'unknown')}: {e}") + + print(f"\nLoaded {len(SAMPLE_PRODUCTS)} sample products.") + + +async def main(): + """Main entry point.""" + try: + await load_sample_data() + except Exception as e: + print(f"Error loading sample data: {e}") + raise + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/content-gen/scripts/test_content_generation.py b/content-gen/scripts/test_content_generation.py new file mode 100644 index 000000000..6d24a9dfe --- /dev/null +++ b/content-gen/scripts/test_content_generation.py @@ -0,0 +1,314 @@ +""" +Test script to simulate marketing content creation workflow. + +This script tests the end-to-end content generation flow: +1. Research Agent searches for products and visual styles +2. Planning Agent creates content strategy +3. Text Content Agent generates marketing copy +4. Image Content Agent generates image prompts +5. Compliance Agent validates content + +Run with: python scripts/test_content_generation.py +""" + +import asyncio +import json +import sys +import os +from typing import Dict, Any + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from dotenv import load_dotenv +load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) + + +# Sample creative briefs for testing +SAMPLE_BRIEFS = [ + { + "name": "Holiday Audio Campaign", + "brief": """ +Create a holiday marketing campaign for our premium wireless headphones. + +Campaign Details: +- Target Audience: Tech-savvy professionals, 25-45 years old +- Campaign Theme: "Gift of Sound" - perfect holiday gift for music lovers +- Tone: Warm, festive, but sophisticated +- Channels: Social media (Instagram, Facebook), Email newsletter +- Key Messages: Premium sound quality, noise cancellation, perfect gift + +Deliverables Needed: +1. Social media post (Instagram) with headline and body copy +2. Email subject line and preview text +3. Product hero image concept description +""" + }, + { + "name": "Fitness Product Launch", + "brief": """ +Launch campaign for our new Performance Yoga Mat. + +Campaign Details: +- Target Audience: Health-conscious individuals, yoga enthusiasts, 20-40 years old +- Campaign Theme: "Elevate Your Practice" - premium eco-friendly yoga experience +- Tone: Calm, inspiring, wellness-focused +- Channels: Instagram, Pinterest, Wellness blogs +- Key Messages: Eco-friendly materials, superior grip, alignment markers + +Deliverables Needed: +1. Instagram carousel post copy (3 slides) +2. Pinterest pin description +3. Lifestyle image concept for the mat in use +""" + }, + { + "name": "Luxury Watch Campaign", + "brief": """ +Create a sophisticated campaign for our Titanium Travel Watch. + +Campaign Details: +- Target Audience: Affluent travelers, adventure seekers, 35-55 years old +- Campaign Theme: "Time for Adventure" - precision meets exploration +- Tone: Sophisticated, aspirational, adventurous +- Channels: Print magazine, LinkedIn, Premium digital ads +- Key Messages: Swiss precision, titanium durability, dual timezone + +Deliverables Needed: +1. Magazine ad headline and body copy +2. LinkedIn post for business travelers +3. Hero image concept showing the watch in travel context +""" + } +] + + +async def test_search_service(): + """Test the search service for grounding data.""" + print("\n" + "=" * 60) + print("STEP 1: Testing Search Service (Grounding Data)") + print("=" * 60) + + from backend.services.search_service import get_search_service + + service = await get_search_service() + + # Test product search + print("\n📦 Searching for products: 'wireless audio'") + products = await service.search_products("wireless audio", top=3) + print(f" Found {len(products)} products:") + for p in products: + print(f" • {p['product_name']} ({p['category']})") + print(f" {p['marketing_description'][:80]}...") + + # Test visual style search + print("\n🎨 Searching for visual styles: 'professional modern'") + images = await service.search_images("professional modern", top=3) + print(f" Found {len(images)} visual styles:") + for img in images: + print(f" • {img['name']}: {img['primary_color']} / {img['mood']}") + + # Test combined grounding context + print("\n📋 Getting grounding context for 'holiday gift electronics'") + context = await service.get_grounding_context( + product_query="holiday gift electronics", + image_query="warm festive", + mood="Warm" + ) + print(f" Products: {context['product_count']}, Images: {context['image_count']}") + + return context + + +async def test_research_agent_tools(): + """Test the research agent's tool functions directly.""" + print("\n" + "=" * 60) + print("STEP 2: Testing Research Agent Tools") + print("=" * 60) + + from backend.agents.research_agent import ( + search_products, + search_visual_styles, + get_grounding_context + ) + + # Test search_products tool + print("\n🔍 Testing search_products tool...") + result = await search_products( + query="yoga fitness", + category="Sports & Fitness", + limit=2 + ) + print(f" Query: 'yoga fitness', Category: 'Sports & Fitness'") + print(f" Found: {result['total_count']} products") + for p in result['products']: + print(f" • {p['product_name']}") + + # Test search_visual_styles tool + print("\n🎨 Testing search_visual_styles tool...") + result = await search_visual_styles( + query="calm natural green", + color_family="Nature", + limit=2 + ) + print(f" Query: 'calm natural green', Color Family: 'Nature'") + print(f" Found: {result['total_count']} visual styles") + for v in result['visual_styles']: + print(f" • {v['name']}: {v['style']}") + + # Test get_grounding_context tool + print("\n📦 Testing get_grounding_context tool...") + result = await get_grounding_context( + product_query="luxury accessories", + visual_query="sophisticated elegant", + mood="Elegant" + ) + print(f" Products: {result['product_count']}, Images: {result['image_count']}") + + return result + + +async def simulate_content_generation(brief: Dict[str, Any]): + """Simulate the content generation workflow for a brief.""" + print("\n" + "=" * 60) + print(f"STEP 3: Simulating Content Generation") + print(f"Campaign: {brief['name']}") + print("=" * 60) + + from backend.services.search_service import get_search_service + from backend.settings import app_settings + + # Extract key terms from brief (simplified - real implementation would use AI) + brief_text = brief['brief'].lower() + + # Determine product category + if 'headphones' in brief_text or 'audio' in brief_text: + product_query = "wireless headphones audio" + category = "Electronics" + elif 'yoga' in brief_text or 'fitness' in brief_text: + product_query = "yoga mat fitness" + category = "Sports & Fitness" + elif 'watch' in brief_text: + product_query = "titanium watch travel" + category = "Accessories" + else: + product_query = "product" + category = None + + # Determine visual mood + if 'holiday' in brief_text or 'festive' in brief_text: + visual_query = "warm inviting" + mood = "Warm" + elif 'calm' in brief_text or 'wellness' in brief_text: + visual_query = "calm peaceful natural" + mood = "Tranquil" + elif 'sophisticated' in brief_text or 'luxury' in brief_text: + visual_query = "sophisticated elegant premium" + mood = "Sophisticated" + else: + visual_query = "modern professional" + mood = None + + # Get grounding context + print(f"\n🔍 Researching products: '{product_query}'") + print(f"🎨 Finding visual styles: '{visual_query}'") + + service = await get_search_service() + context = await service.get_grounding_context( + product_query=product_query, + image_query=visual_query, + category=category, + mood=mood + ) + + print(f"\n📊 Grounding Context Retrieved:") + print(f" • {context['product_count']} matching products") + print(f" • {context['image_count']} matching visual styles") + + # Display matched products + if context['products']: + print("\n📦 Matched Products:") + for p in context['products'][:2]: + print(f" • {p['product_name']} ({p['sku']})") + print(f" {p['marketing_description']}") + + # Display matched visual styles + if context['images']: + print("\n🎨 Matched Visual Styles:") + for img in context['images'][:2]: + print(f" • {img['name']}") + print(f" Colors: {img['primary_color']} / {img['secondary_color']}") + print(f" Mood: {img['mood']}, Style: {img['style']}") + + # Simulate content planning + print("\n📝 Content Plan (Simulated):") + print(" Based on the grounding context, the content would include:") + + if context['products']: + product = context['products'][0] + print(f"\n HEADLINE CONCEPT:") + print(f" \"Experience {product['product_name']} - {product['marketing_description'][:50]}...\"") + + print(f"\n KEY MESSAGING POINTS:") + specs = product.get('detailed_spec_description', '')[:200] + print(f" • Product highlights: {specs}...") + + if product.get('image_description'): + print(f"\n IMAGE CONCEPT:") + print(f" Based on product visual: {product['image_description'][:150]}...") + + if context['images']: + style = context['images'][0] + print(f"\n VISUAL DIRECTION:") + print(f" • Color palette: {style['primary_color']} with {style['secondary_color']} accents") + print(f" • Mood: {style['mood']}") + print(f" • Style: {style['style']}") + print(f" • Best for: {style.get('use_cases', 'General marketing')[:100]}") + + # Show brand compliance context + print("\n✅ Brand Compliance Check:") + print(f" • Tone: {app_settings.brand_guidelines.tone}") + print(f" • Max headline: {app_settings.brand_guidelines.max_headline_length} chars") + print(f" • Prohibited words: {', '.join(app_settings.brand_guidelines.prohibited_words[:3])}...") + + return context + + +async def run_full_test(): + """Run the complete test suite.""" + print("\n" + "=" * 70) + print(" MARKETING CONTENT GENERATION - TEST SIMULATION") + print("=" * 70) + + try: + # Step 1: Test search service + await test_search_service() + + # Step 2: Test research agent tools + await test_research_agent_tools() + + # Step 3: Simulate content generation for each brief + for brief in SAMPLE_BRIEFS: + await simulate_content_generation(brief) + print("\n" + "-" * 60) + + print("\n" + "=" * 70) + print(" ✅ ALL TESTS COMPLETED SUCCESSFULLY") + print("=" * 70) + print("\nThe content generation system is configured and ready.") + print("Products and visual styles are being retrieved from Azure AI Search.") + print("\nNext steps:") + print(" 1. Start the backend: python app.py") + print(" 2. Start the frontend: npm run dev") + print(" 3. Submit a creative brief through the chat interface") + print("=" * 70 + "\n") + + except Exception as e: + print(f"\n❌ Test failed: {e}") + import traceback + traceback.print_exc() + raise + + +if __name__ == "__main__": + asyncio.run(run_full_test()) diff --git a/content-gen/scripts/upload_images.py b/content-gen/scripts/upload_images.py new file mode 100644 index 000000000..ddb371aae --- /dev/null +++ b/content-gen/scripts/upload_images.py @@ -0,0 +1,138 @@ +""" +Image Upload Script for Content Generation Solution Accelerator. + +This script uploads JPG images from the local images folder to Azure Blob Storage. +Uses DefaultAzureCredential for authentication (RBAC). +""" + +import asyncio +import os +import sys +from pathlib import Path + +from azure.identity import DefaultAzureCredential +from azure.storage.blob.aio import BlobServiceClient +from azure.storage.blob import ContentSettings +from dotenv import load_dotenv + +# Load environment variables +env_path = Path(__file__).parent.parent / ".env" +load_dotenv(env_path) + +# Configuration +STORAGE_ACCOUNT_NAME = os.getenv("AZURE_BLOB_ACCOUNT_NAME", "storagecontentgenjh") +CONTAINER_NAME = os.getenv("AZURE_BLOB_PRODUCT_IMAGES_CONTAINER", "product-images") +IMAGES_FOLDER = Path(__file__).parent / "images" + + +async def upload_images(): + """Upload all JPG images from the images folder to Azure Blob Storage.""" + + # Build the blob service URL + account_url = f"https://{STORAGE_ACCOUNT_NAME}.blob.core.windows.net" + + print(f"Connecting to storage account: {STORAGE_ACCOUNT_NAME}") + print(f"Target container: {CONTAINER_NAME}") + print(f"Images folder: {IMAGES_FOLDER}") + print() + + # Create credential and blob service client + credential = DefaultAzureCredential() + + async with BlobServiceClient(account_url=account_url, credential=credential) as blob_service: + # Get or create container + container_client = blob_service.get_container_client(CONTAINER_NAME) + + try: + await container_client.create_container() + print(f"Created container: {CONTAINER_NAME}") + except Exception as e: + if "ContainerAlreadyExists" in str(e): + print(f"Container already exists: {CONTAINER_NAME}") + else: + print(f"Note: {e}") + + # Find all JPG files + jpg_files = list(IMAGES_FOLDER.glob("*.jpg")) + list(IMAGES_FOLDER.glob("*.JPG")) + + if not jpg_files: + print("No JPG files found in the images folder.") + return [] + + print(f"\nFound {len(jpg_files)} JPG files to upload:") + print("-" * 50) + + uploaded_files = [] + + for jpg_path in sorted(jpg_files): + blob_name = jpg_path.name + blob_client = container_client.get_blob_client(blob_name) + + try: + # Read and upload the image + with open(jpg_path, "rb") as image_file: + image_data = image_file.read() + + await blob_client.upload_blob( + image_data, + overwrite=True, + content_settings=ContentSettings(content_type="image/jpeg") + ) + + blob_url = f"{account_url}/{CONTAINER_NAME}/{blob_name}" + uploaded_files.append({ + "name": blob_name, + "url": blob_url, + "size": len(image_data) + }) + + print(f" ✓ Uploaded: {blob_name} ({len(image_data):,} bytes)") + + except Exception as e: + print(f" ✗ Failed to upload {blob_name}: {e}") + + print("-" * 50) + print(f"\nSuccessfully uploaded {len(uploaded_files)} images.") + + return uploaded_files + + +async def list_uploaded_images(): + """List all images in the blob container.""" + + account_url = f"https://{STORAGE_ACCOUNT_NAME}.blob.core.windows.net" + credential = DefaultAzureCredential() + + async with BlobServiceClient(account_url=account_url, credential=credential) as blob_service: + container_client = blob_service.get_container_client(CONTAINER_NAME) + + print(f"\nImages in container '{CONTAINER_NAME}':") + print("-" * 50) + + async for blob in container_client.list_blobs(): + print(f" - {blob.name} ({blob.size:,} bytes)") + + +async def main(): + """Main entry point.""" + try: + # Upload images + uploaded = await upload_images() + + if uploaded: + # List what's in the container + await list_uploaded_images() + + print("\n" + "=" * 50) + print("Image URLs for use in search index:") + print("=" * 50) + for img in uploaded: + print(f" {img['url']}") + + except Exception as e: + print(f"Error: {e}") + raise + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/content-gen/src/Dockerfile b/content-gen/src/Dockerfile new file mode 100644 index 000000000..affcf6e9c --- /dev/null +++ b/content-gen/src/Dockerfile @@ -0,0 +1,31 @@ +# Content Generation Solution Accelerator - Docker Image + +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for layer caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Set environment variables +ENV PYTHONPATH=/app +ENV PORT=5000 + +# Expose port +EXPOSE 5000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5000/health || exit 1 + +# Run the application +CMD ["hypercorn", "app:app", "--config", "hypercorn.conf.py"] diff --git a/content-gen/src/app.py b/content-gen/src/app.py new file mode 100644 index 000000000..b9c7567ea --- /dev/null +++ b/content-gen/src/app.py @@ -0,0 +1,534 @@ +""" +Content Generation Solution Accelerator - Main Application Entry Point. + +This is the main Quart application that provides the REST API for the +Intelligent Content Generation Accelerator. +""" + +import json +import logging +import os +import uuid +from datetime import datetime, timezone + +from quart import Quart, request, jsonify, Response +from quart_cors import cors + +from backend.settings import app_settings +from backend.models import CreativeBrief, Product +from backend.orchestrator import get_orchestrator +from backend.services.cosmos_service import get_cosmos_service +from backend.services.blob_service import get_blob_service + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# Create Quart app +app = Quart(__name__) +app = cors(app, allow_origin="*") + + +# ==================== Health Check ==================== + +@app.route("/health", methods=["GET"]) +async def health_check(): + """Health check endpoint.""" + return jsonify({ + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "version": "1.0.0" + }) + + +# ==================== Chat Endpoints ==================== + +@app.route("/api/chat", methods=["POST"]) +async def chat(): + """ + Process a chat message through the agent orchestration. + + Request body: + { + "message": "User's message", + "conversation_id": "optional-uuid", + "user_id": "user identifier" + } + + Returns streaming response with agent responses. + """ + data = await request.get_json() + + message = data.get("message", "") + conversation_id = data.get("conversation_id") or str(uuid.uuid4()) + user_id = data.get("user_id", "anonymous") + + if not message: + return jsonify({"error": "Message is required"}), 400 + + orchestrator = get_orchestrator() + + # Try to save to CosmosDB but don't fail if it's unavailable + try: + cosmos_service = await get_cosmos_service() + await cosmos_service.add_message_to_conversation( + conversation_id=conversation_id, + user_id=user_id, + message={ + "role": "user", + "content": message, + "timestamp": datetime.now(timezone.utc).isoformat() + } + ) + except Exception as e: + logger.warning(f"Failed to save message to CosmosDB: {e}") + + async def generate(): + """Stream responses from the orchestrator.""" + try: + async for response in orchestrator.process_message( + message=message, + conversation_id=conversation_id + ): + yield f"data: {json.dumps(response)}\n\n" + + # Try to save assistant responses + if response.get("is_final"): + try: + cosmos_service = await get_cosmos_service() + await cosmos_service.add_message_to_conversation( + conversation_id=conversation_id, + user_id=user_id, + message={ + "role": "assistant", + "content": response.get("content", ""), + "agent": response.get("agent", ""), + "timestamp": datetime.now(timezone.utc).isoformat() + } + ) + except Exception as e: + logger.warning(f"Failed to save response to CosmosDB: {e}") + except Exception as e: + logger.exception(f"Error in orchestrator: {e}") + yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n" + + yield "data: [DONE]\n\n" + + return Response( + generate(), + mimetype="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no" + } + ) + + +# ==================== Creative Brief Endpoints ==================== + +@app.route("/api/brief/parse", methods=["POST"]) +async def parse_brief(): + """ + Parse a free-text creative brief into structured format. + + Request body: + { + "brief_text": "Free-form creative brief text" + } + + Returns: + Structured CreativeBrief JSON for user confirmation. + """ + data = await request.get_json() + brief_text = data.get("brief_text", "") + + if not brief_text: + return jsonify({"error": "Brief text is required"}), 400 + + orchestrator = get_orchestrator() + parsed_brief = await orchestrator.parse_brief(brief_text) + + return jsonify({ + "brief": parsed_brief.model_dump(), + "requires_confirmation": True, + "message": "Please review and confirm the parsed creative brief" + }) + + +@app.route("/api/brief/confirm", methods=["POST"]) +async def confirm_brief(): + """ + Confirm or modify a parsed creative brief. + + Request body: + { + "brief": { ... CreativeBrief fields ... }, + "conversation_id": "optional-uuid", + "user_id": "user identifier" + } + + Returns: + Confirmation status and next steps. + """ + data = await request.get_json() + brief_data = data.get("brief", {}) + conversation_id = data.get("conversation_id") or str(uuid.uuid4()) + user_id = data.get("user_id", "anonymous") + + try: + brief = CreativeBrief(**brief_data) + except Exception as e: + return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400 + + # Try to save the confirmed brief to CosmosDB, but don't fail if unavailable + try: + cosmos_service = await get_cosmos_service() + await cosmos_service.save_conversation( + conversation_id=conversation_id, + user_id=user_id, + messages=[], + brief=brief, + metadata={"status": "brief_confirmed"} + ) + except Exception as e: + logger.warning(f"Failed to save brief to CosmosDB: {e}") + + return jsonify({ + "status": "confirmed", + "conversation_id": conversation_id, + "brief": brief.model_dump(), + "message": "Brief confirmed. Ready for content generation." + }) + + +# ==================== Content Generation Endpoints ==================== + +@app.route("/api/generate", methods=["POST"]) +async def generate_content(): + """ + Generate content from a confirmed creative brief. + + Request body: + { + "brief": { ... CreativeBrief fields ... }, + "products": [ ... Product list (optional) ... ], + "generate_images": true/false, + "conversation_id": "uuid" + } + + Returns streaming response with generated content. + """ + data = await request.get_json() + + brief_data = data.get("brief", {}) + products_data = data.get("products", []) + generate_images = data.get("generate_images", True) + conversation_id = data.get("conversation_id") or str(uuid.uuid4()) + + try: + brief = CreativeBrief(**brief_data) + except Exception as e: + return jsonify({"error": f"Invalid brief format: {str(e)}"}), 400 + + orchestrator = get_orchestrator() + + async def generate(): + """Stream content generation responses.""" + try: + response = await orchestrator.generate_content( + brief=brief, + products=products_data, + generate_images=generate_images + ) + + # Try to save generated images to blob storage + try: + blob_service = await get_blob_service() + if response.get("image_base64"): + image_url = await blob_service.save_generated_image( + conversation_id=conversation_id, + image_base64=response["image_base64"] + ) + response["image_url"] = image_url + except Exception as e: + logger.warning(f"Failed to save image to blob storage: {e}") + + # Format response to match what frontend expects + yield f"data: {json.dumps({'type': 'agent_response', 'content': json.dumps(response), 'is_final': True})}\n\n" + except Exception as e: + logger.exception(f"Error generating content: {e}") + yield f"data: {json.dumps({'type': 'error', 'content': str(e), 'is_final': True})}\n\n" + + yield "data: [DONE]\n\n" + + return Response( + generate(), + mimetype="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no" + } + ) + + +# ==================== Product Endpoints ==================== + +@app.route("/api/products", methods=["GET"]) +async def list_products(): + """ + List all products. + + Query params: + category: Filter by category + sub_category: Filter by sub-category + search: Search term + limit: Max number of results (default 20) + """ + category = request.args.get("category") + sub_category = request.args.get("sub_category") + search = request.args.get("search") + limit = int(request.args.get("limit", 20)) + + cosmos_service = await get_cosmos_service() + + if search: + products = await cosmos_service.search_products(search, limit) + elif category: + products = await cosmos_service.get_products_by_category( + category, sub_category, limit + ) + else: + products = await cosmos_service.get_all_products(limit) + + return jsonify({ + "products": [p.model_dump() for p in products], + "count": len(products) + }) + + +@app.route("/api/products/", methods=["GET"]) +async def get_product(sku: str): + """Get a product by SKU.""" + cosmos_service = await get_cosmos_service() + product = await cosmos_service.get_product_by_sku(sku) + + if not product: + return jsonify({"error": "Product not found"}), 404 + + return jsonify(product.model_dump()) + + +@app.route("/api/products", methods=["POST"]) +async def create_product(): + """ + Create or update a product. + + Request body: + { + "product_name": "...", + "category": "...", + "sub_category": "...", + "marketing_description": "...", + "detailed_spec_description": "...", + "sku": "...", + "model": "..." + } + """ + data = await request.get_json() + + try: + product = Product(**data) + except Exception as e: + return jsonify({"error": f"Invalid product format: {str(e)}"}), 400 + + cosmos_service = await get_cosmos_service() + saved_product = await cosmos_service.upsert_product(product) + + return jsonify(saved_product.model_dump()), 201 + + +@app.route("/api/products//image", methods=["POST"]) +async def upload_product_image(sku: str): + """ + Upload an image for a product. + + The image will be stored and a description will be auto-generated + using GPT-5 Vision. + + Request: multipart/form-data with 'image' file + """ + cosmos_service = await get_cosmos_service() + product = await cosmos_service.get_product_by_sku(sku) + + if not product: + return jsonify({"error": "Product not found"}), 404 + + files = await request.files + if "image" not in files: + return jsonify({"error": "No image file provided"}), 400 + + image_file = files["image"] + image_data = image_file.read() + content_type = image_file.content_type or "image/jpeg" + + blob_service = await get_blob_service() + image_url, description = await blob_service.upload_product_image( + sku=sku, + image_data=image_data, + content_type=content_type + ) + + # Update product with image info + product.image_url = image_url + product.image_description = description + await cosmos_service.upsert_product(product) + + return jsonify({ + "image_url": image_url, + "image_description": description, + "message": "Image uploaded and description generated" + }) + + +# ==================== Conversation Endpoints ==================== + +@app.route("/api/conversations", methods=["GET"]) +async def list_conversations(): + """ + List conversations for a user. + + Query params: + user_id: User identifier (required) + limit: Max number of results (default 20) + """ + user_id = request.args.get("user_id") + limit = int(request.args.get("limit", 20)) + + if not user_id: + return jsonify({"error": "user_id is required"}), 400 + + cosmos_service = await get_cosmos_service() + conversations = await cosmos_service.get_user_conversations(user_id, limit) + + return jsonify({ + "conversations": conversations, + "count": len(conversations) + }) + + +@app.route("/api/conversations/", methods=["GET"]) +async def get_conversation(conversation_id: str): + """ + Get a specific conversation. + + Query params: + user_id: User identifier (required) + """ + user_id = request.args.get("user_id") + + if not user_id: + return jsonify({"error": "user_id is required"}), 400 + + cosmos_service = await get_cosmos_service() + conversation = await cosmos_service.get_conversation(conversation_id, user_id) + + if not conversation: + return jsonify({"error": "Conversation not found"}), 404 + + return jsonify(conversation) + + +# ==================== Brand Guidelines Endpoints ==================== + +@app.route("/api/brand-guidelines", methods=["GET"]) +async def get_brand_guidelines(): + """Get current brand guidelines configuration.""" + return jsonify({ + "tone": app_settings.brand_guidelines.tone, + "voice": app_settings.brand_guidelines.voice, + "primary_color": app_settings.brand_guidelines.primary_color, + "secondary_color": app_settings.brand_guidelines.secondary_color, + "prohibited_words": app_settings.brand_guidelines.prohibited_words, + "required_disclosures": app_settings.brand_guidelines.required_disclosures, + "max_headline_length": app_settings.brand_guidelines.max_headline_length, + "max_body_length": app_settings.brand_guidelines.max_body_length, + "require_cta": app_settings.brand_guidelines.require_cta + }) + + +# ==================== UI Configuration ==================== + +@app.route("/api/config", methods=["GET"]) +async def get_ui_config(): + """Get UI configuration.""" + return jsonify({ + "app_name": app_settings.ui.app_name, + "show_brand_guidelines": True, + "enable_image_generation": True, + "enable_compliance_check": True, + "max_file_size_mb": 10 + }) + + +# ==================== Application Lifecycle ==================== + +@app.before_serving +async def startup(): + """Initialize services on application startup.""" + logger.info("Starting Content Generation Solution Accelerator...") + + # Initialize orchestrator + orchestrator = get_orchestrator() + logger.info("Orchestrator initialized with Microsoft Agent Framework") + + # Try to initialize services - they may fail if CosmosDB/Blob storage is not accessible + try: + await get_cosmos_service() + logger.info("CosmosDB service initialized") + except Exception as e: + logger.warning(f"CosmosDB service initialization failed (may be firewall): {e}") + + try: + await get_blob_service() + logger.info("Blob storage service initialized") + except Exception as e: + logger.warning(f"Blob storage service initialization failed: {e}") + + logger.info("Application startup complete") + + +@app.after_serving +async def shutdown(): + """Cleanup on application shutdown.""" + logger.info("Shutting down Content Generation Solution Accelerator...") + + cosmos_service = await get_cosmos_service() + await cosmos_service.close() + + blob_service = await get_blob_service() + await blob_service.close() + + logger.info("Application shutdown complete") + + +# ==================== Error Handlers ==================== + +@app.errorhandler(404) +async def not_found(error): + """Handle 404 errors.""" + return jsonify({"error": "Not found"}), 404 + + +@app.errorhandler(500) +async def server_error(error): + """Handle 500 errors.""" + logger.exception(f"Server error: {error}") + return jsonify({"error": "Internal server error"}), 500 + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host="0.0.0.0", port=port, debug=True) diff --git a/content-gen/src/backend/__init__.py b/content-gen/src/backend/__init__.py new file mode 100644 index 000000000..968f12ad5 --- /dev/null +++ b/content-gen/src/backend/__init__.py @@ -0,0 +1,30 @@ +""" +Backend package for Content Generation Solution Accelerator. + +This package contains: +- models: Data models (CreativeBrief, Product, ComplianceViolation, etc.) +- settings: Application configuration and brand guidelines +- agents: Specialized AI agents for content generation +- services: CosmosDB and Blob Storage services +- orchestrator: HandoffBuilder-based multi-agent orchestration +""" + +from backend.models import ( + CreativeBrief, + Product, + ComplianceViolation, + ComplianceSeverity, + ContentGenerationResponse, + ComplianceResult, +) +from backend.settings import app_settings + +__all__ = [ + "CreativeBrief", + "Product", + "ComplianceViolation", + "ComplianceSeverity", + "ContentGenerationResponse", + "ComplianceResult", + "app_settings", +] diff --git a/content-gen/src/backend/agents/__init__.py b/content-gen/src/backend/agents/__init__.py new file mode 100644 index 000000000..f73fa8d6b --- /dev/null +++ b/content-gen/src/backend/agents/__init__.py @@ -0,0 +1,7 @@ +"""Agents package for Content Generation Solution Accelerator.""" + +from backend.agents.simple_agent import SimpleAgent + +__all__ = [ + "SimpleAgent", +] diff --git a/content-gen/src/backend/agents/base_agent.py b/content-gen/src/backend/agents/base_agent.py new file mode 100644 index 000000000..78fe7a726 --- /dev/null +++ b/content-gen/src/backend/agents/base_agent.py @@ -0,0 +1,95 @@ +""" +Base Agent Factory for the Content Generation Accelerator. + +Provides a singleton pattern for agent creation with async lock +to ensure thread-safe initialization. + +Uses Azure AI Agents SDK for agent orchestration. +""" + +import asyncio +import logging +from abc import ABC, abstractmethod +from typing import Any, Optional + +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.ai.projects import AIProjectClient +from azure.ai.agents.models import Agent + +from backend.settings import app_settings + +logger = logging.getLogger(__name__) + + +def get_azure_credential(client_id: Optional[str] = None): + """Get Azure credential for operations.""" + if client_id: + return ManagedIdentityCredential(client_id=client_id) + return DefaultAzureCredential() + + +class BaseAgentFactory(ABC): + """ + Abstract base factory for creating and managing agent instances. + + Uses singleton pattern with async lock to ensure only one instance + of each agent type exists per application lifecycle. + """ + + _lock: asyncio.Lock = asyncio.Lock() + _agent: Optional[Agent] = None + _project_client: Optional[AIProjectClient] = None + + @classmethod + def get_project_client(cls) -> AIProjectClient: + """Get or create the Azure AI Project client.""" + if cls._project_client is None: + credential = get_azure_credential( + client_id=app_settings.base_settings.azure_client_id + ) + # Use the agent endpoint if configured, otherwise use OpenAI endpoint + endpoint = app_settings.azure_ai.agent_endpoint or app_settings.azure_openai.endpoint + if endpoint is None: + raise ValueError("No agent endpoint or OpenAI endpoint configured") + cls._project_client = AIProjectClient( + credential=credential, + endpoint=endpoint, + ) + return cls._project_client + + @classmethod + @abstractmethod + async def create_agent(cls) -> Agent: + """Create the specific agent instance. Must be implemented by subclasses.""" + pass + + @classmethod + @abstractmethod + def get_agent_name(cls) -> str: + """Return the agent's name. Must be implemented by subclasses.""" + pass + + @classmethod + @abstractmethod + def get_agent_instructions(cls) -> str: + """Return the agent's system instructions. Must be implemented by subclasses.""" + pass + + @classmethod + async def get_agent(cls) -> Agent: + """Get or create the singleton agent instance.""" + async with cls._lock: + if cls._agent is None: + logger.info(f"Creating {cls.get_agent_name()} agent...") + cls._agent = await cls.create_agent() + logger.info(f"{cls.get_agent_name()} agent created successfully") + return cls._agent + + @classmethod + async def delete_agent(cls) -> None: + """Clean up the agent instance.""" + async with cls._lock: + if cls._agent is not None: + logger.info(f"Deleting {cls.get_agent_name()} agent...") + cls._agent = None + cls._project_client = None diff --git a/content-gen/src/backend/agents/compliance_agent.py b/content-gen/src/backend/agents/compliance_agent.py new file mode 100644 index 000000000..5e0d8b375 --- /dev/null +++ b/content-gen/src/backend/agents/compliance_agent.py @@ -0,0 +1,236 @@ +""" +Compliance Agent - Validates all generated content against brand guidelines. + +Responsibilities: +- Final validation of text and image content +- Categorize violations by severity (error, warning, info) +- Provide specific corrections for violations +- Ensure content meets legal and regulatory requirements +""" + +from typing import Any, List, Optional + +from backend.models import ComplianceSeverity +from backend.settings import app_settings + + +def comprehensive_compliance_check( + headline: str = "", + body: str = "", + cta_text: str = "", + image_prompt: str = "", + image_alt_text: str = "" +) -> dict: + """ + Perform comprehensive compliance check on all generated content. + + Args: + headline: Generated headline text + body: Generated body copy + cta_text: Generated call-to-action text + image_prompt: Prompt used for image generation + image_alt_text: Alt text for generated image + + Returns: + Dictionary containing all violations categorized by severity + """ + violations = [] + brand = app_settings.brand_guidelines + + all_text = f"{headline} {body} {cta_text} {image_alt_text}".lower() + + # === ERROR LEVEL: Legal/Regulatory (Must Fix) === + + # Prohibited words + for word in brand.prohibited_words: + if word.lower() in all_text: + field = "headline" if word.lower() in headline.lower() else \ + "body" if word.lower() in body.lower() else \ + "cta" if word.lower() in cta_text.lower() else "content" + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Prohibited word '{word}' found in {field}", + "suggestion": f"Replace '{word}' with an alternative term", + "field": field + }) + + # Unsubstantiated claims + claim_patterns = [ + ("#1", "numerical ranking claim"), + ("best in class", "superlative claim"), + ("guaranteed", "guarantee claim"), + ("100%", "absolute claim"), + ("always", "absolute claim"), + ("never", "absolute claim"), + ("market leader", "leadership claim"), + ("industry leader", "leadership claim"), + ] + for pattern, claim_type in claim_patterns: + if pattern.lower() in all_text: + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Unsubstantiated {claim_type}: '{pattern}'", + "suggestion": "Remove claim or add supporting citation/disclaimer", + "field": "content" + }) + + # Missing required disclosures + for disclosure in brand.required_disclosures: + if disclosure.lower() not in all_text: + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Required disclosure missing: '{disclosure}'", + "suggestion": f"Add '{disclosure}' to the body copy or as a footnote", + "field": "body" + }) + + # === WARNING LEVEL: Brand Guidelines (Review Recommended) === + + # Length limits + if headline and len(headline) > brand.max_headline_length: + violations.append({ + "severity": ComplianceSeverity.WARNING.value, + "message": f"Headline too long: {len(headline)}/{brand.max_headline_length} characters", + "suggestion": "Shorten headline while maintaining key message", + "field": "headline" + }) + + if body and len(body) > brand.max_body_length: + violations.append({ + "severity": ComplianceSeverity.WARNING.value, + "message": f"Body copy too long: {len(body)}/{brand.max_body_length} characters", + "suggestion": "Condense body copy to be more concise", + "field": "body" + }) + + # CTA requirement + if brand.require_cta and not cta_text: + violations.append({ + "severity": ComplianceSeverity.WARNING.value, + "message": "No call-to-action provided", + "suggestion": "Add a clear CTA such as 'Shop Now', 'Learn More', etc.", + "field": "cta" + }) + + # Image prompt checks + if image_prompt: + prohibited_image_terms = ["competitor", "violence", "inappropriate"] + for term in prohibited_image_terms: + if term in image_prompt.lower(): + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Image prompt contains prohibited term: '{term}'", + "suggestion": f"Remove '{term}' from image generation prompt", + "field": "image" + }) + + # === INFO LEVEL: Style Suggestions (Optional) === + + # Engagement suggestions + if body and "?" not in body and "!" not in body: + violations.append({ + "severity": ComplianceSeverity.INFO.value, + "message": "Body copy lacks engaging punctuation", + "suggestion": "Consider adding questions or exclamations to increase engagement", + "field": "body" + }) + + # Alt text accessibility + if not image_alt_text and image_prompt: + violations.append({ + "severity": ComplianceSeverity.INFO.value, + "message": "Image alt text not provided", + "suggestion": "Add descriptive alt text for accessibility", + "field": "image" + }) + + has_errors = any(v["severity"] == ComplianceSeverity.ERROR.value for v in violations) + has_warnings = any(v["severity"] == ComplianceSeverity.WARNING.value for v in violations) + + return { + "is_valid": not has_errors, + "has_errors": has_errors, + "has_warnings": has_warnings, + "error_count": sum(1 for v in violations if v["severity"] == ComplianceSeverity.ERROR.value), + "warning_count": sum(1 for v in violations if v["severity"] == ComplianceSeverity.WARNING.value), + "info_count": sum(1 for v in violations if v["severity"] == ComplianceSeverity.INFO.value), + "violations": violations, + "summary": f"{sum(1 for v in violations if v['severity'] == 'error')} errors, " + f"{sum(1 for v in violations if v['severity'] == 'warning')} warnings, " + f"{sum(1 for v in violations if v['severity'] == 'info')} suggestions" + } + + +def get_compliance_agent_instructions() -> str: + """Get the Compliance agent instructions.""" + return f"""You are the Compliance Agent, responsible for final validation of all marketing content. + +## Your Role +1. Validate all generated text and image content +2. Identify and categorize compliance violations by severity +3. Provide specific, actionable corrections +4. Ensure content meets legal, regulatory, and brand requirements + +## Compliance Severity Levels + +### ERROR (Red) - Must Fix Before Use +- Legal/regulatory violations +- Unsubstantiated claims +- Prohibited words or phrases +- Missing required disclosures +- Content that could cause legal liability + +### WARNING (Yellow) - Review Recommended +- Brand guideline deviations +- Length limit exceedances +- Missing recommended elements (e.g., CTA) +- Tone inconsistencies + +### INFO (Blue) - Optional Improvements +- Style suggestions +- Engagement enhancements +- Accessibility improvements +- Best practice recommendations + +## Response Format +Always respond with a structured validation report: + +```json +{{ + "validation_result": {{ + "is_valid": true/false, + "can_publish": true/false, + "summary": "X errors, Y warnings, Z suggestions" + }}, + "violations": [ + {{ + "severity": "error|warning|info", + "message": "Clear description of the issue", + "suggestion": "Specific actionable fix", + "field": "Which content field" + }} + ], + "corrected_content": {{ + "headline": "Corrected headline (if errors existed)", + "body": "Corrected body (if errors existed)", + "cta_text": "Corrected CTA (if errors existed)" + }}, + "approval_status": "BLOCKED|REVIEW_RECOMMENDED|APPROVED" +}} +``` + +## Approval Status +- **BLOCKED**: Has ERROR-level violations - cannot be used until fixed +- **REVIEW_RECOMMENDED**: Has WARNING-level violations - should be reviewed +- **APPROVED**: No errors or warnings - ready for use + +## Brand Compliance Rules +{app_settings.brand_guidelines.get_compliance_prompt()} + +## Guidelines +1. Be thorough but practical - flag real issues, not nitpicks +2. Provide specific corrections, not vague suggestions +3. Prioritize legal/regulatory issues over style preferences +4. Consider the target audience and deliverable context +5. For BLOCKED content, always provide corrected versions +""" \ No newline at end of file diff --git a/content-gen/src/backend/agents/image_content_agent.py b/content-gen/src/backend/agents/image_content_agent.py new file mode 100644 index 000000000..fa6c7ead2 --- /dev/null +++ b/content-gen/src/backend/agents/image_content_agent.py @@ -0,0 +1,249 @@ +""" +Image Content Agent - Generates marketing images via DALL-E 3. + +Responsibilities: +- Create marketing images using DALL-E 3 based on creative brief +- Incorporate product descriptions as context (workaround for image seeding) +- Apply brand visual guidelines +- Validate generated images for compliance +""" + +import base64 +import logging +from typing import Any, Optional + +from agent_framework import ChatAgent +from openai import AsyncAzureOpenAI +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential + +from backend.agents.base_agent import BaseAgentFactory +from backend.models import ComplianceSeverity +from backend.settings import app_settings + +logger = logging.getLogger(__name__) + + +async def generate_dalle_image( + prompt: str, + product_description: str = "", + scene_description: str = "", + size: str = "1024x1024", + quality: str = "hd" +) -> dict: + """ + Generate a marketing image using DALL-E 3. + + Args: + prompt: The main image generation prompt + product_description: Auto-generated description of product image (for context) + scene_description: Scene/setting description from creative brief + size: Image size (1024x1024, 1024x1792, 1792x1024) + quality: Image quality (standard, hd) + + Returns: + Dictionary containing generated image data and metadata + """ + brand = app_settings.brand_guidelines + + # Build the full prompt with product context and brand guidelines + full_prompt = f""" +Create a professional marketing image. + +{brand.get_image_generation_prompt()} + +PRODUCT CONTEXT: +{product_description if product_description else 'No specific product - create a lifestyle/brand image'} + +SCENE: +{scene_description if scene_description else prompt} + +MAIN REQUIREMENT: +{prompt} + +IMPORTANT: +- Create a polished, professional marketing image +- Suitable for retail advertising +- High visual impact +""" + + try: + # Get credential + client_id = app_settings.base_settings.azure_client_id + if client_id: + credential = ManagedIdentityCredential(client_id=client_id) + else: + credential = DefaultAzureCredential() + + # Get token for Azure OpenAI + token = await credential.get_token("https://cognitiveservices.azure.com/.default") + + # Use the dedicated DALL-E endpoint if configured, otherwise fall back to main endpoint + dalle_endpoint = app_settings.azure_openai.dalle_endpoint or app_settings.azure_openai.endpoint + logger.info(f"Using DALL-E endpoint: {dalle_endpoint}") + + client = AsyncAzureOpenAI( + azure_endpoint=dalle_endpoint, + azure_ad_token=token.token, + api_version=app_settings.azure_openai.preview_api_version, + ) + + response = await client.images.generate( + model=app_settings.azure_openai.dalle_model, + prompt=full_prompt, + size=size, + quality=quality, + n=1, + response_format="b64_json" + ) + + image_data = response.data[0] + + return { + "success": True, + "image_base64": image_data.b64_json, + "prompt_used": full_prompt, + "revised_prompt": getattr(image_data, 'revised_prompt', None), + } + + except Exception as e: + logger.exception(f"Error generating DALL-E image: {e}") + return { + "success": False, + "error": str(e), + "prompt_used": full_prompt + } + + +def validate_image_prompt(prompt: str) -> dict: + """ + Validate an image generation prompt against brand guidelines. + + Args: + prompt: The image generation prompt to validate + + Returns: + Dictionary containing validation results + """ + violations = [] + brand = app_settings.brand_guidelines + + # Check for prohibited content (ERROR level) + prohibited_terms = ["competitor", "violence", "inappropriate", "offensive"] + for term in prohibited_terms: + if term.lower() in prompt.lower(): + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Prompt may generate prohibited content: '{term}'", + "suggestion": f"Remove reference to '{term}' from the prompt", + "field": "image_prompt" + }) + + # Check brand color mentions (INFO level) + if brand.primary_color.lower() not in prompt.lower() and "brand color" not in prompt.lower(): + violations.append({ + "severity": ComplianceSeverity.INFO.value, + "message": "Consider mentioning brand colors for consistency", + "suggestion": f"Include reference to brand primary color {brand.primary_color}", + "field": "image_prompt" + }) + + has_errors = any(v["severity"] == ComplianceSeverity.ERROR.value for v in violations) + + return { + "is_valid": not has_errors, + "violations": violations, + "prompt": prompt + } + + +class ImageContentAgentFactory(BaseAgentFactory): + """Factory for creating the Image Content generation agent.""" + + @classmethod + def get_agent_name(cls) -> str: + return "ImageContentAgent" + + @classmethod + def get_agent_instructions(cls) -> str: + return f"""You are the Image Content Agent, responsible for generating marketing images using DALL-E 3. + +## Your Role +1. Create compelling marketing images based on creative briefs +2. Incorporate product context for accurate representation +3. Apply brand visual guidelines +4. Validate prompts before generation + +## IMPORTANT: DALL-E 3 Limitation +DALL-E 3 only accepts text prompts - it cannot directly use product images as input. +To work around this, use the product's `image_description` field (auto-generated via GPT-5 Vision) +as detailed text context in your prompts. + +## Available Tools +- **generate_dalle_image**: Generate an image using DALL-E 3 +- **validate_image_prompt**: Check prompt for compliance issues + +## Response Format +Always respond with a JSON object: + +```json +{{ + "image_content": {{ + "image_base64": "base64 encoded image data", + "prompt_used": "The full prompt sent to DALL-E", + "alt_text": "Accessibility description of the image" + }}, + "compliance": {{ + "is_valid": true/false, + "violations": [ + {{ + "severity": "error|warning|info", + "message": "Description of the issue", + "suggestion": "How to fix it", + "field": "image_prompt" + }} + ] + }}, + "rationale": "Explanation of visual choices" +}} +``` + +## Brand Visual Guidelines +{app_settings.brand_guidelines.get_image_generation_prompt()} + +## Guidelines for Effective Prompts +1. Be specific about composition, lighting, and style +2. Include product description context for accuracy +3. Mention brand colors when appropriate +4. Specify the intended use (social media, banner, etc.) +5. Request professional, high-quality output +6. Avoid mentioning competitors or prohibited content + +## Prompt Structure +Build prompts with this structure: +1. Main subject and action +2. Product context (from image_description) +3. Setting/environment +4. Lighting and mood +5. Brand style elements +6. Technical requirements (composition, quality) + +## Example Prompt Construction +For a wireless headphone social media ad: + +"Professional marketing photo of sleek wireless headphones in matte black with rose gold accents. +The headphones are positioned on a minimalist white desk with subtle lifestyle elements. +Bright, natural lighting with soft shadows. Modern, clean aesthetic. +Incorporate brand blue (#0078D4) in background accents. +High-end product photography style, suitable for Instagram." +""" + + @classmethod + async def create_agent(cls) -> ChatAgent: + """Create the Image Content agent instance.""" + chat_client = await cls.get_chat_client() + + return chat_client.create_agent( + name=cls.get_agent_name(), + instructions=cls.get_agent_instructions(), + tools=[generate_dalle_image, validate_image_prompt], + ) diff --git a/content-gen/src/backend/agents/planning_agent.py b/content-gen/src/backend/agents/planning_agent.py new file mode 100644 index 000000000..62747b334 --- /dev/null +++ b/content-gen/src/backend/agents/planning_agent.py @@ -0,0 +1,102 @@ +""" +Planning Agent - Parses creative briefs and develops content strategy. + +Responsibilities: +- Parse free-text creative briefs into structured 9-field format +- Develop content strategy based on brief requirements +- Return parsed brief for user confirmation before content generation +""" + +import json +from typing import Any + +from agent_framework import ChatAgent + +from backend.agents.base_agent import BaseAgentFactory +from backend.settings import app_settings + + +class PlanningAgentFactory(BaseAgentFactory): + """Factory for creating the Planning agent.""" + + @classmethod + def get_agent_name(cls) -> str: + return "PlanningAgent" + + @classmethod + def get_agent_instructions(cls) -> str: + return f"""You are the Planning Agent, responsible for parsing creative briefs and developing content strategy. + +## Your Role +1. Accept free-text creative brief descriptions from users +2. Extract and structure the information into the 9 required fields +3. Develop a content strategy based on the brief +4. Return the parsed brief for user confirmation + +## Creative Brief Fields to Extract +Parse the user's input to identify these 9 fields: + +1. **Overview**: Campaign summary and context +2. **Objectives**: Goals, KPIs, and success metrics +3. **Target Audience**: Demographics, psychographics, and customer segments +4. **Key Message**: Core messaging and value proposition +5. **Tone and Style**: Voice, manner, and communication style +6. **Deliverable**: Expected outputs (e.g., social posts, banners, email) +7. **Timelines**: Due dates, milestones, and scheduling +8. **Visual Guidelines**: Image requirements and visual direction +9. **CTA**: Call to action text and placement + +## Response Format +Always respond with a JSON object containing the parsed brief: + +```json +{{ + "creative_brief": {{ + "overview": "...", + "objectives": "...", + "target_audience": "...", + "key_message": "...", + "tone_and_style": "...", + "deliverable": "...", + "timelines": "...", + "visual_guidelines": "...", + "cta": "..." + }}, + "confidence_score": 0.85, + "missing_fields": ["list of fields that need clarification"], + "suggested_products": ["relevant product categories based on brief"], + "content_strategy": "Brief description of recommended approach" +}} +``` + +## Guidelines +- If a field is not explicitly mentioned, infer from context or mark as needing clarification +- Provide a confidence score (0-1) for the overall extraction quality +- List any fields that are ambiguous or missing +- Suggest relevant product categories that might be needed +- Keep the user informed about what's been understood and what needs clarification + +## Brand Context +{app_settings.brand_guidelines.get_text_generation_prompt()} + +## Example Interaction +User: "We need a summer campaign for our new wireless headphones targeting young professionals. Fun and energetic vibe, launching next month on social media." + +Your response should extract: +- Overview: Summer campaign for wireless headphones launch +- Target Audience: Young professionals +- Tone and Style: Fun and energetic +- Deliverable: Social media content +- Timelines: Next month +- And infer/request missing details for other fields +""" + + @classmethod + async def create_agent(cls) -> ChatAgent: + """Create the Planning agent instance.""" + chat_client = await cls.get_chat_client() + + return chat_client.create_agent( + name=cls.get_agent_name(), + instructions=cls.get_agent_instructions(), + ) diff --git a/content-gen/src/backend/agents/research_agent.py b/content-gen/src/backend/agents/research_agent.py new file mode 100644 index 000000000..a2e74f371 --- /dev/null +++ b/content-gen/src/backend/agents/research_agent.py @@ -0,0 +1,227 @@ +""" +Research Agent - Retrieves products and assembles grounding data. + +Responsibilities: +- Query products from Azure AI Search based on brief requirements +- Fetch product details including image descriptions +- Search for visual styles and color palettes +- Assemble grounding data for content generation agents +""" + +from typing import Any, Dict, List, Optional +import json + +from agent_framework import ChatAgent + +from backend.agents.base_agent import BaseAgentFactory +from backend.services.search_service import get_search_service +from backend.settings import app_settings + + +# Tool function for product search +async def search_products( + query: str, + category: str = None, + sub_category: str = None, + limit: int = 5 +) -> Dict[str, Any]: + """ + Search for products using Azure AI Search. + + Args: + query: Search query text (product names, features, keywords) + category: Product category to filter by (e.g., "Electronics", "Footwear") + sub_category: Product sub-category to filter by (e.g., "Audio", "Athletic") + limit: Maximum number of products to return (default: 5) + + Returns: + Dictionary containing matching products with details + """ + search_service = await get_search_service() + + products = await search_service.search_products( + query=query, + category=category, + sub_category=sub_category, + top=limit + ) + + return { + "products": products, + "total_count": len(products), + "query": query, + "filters": { + "category": category, + "sub_category": sub_category + } + } + + +async def search_visual_styles( + query: str, + color_family: str = None, + mood: str = None, + limit: int = 3 +) -> Dict[str, Any]: + """ + Search for visual styles and color palettes for image generation. + + Args: + query: Search query (color names, moods, styles) + color_family: Color family filter (Cool, Warm, Neutral, Earth, Nature, Contrast) + mood: Mood filter (e.g., "Professional", "Calm", "Energetic") + limit: Maximum number of results (default: 3) + + Returns: + Dictionary containing matching visual styles with color info + """ + search_service = await get_search_service() + + images = await search_service.search_images( + query=query, + color_family=color_family, + mood=mood, + top=limit + ) + + return { + "visual_styles": images, + "total_count": len(images), + "query": query, + "filters": { + "color_family": color_family, + "mood": mood + } + } + + +async def get_grounding_context( + product_query: str, + visual_query: str = None, + category: str = None, + mood: str = None +) -> Dict[str, Any]: + """ + Get comprehensive grounding context for content generation. + + Searches both products and visual styles to provide complete + context for generating marketing content. + + Args: + product_query: Query for finding relevant products + visual_query: Query for visual style/color palette (optional) + category: Product category filter (optional) + mood: Visual mood filter (optional) + + Returns: + Combined grounding context with products, visuals, and summary + """ + search_service = await get_search_service() + + context = await search_service.get_grounding_context( + product_query=product_query, + image_query=visual_query, + category=category, + mood=mood + ) + + return context + + +class ResearchAgentFactory(BaseAgentFactory): + """Factory for creating the Research agent.""" + + @classmethod + def get_agent_name(cls) -> str: + return "ResearchAgent" + + @classmethod + def get_agent_instructions(cls) -> str: + return f"""You are the Research Agent, responsible for gathering product information and grounding data from Azure AI Search. + +## Your Role +1. Search and retrieve relevant products based on creative brief requirements +2. Find appropriate visual styles and color palettes for image generation +3. Assemble comprehensive grounding context for content generation +4. Provide product and style recommendations based on campaign needs + +## Available Tools +You have access to the following search tools: + +### 1. search_products +Search for products in the product catalog. +- **query**: What to search for (product names, features, keywords) +- **category**: Filter by category (Electronics, Footwear, Accessories, Home & Kitchen, etc.) +- **sub_category**: Filter by sub-category (Audio, Athletic, Watches, Coffee, etc.) +- **limit**: Max results (default: 5) + +### 2. search_visual_styles +Search for visual styles and color palettes for image generation. +- **query**: Color, mood, or style keywords +- **color_family**: Filter by color family (Cool, Warm, Neutral, Earth, Nature, Contrast) +- **mood**: Filter by mood (Professional, Calm, Energetic, Luxurious, etc.) +- **limit**: Max results (default: 3) + +### 3. get_grounding_context +Get combined product and visual context for content generation. +- **product_query**: What products to find +- **visual_query**: What visual style to find (optional) +- **category**: Product category filter (optional) +- **mood**: Visual mood filter (optional) + +## Product Data Schema +Products contain: +- product_name, sku, model +- category, sub_category +- marketing_description: Short marketing copy +- detailed_spec_description: Technical specifications +- image_description: Visual description for DALL-E context + +## Visual Style Data Schema +Visual styles contain: +- name: Style name (e.g., "BlueAsh", "SteelSky") +- primary_color, secondary_color +- color_family: Cool, Warm, Neutral, Earth, Nature, Contrast +- mood: Emotional tone (Professional, Calm, Energetic, etc.) +- style: Visual style (Modern, Rustic, Minimalist, etc.) +- use_cases: Recommended applications +- blob_url: URL to the image + +## Response Format +When returning research results, provide structured JSON: + +```json +{{{{ + "products": [...], + "visual_styles": [...], + "grounding_context": "Summary of relevant information for content generation", + "recommendations": {{{{ + "suggested_products": "Which products best fit the campaign", + "suggested_visuals": "Which visual styles match the campaign mood", + "content_direction": "How to approach the content creation" + }}}} +}}}} +``` + +## Guidelines +- Match products to the creative brief's target audience and campaign goals +- Select visual styles that complement the product and campaign mood +- Include image descriptions for DALL-E image generation context +- Summarize key product features relevant to the marketing message +- Flag if no suitable products or styles are found +- Consider color harmony between product and visual style choices + +## Brand Context +{app_settings.brand_guidelines.get_compliance_prompt()} +""" + + @classmethod + async def create_agent(cls) -> ChatAgent: + """Create the Research agent instance.""" + chat_client = await cls.get_chat_client() + + return chat_client.create_agent( + name=cls.get_agent_name(), + instructions=cls.get_agent_instructions(), + tools=[search_products, search_visual_styles, get_grounding_context], + ) diff --git a/content-gen/src/backend/agents/simple_agent.py b/content-gen/src/backend/agents/simple_agent.py new file mode 100644 index 000000000..fbb54b655 --- /dev/null +++ b/content-gen/src/backend/agents/simple_agent.py @@ -0,0 +1,181 @@ +""" +Simple Agent implementation using Azure OpenAI directly. + +This provides a working implementation that can be upgraded to +Azure AI Agents SDK when production-ready. +""" + +import json +import logging +from typing import Any, Callable, List, Optional + +from openai import AsyncAzureOpenAI +from openai.types.chat import ChatCompletionMessageParam +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential + +from backend.settings import app_settings + +logger = logging.getLogger(__name__) + + +class SimpleAgent: + """ + A simple agent that uses Azure OpenAI chat completions. + + This is a lightweight implementation suitable for development + and can be replaced with Azure AI Agents SDK for production. + """ + + def __init__( + self, + name: str, + instructions: str, + model: Optional[str] = None, + tools: Optional[List[Callable]] = None, + ): + self.name = name + self.instructions = instructions + self.model = model or app_settings.azure_openai.gpt_model + self.tools = tools or [] + self._client: Optional[AsyncAzureOpenAI] = None + + async def _get_client(self) -> AsyncAzureOpenAI: + """Get or create the Azure OpenAI client.""" + if self._client is None: + endpoint = app_settings.azure_openai.endpoint + if not endpoint: + raise ValueError("Azure OpenAI endpoint is not configured") + + client_id = app_settings.base_settings.azure_client_id + if client_id: + credential = ManagedIdentityCredential(client_id=client_id) + else: + credential = DefaultAzureCredential() + + token = await credential.get_token("https://cognitiveservices.azure.com/.default") + + self._client = AsyncAzureOpenAI( + azure_endpoint=endpoint, + azure_ad_token=token.token, + api_version=app_settings.azure_openai.api_version, + ) + return self._client + + async def run(self, user_message: str, context: Optional[str] = None) -> dict: + """ + Run the agent with a user message. + + Args: + user_message: The user's input + context: Optional additional context + + Returns: + dict with 'content' and 'agent_name' keys + """ + client = await self._get_client() + + messages: List[ChatCompletionMessageParam] = [ + {"role": "system", "content": self.instructions}, + ] + + if context: + messages.append({"role": "system", "content": f"Context: {context}"}) + + messages.append({"role": "user", "content": user_message}) + + try: + response = await client.chat.completions.create( + model=self.model, + messages=messages, + temperature=app_settings.azure_openai.temperature, + max_tokens=app_settings.azure_openai.max_tokens, + ) + + content = response.choices[0].message.content + + return { + "content": content, + "agent_name": self.name, + "is_final": True, + } + + except Exception as e: + logger.exception(f"Error in agent {self.name}: {e}") + return { + "content": f"Error: {str(e)}", + "agent_name": self.name, + "is_final": True, + } + + async def run_stream(self, user_message: str, context: Optional[str] = None): + """ + Stream the agent response. + + Yields chunks of the response as they arrive. + """ + client = await self._get_client() + + messages: List[ChatCompletionMessageParam] = [ + {"role": "system", "content": self.instructions}, + ] + + if context: + messages.append({"role": "system", "content": f"Context: {context}"}) + + messages.append({"role": "user", "content": user_message}) + + try: + stream = await client.chat.completions.create( + model=self.model, + messages=messages, + temperature=app_settings.azure_openai.temperature, + max_tokens=app_settings.azure_openai.max_tokens, + stream=True, + ) + + full_content = "" + async for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + full_content += content + yield { + "content": content, + "agent_name": self.name, + "is_final": False, + } + + # Final message with complete content + yield { + "content": full_content, + "agent_name": self.name, + "is_final": True, + } + + except Exception as e: + logger.exception(f"Error in agent {self.name}: {e}") + yield { + "content": f"Error: {str(e)}", + "agent_name": self.name, + "is_final": True, + } + + +class AgentRegistry: + """Registry for managing agent instances.""" + + _agents: dict = {} + + @classmethod + def register(cls, name: str, agent: SimpleAgent) -> None: + """Register an agent.""" + cls._agents[name] = agent + + @classmethod + def get(cls, name: str) -> Optional[SimpleAgent]: + """Get an agent by name.""" + return cls._agents.get(name) + + @classmethod + def list_agents(cls) -> List[str]: + """List all registered agent names.""" + return list(cls._agents.keys()) diff --git a/content-gen/src/backend/agents/text_content_agent.py b/content-gen/src/backend/agents/text_content_agent.py new file mode 100644 index 000000000..bf59227ad --- /dev/null +++ b/content-gen/src/backend/agents/text_content_agent.py @@ -0,0 +1,174 @@ +""" +Text Content Agent - Generates marketing copy with embedded compliance. + +Responsibilities: +- Generate headlines, body copy, CTAs based on creative brief +- Apply brand voice and tone guidelines +- Self-validate content against compliance rules +- Return content with inline warnings and suggested corrections +""" + +from typing import Any, List + +from agent_framework import ChatAgent + +from backend.agents.base_agent import BaseAgentFactory +from backend.models import ComplianceSeverity +from backend.settings import app_settings + + +def validate_text_compliance( + content: str, + content_type: str = "body" +) -> dict: + """ + Validate text content against brand guidelines and compliance rules. + + Args: + content: The text content to validate + content_type: Type of content (headline, body, cta, tagline) + + Returns: + Dictionary containing validation results with severity-categorized violations + """ + violations = [] + brand = app_settings.brand_guidelines + + # Check prohibited words (ERROR level) + for word in brand.prohibited_words: + if word.lower() in content.lower(): + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Prohibited word found: '{word}'", + "suggestion": f"Remove or replace the word '{word}' with an alternative", + "field": content_type + }) + + # Check length limits (WARNING level) + if content_type == "headline" and len(content) > brand.max_headline_length: + violations.append({ + "severity": ComplianceSeverity.WARNING.value, + "message": f"Headline exceeds {brand.max_headline_length} characters ({len(content)} chars)", + "suggestion": f"Shorten headline to under {brand.max_headline_length} characters", + "field": "headline" + }) + + if content_type == "body" and len(content) > brand.max_body_length: + violations.append({ + "severity": ComplianceSeverity.WARNING.value, + "message": f"Body copy exceeds {brand.max_body_length} characters ({len(content)} chars)", + "suggestion": f"Condense body copy to under {brand.max_body_length} characters", + "field": "body" + }) + + # Check for unsubstantiated claims (ERROR level) + claim_patterns = ["#1", "best in class", "market leader", "industry leader", "guaranteed"] + for pattern in claim_patterns: + if pattern.lower() in content.lower(): + violations.append({ + "severity": ComplianceSeverity.ERROR.value, + "message": f"Unsubstantiated claim: '{pattern}'", + "suggestion": "Remove claim or provide citation/disclaimer", + "field": content_type + }) + + # Style suggestions (INFO level) + if content_type == "body" and "!" not in content and "?" not in content: + violations.append({ + "severity": ComplianceSeverity.INFO.value, + "message": "Consider adding engaging punctuation", + "suggestion": "Add questions or exclamations to increase engagement", + "field": content_type + }) + + has_errors = any(v["severity"] == ComplianceSeverity.ERROR.value for v in violations) + + return { + "is_valid": not has_errors, + "violations": violations, + "content": content + } + + +class TextContentAgentFactory(BaseAgentFactory): + """Factory for creating the Text Content generation agent.""" + + @classmethod + def get_agent_name(cls) -> str: + return "TextContentAgent" + + @classmethod + def get_agent_instructions(cls) -> str: + return f"""You are the Text Content Agent, responsible for generating marketing copy. + +## Your Role +1. Generate compelling marketing text based on creative briefs +2. Create headlines, body copy, CTAs, and taglines +3. Apply brand voice and compliance rules during generation +4. Self-validate content and report any violations + +## Content Types You Generate +- **Headline**: Short, attention-grabbing text (max {app_settings.brand_guidelines.max_headline_length} chars) +- **Body**: Main marketing message (max {app_settings.brand_guidelines.max_body_length} chars) +- **CTA**: Clear call-to-action text +- **Tagline**: Memorable brand/campaign phrase + +## Available Tools +- **validate_text_compliance**: Check content against brand rules + +## Response Format +Always respond with a JSON object: + +```json +{{ + "text_content": {{ + "headline": "...", + "body": "...", + "cta_text": "...", + "tagline": "..." + }}, + "compliance": {{ + "is_valid": true/false, + "violations": [ + {{ + "severity": "error|warning|info", + "message": "Description of the issue", + "suggestion": "How to fix it", + "field": "Which field has the issue" + }} + ] + }}, + "rationale": "Brief explanation of creative choices" +}} +``` + +## Compliance Severity Levels +- **ERROR**: Legal/regulatory issues - content MUST be modified before use +- **WARNING**: Brand guideline deviations - review recommended +- **INFO**: Style suggestions - optional improvements + +## Brand Voice Guidelines +{app_settings.brand_guidelines.get_text_generation_prompt()} + +## Compliance Rules +{app_settings.brand_guidelines.get_compliance_prompt()} + +## Guidelines +1. ALWAYS self-validate content using validate_text_compliance before returning +2. Include all violations in the response, even if content is otherwise good +3. For ERROR-level violations, suggest specific corrections +4. Maintain brand voice while being creative +5. Tailor content to the target audience from the creative brief +6. Use product information from research to ground the content +""" + + @classmethod + async def create_agent(cls) -> ChatAgent: + """Create the Text Content agent instance.""" + chat_client = await cls.get_chat_client() + + return chat_client.create_agent( + name=cls.get_agent_name(), + instructions=cls.get_agent_instructions(), + tools=[validate_text_compliance], + ) diff --git a/content-gen/src/backend/agents/triage_agent.py b/content-gen/src/backend/agents/triage_agent.py new file mode 100644 index 000000000..85effa534 --- /dev/null +++ b/content-gen/src/backend/agents/triage_agent.py @@ -0,0 +1,79 @@ +""" +Triage Agent - Coordinator for the HandoffBuilder orchestration. + +Routes user requests to appropriate specialist agents based on intent: +- PlanningAgent: For new creative briefs or strategy requests +- ResearchAgent: For product information queries +- TextContentAgent: For text generation requests +- ImageContentAgent: For image generation requests +- ComplianceAgent: For compliance validation requests +""" + +from typing import Any + +from agent_framework import ChatAgent + +from backend.agents.base_agent import BaseAgentFactory +from backend.settings import app_settings + + +class TriageAgentFactory(BaseAgentFactory): + """Factory for creating the Triage (coordinator) agent.""" + + @classmethod + def get_agent_name(cls) -> str: + return "TriageAgent" + + @classmethod + def get_agent_instructions(cls) -> str: + return f"""You are the Triage Agent, the coordinator for a marketing content generation system. + +## Your Role +You analyze user requests and route them to the appropriate specialist agent: + +1. **PlanningAgent** - Route here when: + - User provides a new creative brief (free-text description of a campaign) + - User wants to modify or refine an existing brief + - User asks about campaign strategy or planning + +2. **ResearchAgent** - Route here when: + - User asks about available products + - User needs product information for content creation + - User wants to search or filter products + +3. **TextContentAgent** - Route here when: + - User wants to generate marketing copy (headlines, body text, CTAs) + - User wants to iterate on previously generated text + - Creative brief is confirmed and text content is needed + +4. **ImageContentAgent** - Route here when: + - User wants to generate marketing images + - User wants to iterate on previously generated images + - Creative brief is confirmed and visual content is needed + +5. **ComplianceAgent** - Route here when: + - User explicitly asks to check content compliance + - Content needs validation before final approval + +## Routing Guidelines +- For new conversations, typically start with PlanningAgent to parse the creative brief +- After brief confirmation, route to TextContentAgent and/or ImageContentAgent based on deliverables +- Always ensure generated content passes through ComplianceAgent before final delivery +- If unsure, ask clarifying questions before routing + +## Brand Context +{app_settings.brand_guidelines.get_compliance_prompt()} + +## Response Format +When routing, clearly indicate which agent you're handing off to and why. +""" + + @classmethod + async def create_agent(cls) -> ChatAgent: + """Create the Triage agent instance.""" + chat_client = await cls.get_chat_client() + + return chat_client.create_agent( + name=cls.get_agent_name(), + instructions=cls.get_agent_instructions(), + ) diff --git a/content-gen/src/backend/models.py b/content-gen/src/backend/models.py new file mode 100644 index 000000000..882cfb8c9 --- /dev/null +++ b/content-gen/src/backend/models.py @@ -0,0 +1,169 @@ +""" +Data models for the Intelligent Content Generation Accelerator. + +This module defines Pydantic models for: +- Creative briefs (parsed from free-text input) +- Products (stored in CosmosDB) +- Compliance validation results +- Generated content responses +""" + +from datetime import datetime +from enum import Enum +from typing import List, Optional +from pydantic import BaseModel, Field + + +class ComplianceSeverity(str, Enum): + """Severity levels for compliance violations.""" + ERROR = "error" # Legal/regulatory - blocks until modified + WARNING = "warning" # Brand guideline deviation - review recommended + INFO = "info" # Style suggestion - optional + + +class ComplianceViolation(BaseModel): + """A single compliance violation with severity and suggested fix.""" + severity: ComplianceSeverity + message: str + suggestion: str + field: Optional[str] = None # Which field the violation relates to + + +class ComplianceResult(BaseModel): + """Result of compliance validation on generated content.""" + is_valid: bool = Field(description="True if no error-level violations") + violations: List[ComplianceViolation] = Field(default_factory=list) + + @property + def has_errors(self) -> bool: + """Check if there are any error-level violations.""" + return any(v.severity == ComplianceSeverity.ERROR for v in self.violations) + + @property + def has_warnings(self) -> bool: + """Check if there are any warning-level violations.""" + return any(v.severity == ComplianceSeverity.WARNING for v in self.violations) + + +class CreativeBrief(BaseModel): + """ + Structured creative brief parsed from free-text input. + + The PlanningAgent extracts these fields from user's natural language + creative brief description. + """ + overview: str = Field(description="Campaign summary and context") + objectives: str = Field(description="Goals and KPIs for the campaign") + target_audience: str = Field(description="Demographics and psychographics") + key_message: str = Field(description="Core messaging and value proposition") + tone_and_style: str = Field(description="Voice, manner, and communication style") + deliverable: str = Field(description="Expected outputs (e.g., social posts, banners)") + timelines: str = Field(description="Due dates and milestones") + visual_guidelines: str = Field(description="Image requirements and visual direction") + cta: str = Field(description="Call to action text and placement") + + # Metadata + raw_input: Optional[str] = Field(default=None, description="Original free-text input") + confidence_score: Optional[float] = Field(default=None, description="Extraction confidence 0-1") + + +class Product(BaseModel): + """ + Product information stored in CosmosDB. + + Image descriptions are auto-generated via GPT-5 Vision during + product ingestion to enable DALL-E 3 text-based image generation. + """ + id: Optional[str] = None + product_name: str = Field(description="Display name of the product") + category: str = Field(description="Primary product category") + sub_category: Optional[str] = Field(default=None, description="Secondary category") + marketing_description: str = Field(description="Short marketing copy for the product") + detailed_spec_description: str = Field(description="Detailed specifications") + sku: str = Field(description="Stock keeping unit identifier") + model: Optional[str] = Field(default=None, description="Model number or name") + image_description: Optional[str] = Field( + default=None, + description="Auto-generated text description of product image via GPT-5 Vision" + ) + image_url: Optional[str] = Field(default=None, description="URL to product image in Blob Storage") + + # Metadata + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class GeneratedTextContent(BaseModel): + """Generated marketing text content with compliance status.""" + headline: Optional[str] = None + body: Optional[str] = None + cta_text: Optional[str] = None + tagline: Optional[str] = None + compliance: ComplianceResult = Field(default_factory=ComplianceResult) + + +class GeneratedImageContent(BaseModel): + """Generated marketing image content with compliance status.""" + image_base64: str = Field(description="Base64-encoded image data") + image_url: Optional[str] = Field(default=None, description="URL if saved to Blob Storage") + prompt_used: str = Field(description="DALL-E prompt that generated the image") + alt_text: str = Field(description="Accessibility alt text for the image") + compliance: ComplianceResult = Field(default_factory=ComplianceResult) + + +class ContentGenerationResponse(BaseModel): + """Complete response from content generation workflow.""" + text_content: Optional[GeneratedTextContent] = None + image_content: Optional[GeneratedImageContent] = None + creative_brief: CreativeBrief + products_used: List[str] = Field(default_factory=list, description="Product IDs used") + generation_id: str = Field(description="Unique ID for this generation") + created_at: datetime = Field(default_factory=datetime.utcnow) + + @property + def requires_modification(self) -> bool: + """Check if content has error-level violations requiring modification.""" + text_has_errors = self.text_content and self.text_content.compliance.has_errors + image_has_errors = self.image_content and self.image_content.compliance.has_errors + return text_has_errors or image_has_errors + + +class ConversationMessage(BaseModel): + """A message in the chat conversation.""" + id: str + role: str = Field(description="user, assistant, or system") + content: str + created_at: datetime = Field(default_factory=datetime.utcnow) + feedback: Optional[str] = None + + # For multimodal responses + image_base64: Optional[str] = None + compliance_warnings: Optional[List[ComplianceViolation]] = None + + +class Conversation(BaseModel): + """A conversation session stored in CosmosDB.""" + id: str + user_id: str + title: str + messages: List[ConversationMessage] = Field(default_factory=list) + creative_brief: Optional[CreativeBrief] = None + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + +class BriefConfirmationRequest(BaseModel): + """Request to confirm or edit a parsed creative brief.""" + conversation_id: str + creative_brief: CreativeBrief + confirmed: bool = False + edits: Optional[dict] = None # Field-level edits to apply + + +class ContentIterationRequest(BaseModel): + """Request to iterate on generated content with additional direction.""" + conversation_id: str + generation_id: str + feedback: str = Field(description="User's feedback or additional direction") + regenerate_text: bool = False + regenerate_image: bool = False diff --git a/content-gen/src/backend/orchestrator.py b/content-gen/src/backend/orchestrator.py new file mode 100644 index 000000000..a18e93c7a --- /dev/null +++ b/content-gen/src/backend/orchestrator.py @@ -0,0 +1,748 @@ +""" +Content Generation Orchestrator - Microsoft Agent Framework multi-agent orchestration. + +This module implements the multi-agent content generation workflow using +Microsoft Agent Framework's HandoffBuilder pattern for agent coordination. + +Workflow: +1. TriageAgent (Coordinator) receives user input and routes requests +2. PlanningAgent interprets creative briefs +3. ResearchAgent retrieves product/data information +4. TextContentAgent generates marketing copy +5. ImageContentAgent creates marketing images +6. ComplianceAgent validates all content + +Agents can hand off to each other dynamically based on context. +""" + +import asyncio +import json +import logging +import re +from typing import Any, AsyncIterator, Optional, cast +from collections.abc import AsyncIterable + +from agent_framework import ( + ChatAgent, + ChatMessage, + HandoffBuilder, + HandoffUserInputRequest, + RequestInfoEvent, + WorkflowEvent, + WorkflowOutputEvent, + WorkflowStatusEvent, + WorkflowRunState, +) +from agent_framework.azure import AzureOpenAIChatClient +from azure.identity import DefaultAzureCredential + +from backend.models import ( + CreativeBrief, + ContentGenerationResponse, + ComplianceViolation, + ComplianceSeverity, + ComplianceResult, +) +from backend.settings import app_settings + +logger = logging.getLogger(__name__) + + +# Agent system instructions +TRIAGE_INSTRUCTIONS = f"""You are a Triage Agent (coordinator) for a retail marketing content generation system. +Your role is to understand user requests and route them to the appropriate specialist agent. + +Analyze the user's message and determine what they need: +- Creative brief interpretation → hand off to planning_agent +- Product data lookup → hand off to research_agent +- Text content creation → hand off to text_content_agent +- Image creation → hand off to image_content_agent +- Content validation → hand off to compliance_agent + +When you identify the need, use the appropriate handoff tool to transfer to the specialist. +If the request is unclear, ask clarifying questions before handing off. +After receiving results from specialists, summarize them for the user. + +{app_settings.brand_guidelines.get_compliance_prompt()} +""" + +PLANNING_INSTRUCTIONS = """You are a Planning Agent specializing in creative brief interpretation. +Parse user-provided creative briefs and extract structured information. + +When given a creative brief, extract and return a JSON object with: +- overview: Campaign summary +- objectives: What the campaign aims to achieve +- target_audience: Who the content is for +- key_message: Core message to communicate +- tone_and_style: Voice and aesthetic direction +- deliverable: Expected outputs (social posts, ads, etc.) +- timelines: Any deadline information +- visual_guidelines: Visual style requirements +- cta: Call to action + +After parsing, hand back to the triage agent with your results. +""" + +RESEARCH_INSTRUCTIONS = """You are a Research Agent for a retail marketing system. +Your role is to provide product information, market insights, and relevant data. + +When asked about products or market data: +- Provide realistic product details (features, pricing, benefits) +- Include relevant market trends +- Suggest relevant product attributes for marketing + +Return structured JSON with product and market information. +After completing research, hand back to the triage agent with your findings. +""" + +TEXT_CONTENT_INSTRUCTIONS = f"""You are a Text Content Agent specializing in marketing copy. +Create compelling marketing copy for retail campaigns. + +{app_settings.brand_guidelines.get_text_generation_prompt()} + +Guidelines: +- Write engaging headlines and body copy +- Match the requested tone and style +- Include clear calls-to-action +- Adapt content for the specified platform (social, email, web) +- Keep content concise and impactful + +Return JSON with: +- "headline": Main headline text +- "body": Body copy text +- "cta": Call to action text +- "hashtags": Relevant hashtags (for social) +- "variations": Alternative versions if requested + +After generating content, you may hand off to compliance_agent for validation, +or hand back to triage_agent with your results. +""" + +IMAGE_CONTENT_INSTRUCTIONS = f"""You are an Image Content Agent for marketing image generation. +Create detailed image prompts for DALL-E based on marketing requirements. + +{app_settings.brand_guidelines.get_image_generation_prompt()} + +When creating image prompts: +- Describe the scene, composition, and style clearly +- Include lighting, color palette, and mood +- Specify any brand elements or product placement +- Ensure the prompt aligns with campaign objectives + +Return JSON with: +- "prompt": Detailed DALL-E prompt +- "style": Visual style description +- "aspect_ratio": Recommended aspect ratio +- "notes": Additional considerations + +After generating the prompt, you may hand off to compliance_agent for validation, +or hand back to triage_agent with your results. +""" + +COMPLIANCE_INSTRUCTIONS = f"""You are a Compliance Agent for marketing content validation. +Review content against brand guidelines and compliance requirements. + +{app_settings.brand_guidelines.get_compliance_prompt()} + +Check for: +- Brand voice consistency +- Prohibited words or phrases +- Legal/regulatory compliance +- Tone appropriateness +- Factual accuracy claims + +Return JSON with: +- "approved": boolean +- "violations": array of issues found, each with: + - "severity": "info", "warning", or "error" + - "message": description of the issue + - "suggestion": how to fix it +- "corrected_content": corrected versions if there are errors +- "approval_status": "BLOCKED", "REVIEW_RECOMMENDED", or "APPROVED" + +After validation, hand back to triage_agent with results. +""" + + +class ContentGenerationOrchestrator: + """ + Orchestrates the multi-agent content generation workflow using + Microsoft Agent Framework's HandoffBuilder. + + Agents: + - Triage (coordinator) - routes requests to specialists + - Planning (brief interpretation) + - Research (data retrieval) + - TextContent (copy generation) + - ImageContent (image creation) + - Compliance (validation) + """ + + def __init__(self): + self._chat_client: Optional[AzureOpenAIChatClient] = None + self._agents: dict = {} + self._workflow = None + self._initialized = False + + def _get_chat_client(self) -> AzureOpenAIChatClient: + """Get or create the Azure OpenAI chat client.""" + if self._chat_client is None: + endpoint = app_settings.azure_openai.endpoint + if not endpoint: + raise ValueError("AZURE_OPENAI_ENDPOINT is not configured") + + # Use DefaultAzureCredential for RBAC authentication + logger.info("Using DefaultAzureCredential for Azure OpenAI") + self._chat_client = AzureOpenAIChatClient( + endpoint=endpoint, + deployment_name=app_settings.azure_openai.gpt_model, + api_version=app_settings.azure_openai.api_version, + credential=DefaultAzureCredential(), + ) + return self._chat_client + + def initialize(self) -> None: + """Initialize all agents and build the handoff workflow.""" + if self._initialized: + return + + logger.info("Initializing Content Generation Orchestrator with Agent Framework...") + + # Get the chat client + chat_client = self._get_chat_client() + + # Create all agents + triage_agent = chat_client.create_agent( + name="triage_agent", + instructions=TRIAGE_INSTRUCTIONS, + ) + + planning_agent = chat_client.create_agent( + name="planning_agent", + instructions=PLANNING_INSTRUCTIONS, + ) + + research_agent = chat_client.create_agent( + name="research_agent", + instructions=RESEARCH_INSTRUCTIONS, + ) + + text_content_agent = chat_client.create_agent( + name="text_content_agent", + instructions=TEXT_CONTENT_INSTRUCTIONS, + ) + + image_content_agent = chat_client.create_agent( + name="image_content_agent", + instructions=IMAGE_CONTENT_INSTRUCTIONS, + ) + + compliance_agent = chat_client.create_agent( + name="compliance_agent", + instructions=COMPLIANCE_INSTRUCTIONS, + ) + + # Store agents for direct access + self._agents = { + "triage": triage_agent, + "planning": planning_agent, + "research": research_agent, + "text_content": text_content_agent, + "image_content": image_content_agent, + "compliance": compliance_agent, + } + + # Build the handoff workflow + # Triage can route to all specialists + # Specialists hand back to triage after completing their task + # Content agents can also hand off to compliance for validation + self._workflow = ( + HandoffBuilder( + name="content_generation_workflow", + participants=[ + triage_agent, + planning_agent, + research_agent, + text_content_agent, + image_content_agent, + compliance_agent, + ], + ) + .set_coordinator("triage_agent") + # Triage can hand off to all specialists + .add_handoff("triage_agent", [ + "planning_agent", + "research_agent", + "text_content_agent", + "image_content_agent", + "compliance_agent" + ]) + # All specialists can hand back to triage + .add_handoff("planning_agent", ["triage_agent"]) + .add_handoff("research_agent", ["triage_agent"]) + # Content agents can request compliance check + .add_handoff("text_content_agent", ["compliance_agent", "triage_agent"]) + .add_handoff("image_content_agent", ["compliance_agent", "triage_agent"]) + # Compliance can hand back to content agents for corrections or to triage + .add_handoff("compliance_agent", ["text_content_agent", "image_content_agent", "triage_agent"]) + .with_termination_condition( + # Terminate after 10 user messages to prevent infinite loops + lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 10 + ) + .build() + ) + + self._initialized = True + logger.info("Content Generation Orchestrator initialized successfully with Agent Framework") + + async def process_message( + self, + message: str, + conversation_id: str, + context: Optional[dict] = None + ) -> AsyncIterator[dict]: + """ + Process a user message through the orchestrated workflow. + + Uses the Agent Framework's HandoffBuilder workflow to coordinate + between specialized agents. + + Args: + message: The user's input message + conversation_id: Unique identifier for the conversation + context: Optional context (previous messages, user preferences) + + Yields: + dict: Response chunks with agent responses and status updates + """ + if not self._initialized: + self.initialize() + + logger.info(f"Processing message for conversation {conversation_id}") + + # Prepare the input with context + full_input = message + if context: + full_input = f"Context:\n{json.dumps(context, indent=2)}\n\nUser Message:\n{message}" + + try: + # Collect events from the workflow stream + events = [] + async for event in self._workflow.run_stream(full_input): + events.append(event) + + # Handle different event types from the workflow + if isinstance(event, WorkflowStatusEvent): + yield { + "type": "status", + "content": event.state.name, + "is_final": False, + "metadata": {"conversation_id": conversation_id} + } + + elif isinstance(event, RequestInfoEvent): + # Workflow is requesting user input + if isinstance(event.data, HandoffUserInputRequest): + # Extract conversation history + conversation_text = "\n".join([ + f"{msg.author_name or msg.role.value}: {msg.text}" + for msg in event.data.conversation + ]) + yield { + "type": "agent_response", + "agent": event.data.conversation[-1].author_name if event.data.conversation else "unknown", + "content": event.data.conversation[-1].text if event.data.conversation else "", + "conversation_history": conversation_text, + "is_final": False, + "requires_user_input": True, + "request_id": event.request_id, + "metadata": {"conversation_id": conversation_id} + } + + elif isinstance(event, WorkflowOutputEvent): + # Final output from the workflow + conversation = cast(list[ChatMessage], event.data) + if isinstance(conversation, list) and conversation: + # Get the last assistant message as the final response + assistant_messages = [ + msg for msg in conversation + if msg.role.value != "user" + ] + if assistant_messages: + last_msg = assistant_messages[-1] + yield { + "type": "agent_response", + "agent": last_msg.author_name or "assistant", + "content": last_msg.text, + "is_final": True, + "metadata": {"conversation_id": conversation_id} + } + + except Exception as e: + logger.exception(f"Error processing message: {e}") + yield { + "type": "error", + "content": f"An error occurred: {str(e)}", + "is_final": True, + "metadata": {"conversation_id": conversation_id} + } + + async def send_user_response( + self, + request_id: str, + user_response: str, + conversation_id: str + ) -> AsyncIterator[dict]: + """ + Send a user response to a pending workflow request. + + Args: + request_id: The ID of the pending request + user_response: The user's response + conversation_id: Unique identifier for the conversation + + Yields: + dict: Response chunks from continuing the workflow + """ + if not self._initialized: + self.initialize() + + try: + responses = {request_id: user_response} + async for event in self._workflow.send_responses_streaming(responses): + if isinstance(event, WorkflowStatusEvent): + yield { + "type": "status", + "content": event.state.name, + "is_final": False, + "metadata": {"conversation_id": conversation_id} + } + + elif isinstance(event, RequestInfoEvent): + if isinstance(event.data, HandoffUserInputRequest): + yield { + "type": "agent_response", + "agent": event.data.conversation[-1].author_name if event.data.conversation else "unknown", + "content": event.data.conversation[-1].text if event.data.conversation else "", + "is_final": False, + "requires_user_input": True, + "request_id": event.request_id, + "metadata": {"conversation_id": conversation_id} + } + + elif isinstance(event, WorkflowOutputEvent): + conversation = cast(list[ChatMessage], event.data) + if isinstance(conversation, list) and conversation: + assistant_messages = [ + msg for msg in conversation + if msg.role.value != "user" + ] + if assistant_messages: + last_msg = assistant_messages[-1] + yield { + "type": "agent_response", + "agent": last_msg.author_name or "assistant", + "content": last_msg.text, + "is_final": True, + "metadata": {"conversation_id": conversation_id} + } + + except Exception as e: + logger.exception(f"Error sending user response: {e}") + yield { + "type": "error", + "content": f"An error occurred: {str(e)}", + "is_final": True, + "metadata": {"conversation_id": conversation_id} + } + + async def parse_brief( + self, + brief_text: str + ) -> CreativeBrief: + """ + Parse a free-text creative brief into structured format. + + Args: + brief_text: Free-text creative brief from user + + Returns: + CreativeBrief: Parsed and structured creative brief + """ + if not self._initialized: + self.initialize() + + planning_agent = self._agents["planning"] + + parse_prompt = f""" +Parse the following creative brief into the structured JSON format: + +{brief_text} + +Return ONLY a valid JSON object with these fields: +- overview +- objectives +- target_audience +- key_message +- tone_and_style +- deliverable +- timelines +- visual_guidelines +- cta +""" + + # Use the agent's run method (async in Agent Framework) + response = await planning_agent.run(parse_prompt) + + # Parse the JSON response + try: + # Extract JSON from the response + response_text = str(response) + if "```json" in response_text: + json_start = response_text.index("```json") + 7 + json_end = response_text.index("```", json_start) + response_text = response_text[json_start:json_end].strip() + elif "```" in response_text: + json_start = response_text.index("```") + 3 + json_end = response_text.index("```", json_start) + response_text = response_text[json_start:json_end].strip() + + brief_data = json.loads(response_text) + + # Ensure all fields are strings (agent might return dicts for some fields) + for key in brief_data: + if isinstance(brief_data[key], dict): + # Convert dict to formatted string + brief_data[key] = " | ".join(f"{k}: {v}" for k, v in brief_data[key].items()) + elif isinstance(brief_data[key], list): + # Convert list to comma-separated string + brief_data[key] = ", ".join(str(item) for item in brief_data[key]) + elif brief_data[key] is None: + brief_data[key] = "" + elif not isinstance(brief_data[key], str): + brief_data[key] = str(brief_data[key]) + + return CreativeBrief(**brief_data) + except Exception as e: + logger.error(f"Failed to parse brief response: {e}") + # Try to extract fields manually from the input text + return self._extract_brief_from_text(brief_text) + + def _extract_brief_from_text(self, text: str) -> CreativeBrief: + """Extract brief fields from labeled text like 'Overview: ...'""" + fields = { + 'overview': '', + 'objectives': '', + 'target_audience': '', + 'key_message': '', + 'tone_and_style': '', + 'deliverable': '', + 'timelines': '', + 'visual_guidelines': '', + 'cta': '' + } + + # Common label variations + label_map = { + 'overview': ['overview'], + 'objectives': ['objectives', 'objective'], + 'target_audience': ['target audience', 'target_audience', 'audience'], + 'key_message': ['key message', 'key_message', 'message'], + 'tone_and_style': ['tone & style', 'tone and style', 'tone_and_style', 'tone', 'style'], + 'deliverable': ['deliverable', 'deliverables'], + 'timelines': ['timeline', 'timelines', 'timing'], + 'visual_guidelines': ['visual guidelines', 'visual_guidelines', 'visuals'], + 'cta': ['call to action', 'cta', 'call-to-action'] + } + + lines = text.strip().split('\n') + current_field = None + + for line in lines: + line = line.strip() + if not line: + continue + + # Check if line starts with a label + found_label = False + for field, labels in label_map.items(): + for label in labels: + if line.lower().startswith(label + ':'): + current_field = field + # Get the value after the colon + value = line[len(label) + 1:].strip() + fields[field] = value + found_label = True + break + if found_label: + break + + # If no label found and we have a current field, append to it + if not found_label and current_field: + fields[current_field] += ' ' + line + + # If no fields were extracted, put everything in overview + if not any(fields.values()): + fields['overview'] = text + + return CreativeBrief(**fields) + + async def generate_content( + self, + brief: CreativeBrief, + products: list = None, + generate_images: bool = True + ) -> dict: + """ + Generate complete content package from a confirmed creative brief. + + Args: + brief: Confirmed creative brief + products: List of products to feature + generate_images: Whether to generate images + + Returns: + dict: Generated content with compliance results + """ + if not self._initialized: + self.initialize() + + results = { + "text_content": None, + "image_prompt": None, + "compliance": None, + "violations": [], + "requires_modification": False + } + + # Build the generation request for text content + text_request = f""" +Generate marketing content based on this creative brief: + +Overview: {brief.overview} +Objectives: {brief.objectives} +Target Audience: {brief.target_audience} +Key Message: {brief.key_message} +Tone and Style: {brief.tone_and_style} +Deliverable: {brief.deliverable} +CTA: {brief.cta} + +Products to feature: {json.dumps(products or [])} +""" + + try: + # Generate text content + text_response = await self._agents["text_content"].run(text_request) + results["text_content"] = str(text_response) + + # Generate image prompt if requested + if generate_images: + image_request = f""" +Create an image generation prompt for this campaign: + +Visual Guidelines: {brief.visual_guidelines} +Key Message: {brief.key_message} +Tone and Style: {brief.tone_and_style} + +Text content context: {str(text_response)[:500] if text_response else 'N/A'} +""" + image_response = await self._agents["image_content"].run(image_request) + results["image_prompt"] = str(image_response) + + # Extract clean prompt from the response and generate actual image + try: + from backend.agents.image_content_agent import generate_dalle_image + + # Try to extract a clean prompt from the agent response + prompt_text = str(image_response) + + # If response is JSON, extract the prompt field + if '{' in prompt_text: + try: + prompt_data = json.loads(prompt_text) + if isinstance(prompt_data, dict): + prompt_text = prompt_data.get('prompt', prompt_data.get('image_prompt', prompt_text)) + except json.JSONDecodeError: + # Try to extract JSON from markdown code blocks + json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', prompt_text, re.DOTALL) + if json_match: + try: + prompt_data = json.loads(json_match.group(1)) + prompt_text = prompt_data.get('prompt', prompt_data.get('image_prompt', prompt_text)) + except: + pass + + # Generate the actual image using DALL-E + logger.info(f"Generating DALL-E image with prompt: {prompt_text[:200]}...") + image_result = await generate_dalle_image( + prompt=prompt_text, + scene_description=brief.visual_guidelines + ) + + if image_result.get("success"): + results["image_base64"] = image_result.get("image_base64") + results["image_revised_prompt"] = image_result.get("revised_prompt") + logger.info("DALL-E image generated successfully") + else: + logger.warning(f"DALL-E image generation failed: {image_result.get('error')}") + results["image_error"] = image_result.get("error") + + except Exception as img_error: + logger.exception(f"Error generating DALL-E image: {img_error}") + results["image_error"] = str(img_error) + + # Run compliance check + compliance_request = f""" +Review this marketing content for compliance: + +TEXT CONTENT: +{results["text_content"]} + +IMAGE PROMPT: +{results.get('image_prompt', 'N/A')} + +Check against brand guidelines and flag any issues. +""" + compliance_response = await self._agents["compliance"].run(compliance_request) + results["compliance"] = str(compliance_response) + + # Try to parse compliance violations + try: + compliance_data = json.loads(str(compliance_response)) + violations = compliance_data.get("violations", []) + # Store violations as dicts for JSON serialization + results["violations"] = [ + { + "severity": v.get("severity", "warning"), + "message": v.get("message", v.get("description", "")), + "suggestion": v.get("suggestion", ""), + "field": v.get("field", v.get("location")) + } + for v in violations + if v.get("message") or v.get("description") # Only include if has message + ] + results["requires_modification"] = any( + v.get("severity") == "error" + for v in results["violations"] + ) + except (json.JSONDecodeError, KeyError): + pass + + except Exception as e: + logger.exception(f"Error generating content: {e}") + results["error"] = str(e) + + return results + + +# Singleton instance +_orchestrator: Optional[ContentGenerationOrchestrator] = None + + +def get_orchestrator() -> ContentGenerationOrchestrator: + """Get or create the singleton orchestrator instance.""" + global _orchestrator + if _orchestrator is None: + _orchestrator = ContentGenerationOrchestrator() + _orchestrator.initialize() + return _orchestrator diff --git a/content-gen/src/backend/services/__init__.py b/content-gen/src/backend/services/__init__.py new file mode 100644 index 000000000..f9b2d8254 --- /dev/null +++ b/content-gen/src/backend/services/__init__.py @@ -0,0 +1,11 @@ +"""Services package for Content Generation Solution Accelerator.""" + +from backend.services.cosmos_service import CosmosDBService, get_cosmos_service +from backend.services.blob_service import BlobStorageService, get_blob_service + +__all__ = [ + "CosmosDBService", + "get_cosmos_service", + "BlobStorageService", + "get_blob_service", +] diff --git a/content-gen/src/backend/services/blob_service.py b/content-gen/src/backend/services/blob_service.py new file mode 100644 index 000000000..b1edce8dc --- /dev/null +++ b/content-gen/src/backend/services/blob_service.py @@ -0,0 +1,284 @@ +""" +Blob Storage Service - Manages product images and generated content. + +Provides async operations for: +- Product image upload and retrieval +- Generated image storage +- Image description generation via GPT-5 Vision +""" + +import base64 +import io +import logging +from typing import Optional, Tuple +from datetime import datetime, timezone + +from azure.storage.blob.aio import BlobServiceClient, ContainerClient +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential +from openai import AsyncAzureOpenAI + +from backend.settings import app_settings + +logger = logging.getLogger(__name__) + + +class BlobStorageService: + """Service for interacting with Azure Blob Storage.""" + + def __init__(self): + self._client: Optional[BlobServiceClient] = None + self._product_images_container: Optional[ContainerClient] = None + self._generated_images_container: Optional[ContainerClient] = None + + async def _get_credential(self): + """Get Azure credential for authentication.""" + client_id = app_settings.base_settings.azure_client_id + if client_id: + return ManagedIdentityCredential(client_id=client_id) + return DefaultAzureCredential() + + async def initialize(self) -> None: + """Initialize Blob Storage client and containers.""" + if self._client: + return + + credential = await self._get_credential() + + self._client = BlobServiceClient( + account_url=f"https://{app_settings.blob.account_name}.blob.core.windows.net", + credential=credential + ) + + self._product_images_container = self._client.get_container_client( + app_settings.blob.product_images_container + ) + + self._generated_images_container = self._client.get_container_client( + app_settings.blob.generated_images_container + ) + + logger.info("Blob Storage service initialized") + + async def close(self) -> None: + """Close the Blob Storage client.""" + if self._client: + await self._client.close() + self._client = None + + # ==================== Product Image Operations ==================== + + async def upload_product_image( + self, + sku: str, + image_data: bytes, + content_type: str = "image/jpeg" + ) -> Tuple[str, str]: + """ + Upload a product image and generate its description. + + Args: + sku: Product SKU (used as blob name prefix) + image_data: Raw image bytes + content_type: MIME type of the image + + Returns: + Tuple of (blob_url, generated_description) + """ + await self.initialize() + + # Generate unique blob name + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") + extension = content_type.split("/")[-1] + blob_name = f"{sku}/{timestamp}.{extension}" + + # Upload the image + blob_client = self._product_images_container.get_blob_client(blob_name) + await blob_client.upload_blob( + image_data, + content_type=content_type, + overwrite=True + ) + + blob_url = blob_client.url + + # Generate description using GPT-5 Vision + description = await self.generate_image_description(image_data) + + logger.info(f"Uploaded product image: {blob_name}") + return blob_url, description + + async def get_product_image_url(self, sku: str) -> Optional[str]: + """ + Get the URL of the latest product image. + + Args: + sku: Product SKU + + Returns: + URL of the latest image, or None if not found + """ + await self.initialize() + + # List blobs with the SKU prefix + blobs = [] + async for blob in self._product_images_container.list_blobs( + name_starts_with=f"{sku}/" + ): + blobs.append(blob) + + if not blobs: + return None + + # Get the most recent blob + latest_blob = sorted(blobs, key=lambda b: b.name, reverse=True)[0] + blob_client = self._product_images_container.get_blob_client(latest_blob.name) + + return blob_client.url + + # ==================== Generated Image Operations ==================== + + async def save_generated_image( + self, + conversation_id: str, + image_base64: str, + content_type: str = "image/png" + ) -> str: + """ + Save a DALL-E generated image to blob storage. + + Args: + conversation_id: ID of the conversation that generated the image + image_base64: Base64-encoded image data + content_type: MIME type of the image + + Returns: + URL of the saved image + """ + await self.initialize() + + # Decode base64 image + image_data = base64.b64decode(image_base64) + + # Generate unique blob name + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") + extension = content_type.split("/")[-1] + blob_name = f"{conversation_id}/{timestamp}.{extension}" + + # Upload the image + blob_client = self._generated_images_container.get_blob_client(blob_name) + await blob_client.upload_blob( + image_data, + content_type=content_type, + overwrite=True + ) + + logger.info(f"Saved generated image: {blob_name}") + return blob_client.url + + async def get_generated_images( + self, + conversation_id: str + ) -> list[str]: + """ + Get all generated images for a conversation. + + Args: + conversation_id: ID of the conversation + + Returns: + List of image URLs + """ + await self.initialize() + + urls = [] + async for blob in self._generated_images_container.list_blobs( + name_starts_with=f"{conversation_id}/" + ): + blob_client = self._generated_images_container.get_blob_client(blob.name) + urls.append(blob_client.url) + + return urls + + # ==================== Image Description Generation ==================== + + async def generate_image_description(self, image_data: bytes) -> str: + """ + Generate a detailed text description of an image using GPT-5 Vision. + + This is used to create descriptions of product images that can be + used as context for DALL-E 3 image generation (since DALL-E 3 + cannot accept image inputs directly). + + Args: + image_data: Raw image bytes + + Returns: + Detailed text description of the image + """ + # Encode image to base64 + image_base64 = base64.b64encode(image_data).decode("utf-8") + + try: + credential = await self._get_credential() + token = await credential.get_token("https://cognitiveservices.azure.com/.default") + + client = AsyncAzureOpenAI( + azure_endpoint=app_settings.azure_openai.endpoint, + azure_ad_token=token.token, + api_version=app_settings.azure_openai.api_version, + ) + + response = await client.chat.completions.create( + model=app_settings.azure_openai.gpt_model, + messages=[ + { + "role": "system", + "content": """You are an expert at describing product images for marketing purposes. +Provide detailed, accurate descriptions that capture: +- Product appearance (shape, color, materials, finish) +- Key visual features and design elements +- Size and proportions (relative descriptions) +- Styling and aesthetic qualities +- Any visible branding or labels + +Your descriptions will be used to guide AI image generation, so be specific and vivid.""" + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Describe this product image in detail for use in marketing content generation:" + }, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{image_base64}" + } + } + ] + } + ], + max_tokens=500 + ) + + description = response.choices[0].message.content + logger.info(f"Generated image description: {description[:100]}...") + return description + + except Exception as e: + logger.exception(f"Error generating image description: {e}") + return "Product image - description unavailable" + + +# Singleton instance +_blob_service: Optional[BlobStorageService] = None + + +async def get_blob_service() -> BlobStorageService: + """Get or create the singleton Blob Storage service instance.""" + global _blob_service + if _blob_service is None: + _blob_service = BlobStorageService() + await _blob_service.initialize() + return _blob_service diff --git a/content-gen/src/backend/services/cosmos_service.py b/content-gen/src/backend/services/cosmos_service.py new file mode 100644 index 000000000..baf83dfc0 --- /dev/null +++ b/content-gen/src/backend/services/cosmos_service.py @@ -0,0 +1,371 @@ +""" +CosmosDB Service - Manages products and conversation storage. + +Provides async operations for: +- Product catalog (CRUD operations) +- Conversation history +- Creative brief storage +""" + +import logging +from typing import Any, List, Optional +from datetime import datetime, timezone + +from azure.cosmos.aio import CosmosClient, ContainerProxy +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential + +from backend.settings import app_settings +from backend.models import Product, CreativeBrief + +logger = logging.getLogger(__name__) + + +class CosmosDBService: + """Service for interacting with Azure Cosmos DB.""" + + def __init__(self): + self._client: Optional[CosmosClient] = None + self._products_container: Optional[ContainerProxy] = None + self._conversations_container: Optional[ContainerProxy] = None + + async def _get_credential(self): + """Get Azure credential for authentication.""" + client_id = app_settings.base_settings.azure_client_id + if client_id: + return ManagedIdentityCredential(client_id=client_id) + return DefaultAzureCredential() + + async def initialize(self) -> None: + """Initialize CosmosDB client and containers.""" + if self._client: + return + + credential = await self._get_credential() + + self._client = CosmosClient( + url=app_settings.cosmos.endpoint, + credential=credential + ) + + database = self._client.get_database_client( + app_settings.cosmos.database_name + ) + + self._products_container = database.get_container_client( + app_settings.cosmos.products_container + ) + + self._conversations_container = database.get_container_client( + app_settings.cosmos.conversations_container + ) + + logger.info("CosmosDB service initialized") + + async def close(self) -> None: + """Close the CosmosDB client.""" + if self._client: + await self._client.close() + self._client = None + + # ==================== Product Operations ==================== + + async def get_product_by_sku(self, sku: str) -> Optional[Product]: + """ + Retrieve a product by its SKU. + + Args: + sku: Product SKU identifier + + Returns: + Product if found, None otherwise + """ + await self.initialize() + + query = "SELECT * FROM c WHERE c.sku = @sku" + params = [{"name": "@sku", "value": sku}] + + items = [] + async for item in self._products_container.query_items( + query=query, + parameters=params + ): + items.append(item) + + if items: + return Product(**items[0]) + return None + + async def get_products_by_category( + self, + category: str, + sub_category: Optional[str] = None, + limit: int = 10 + ) -> List[Product]: + """ + Retrieve products by category. + + Args: + category: Product category + sub_category: Optional sub-category filter + limit: Maximum number of products to return + + Returns: + List of matching products + """ + await self.initialize() + + if sub_category: + query = """ + SELECT TOP @limit * FROM c + WHERE c.category = @category AND c.sub_category = @sub_category + """ + params = [ + {"name": "@category", "value": category}, + {"name": "@sub_category", "value": sub_category}, + {"name": "@limit", "value": limit} + ] + else: + query = "SELECT TOP @limit * FROM c WHERE c.category = @category" + params = [ + {"name": "@category", "value": category}, + {"name": "@limit", "value": limit} + ] + + products = [] + async for item in self._products_container.query_items( + query=query, + parameters=params + ): + products.append(Product(**item)) + + return products + + async def search_products( + self, + search_term: str, + limit: int = 10 + ) -> List[Product]: + """ + Search products by name or description. + + Args: + search_term: Text to search for + limit: Maximum number of products to return + + Returns: + List of matching products + """ + await self.initialize() + + search_lower = search_term.lower() + query = """ + SELECT TOP @limit * FROM c + WHERE CONTAINS(LOWER(c.product_name), @search) + OR CONTAINS(LOWER(c.marketing_description), @search) + OR CONTAINS(LOWER(c.detailed_spec_description), @search) + """ + params = [ + {"name": "@search", "value": search_lower}, + {"name": "@limit", "value": limit} + ] + + products = [] + async for item in self._products_container.query_items( + query=query, + parameters=params + ): + products.append(Product(**item)) + + return products + + async def upsert_product(self, product: Product) -> Product: + """ + Create or update a product. + + Args: + product: Product to upsert + + Returns: + The upserted product + """ + await self.initialize() + + item = product.model_dump() + item["id"] = product.sku # Use SKU as document ID + item["updated_at"] = datetime.now(timezone.utc).isoformat() + + result = await self._products_container.upsert_item(item) + return Product(**result) + + async def get_all_products(self, limit: int = 100) -> List[Product]: + """ + Retrieve all products. + + Args: + limit: Maximum number of products to return + + Returns: + List of all products + """ + await self.initialize() + + query = "SELECT TOP @limit * FROM c" + params = [{"name": "@limit", "value": limit}] + + products = [] + async for item in self._products_container.query_items( + query=query, + parameters=params + ): + products.append(Product(**item)) + + return products + + # ==================== Conversation Operations ==================== + + async def get_conversation( + self, + conversation_id: str, + user_id: str + ) -> Optional[dict]: + """ + Retrieve a conversation by ID. + + Args: + conversation_id: Unique conversation identifier + user_id: User ID for partition key + + Returns: + Conversation data if found + """ + await self.initialize() + + try: + item = await self._conversations_container.read_item( + item=conversation_id, + partition_key=user_id + ) + return item + except Exception: + return None + + async def save_conversation( + self, + conversation_id: str, + user_id: str, + messages: List[dict], + brief: Optional[CreativeBrief] = None, + metadata: Optional[dict] = None + ) -> dict: + """ + Save or update a conversation. + + Args: + conversation_id: Unique conversation identifier + user_id: User ID for partition key + messages: List of conversation messages + brief: Associated creative brief + metadata: Additional metadata + + Returns: + The saved conversation document + """ + await self.initialize() + + item = { + "id": conversation_id, + "user_id": user_id, + "messages": messages, + "brief": brief.model_dump() if brief else None, + "metadata": metadata or {}, + "updated_at": datetime.now(timezone.utc).isoformat() + } + + result = await self._conversations_container.upsert_item(item) + return result + + async def add_message_to_conversation( + self, + conversation_id: str, + user_id: str, + message: dict + ) -> dict: + """ + Add a message to an existing conversation. + + Args: + conversation_id: Unique conversation identifier + user_id: User ID for partition key + message: Message to add + + Returns: + Updated conversation document + """ + await self.initialize() + + conversation = await self.get_conversation(conversation_id, user_id) + + if conversation: + conversation["messages"].append(message) + conversation["updated_at"] = datetime.now(timezone.utc).isoformat() + else: + conversation = { + "id": conversation_id, + "user_id": user_id, + "messages": [message], + "updated_at": datetime.now(timezone.utc).isoformat() + } + + result = await self._conversations_container.upsert_item(conversation) + return result + + async def get_user_conversations( + self, + user_id: str, + limit: int = 20 + ) -> List[dict]: + """ + Get all conversations for a user. + + Args: + user_id: User ID + limit: Maximum number of conversations + + Returns: + List of conversations + """ + await self.initialize() + + query = """ + SELECT TOP @limit c.id, c.user_id, c.updated_at, + ARRAY_LENGTH(c.messages) as message_count + FROM c + WHERE c.user_id = @user_id + ORDER BY c.updated_at DESC + """ + params = [ + {"name": "@user_id", "value": user_id}, + {"name": "@limit", "value": limit} + ] + + conversations = [] + async for item in self._conversations_container.query_items( + query=query, + parameters=params + ): + conversations.append(item) + + return conversations + + +# Singleton instance +_cosmos_service: Optional[CosmosDBService] = None + + +async def get_cosmos_service() -> CosmosDBService: + """Get or create the singleton CosmosDB service instance.""" + global _cosmos_service + if _cosmos_service is None: + _cosmos_service = CosmosDBService() + await _cosmos_service.initialize() + return _cosmos_service diff --git a/content-gen/src/backend/services/search_service.py b/content-gen/src/backend/services/search_service.py new file mode 100644 index 000000000..8087a7f26 --- /dev/null +++ b/content-gen/src/backend/services/search_service.py @@ -0,0 +1,289 @@ +""" +Azure AI Search Service for grounding content generation. + +Provides search capabilities across products and images for +AI content generation grounding. +""" + +import logging +from typing import Any, Dict, List, Optional + +from azure.core.credentials import AzureKeyCredential +from azure.identity import DefaultAzureCredential +from azure.search.documents import SearchClient +from azure.search.documents.models import VectorizedQuery + +from backend.settings import app_settings + +logger = logging.getLogger(__name__) + + +class SearchService: + """Service for searching products and images in Azure AI Search.""" + + def __init__(self): + self._products_client: Optional[SearchClient] = None + self._images_client: Optional[SearchClient] = None + self._credential = None + + def _get_credential(self): + """Get search credential - prefer RBAC, fall back to API key.""" + if self._credential: + return self._credential + + # Try RBAC first + try: + self._credential = DefaultAzureCredential() + return self._credential + except Exception: + pass + + # Fall back to API key + if app_settings.search and app_settings.search.admin_key: + self._credential = AzureKeyCredential(app_settings.search.admin_key) + return self._credential + + raise ValueError("No valid search credentials available") + + def _get_products_client(self) -> SearchClient: + """Get or create the products search client.""" + if self._products_client is None: + if not app_settings.search or not app_settings.search.endpoint: + raise ValueError("Azure AI Search endpoint not configured") + + self._products_client = SearchClient( + endpoint=app_settings.search.endpoint, + index_name=app_settings.search.products_index, + credential=self._get_credential() + ) + return self._products_client + + def _get_images_client(self) -> SearchClient: + """Get or create the images search client.""" + if self._images_client is None: + if not app_settings.search or not app_settings.search.endpoint: + raise ValueError("Azure AI Search endpoint not configured") + + self._images_client = SearchClient( + endpoint=app_settings.search.endpoint, + index_name=app_settings.search.images_index, + credential=self._get_credential() + ) + return self._images_client + + async def search_products( + self, + query: str, + category: Optional[str] = None, + sub_category: Optional[str] = None, + top: int = 5 + ) -> List[Dict[str, Any]]: + """ + Search for products using Azure AI Search. + + Args: + query: Search query text + category: Optional category filter + sub_category: Optional sub-category filter + top: Maximum number of results + + Returns: + List of matching products + """ + try: + client = self._get_products_client() + + # Build filter + filters = [] + if category: + filters.append(f"category eq '{category}'") + if sub_category: + filters.append(f"sub_category eq '{sub_category}'") + + filter_str = " and ".join(filters) if filters else None + + # Execute search + results = client.search( + search_text=query, + filter=filter_str, + top=top, + select=["id", "product_name", "sku", "model", "category", "sub_category", + "marketing_description", "detailed_spec_description", "image_description"] + ) + + products = [] + for result in results: + products.append({ + "id": result.get("id"), + "product_name": result.get("product_name"), + "sku": result.get("sku"), + "model": result.get("model"), + "category": result.get("category"), + "sub_category": result.get("sub_category"), + "marketing_description": result.get("marketing_description"), + "detailed_spec_description": result.get("detailed_spec_description"), + "image_description": result.get("image_description"), + "search_score": result.get("@search.score") + }) + + logger.info(f"Product search for '{query}' returned {len(products)} results") + return products + + except Exception as e: + logger.error(f"Product search failed: {e}") + return [] + + async def search_images( + self, + query: str, + color_family: Optional[str] = None, + mood: Optional[str] = None, + top: int = 5 + ) -> List[Dict[str, Any]]: + """ + Search for images/color palettes using Azure AI Search. + + Args: + query: Search query (color, mood, style keywords) + color_family: Optional color family filter (Cool, Warm, Neutral, etc.) + mood: Optional mood filter + top: Maximum number of results + + Returns: + List of matching images with metadata + """ + try: + client = self._get_images_client() + + # Build filter + filters = [] + if color_family: + filters.append(f"color_family eq '{color_family}'") + + filter_str = " and ".join(filters) if filters else None + + # Execute search + results = client.search( + search_text=query, + filter=filter_str, + top=top, + select=["id", "name", "filename", "primary_color", "secondary_color", + "color_family", "mood", "style", "description", "use_cases", + "blob_url", "keywords"] + ) + + images = [] + for result in results: + images.append({ + "id": result.get("id"), + "name": result.get("name"), + "filename": result.get("filename"), + "primary_color": result.get("primary_color"), + "secondary_color": result.get("secondary_color"), + "color_family": result.get("color_family"), + "mood": result.get("mood"), + "style": result.get("style"), + "description": result.get("description"), + "use_cases": result.get("use_cases"), + "blob_url": result.get("blob_url"), + "keywords": result.get("keywords"), + "search_score": result.get("@search.score") + }) + + logger.info(f"Image search for '{query}' returned {len(images)} results") + return images + + except Exception as e: + logger.error(f"Image search failed: {e}") + return [] + + async def get_grounding_context( + self, + product_query: str, + image_query: Optional[str] = None, + category: Optional[str] = None, + mood: Optional[str] = None + ) -> Dict[str, Any]: + """ + Get combined grounding context for content generation. + + Searches both products and images to provide comprehensive + context for AI content generation. + + Args: + product_query: Query for product search + image_query: Optional query for image/style search + category: Optional product category filter + mood: Optional mood/style filter for images + + Returns: + Combined grounding context with products and images + """ + # Search products + products = await self.search_products( + query=product_query, + category=category, + top=5 + ) + + # Search images if query provided + images = [] + if image_query: + images = await self.search_images( + query=image_query, + mood=mood, + top=3 + ) + + # Build grounding context + context = { + "products": products, + "images": images, + "product_count": len(products), + "image_count": len(images), + "grounding_summary": self._build_grounding_summary(products, images) + } + + return context + + def _build_grounding_summary( + self, + products: List[Dict[str, Any]], + images: List[Dict[str, Any]] + ) -> str: + """Build a text summary of grounding context for agents.""" + parts = [] + + if products: + parts.append("## Available Products\n") + for p in products[:5]: + parts.append(f"- **{p.get('product_name')}** ({p.get('sku')})") + parts.append(f" Category: {p.get('category')} > {p.get('sub_category')}") + parts.append(f" Marketing: {p.get('marketing_description', '')[:150]}...") + if p.get('image_description'): + parts.append(f" Visual: {p.get('image_description', '')[:100]}...") + parts.append("") + + if images: + parts.append("\n## Available Visual Styles\n") + for img in images[:3]: + parts.append(f"- **{img.get('name')}**") + parts.append(f" Colors: {img.get('primary_color')}, {img.get('secondary_color')}") + parts.append(f" Mood: {img.get('mood')}") + parts.append(f" Style: {img.get('style')}") + parts.append(f" Best for: {img.get('use_cases', '')[:100]}") + parts.append("") + + return "\n".join(parts) + + +# Global service instance +_search_service: Optional[SearchService] = None + + +async def get_search_service() -> SearchService: + """Get or create the search service instance.""" + global _search_service + if _search_service is None: + _search_service = SearchService() + return _search_service diff --git a/content-gen/src/backend/settings.py b/content-gen/src/backend/settings.py new file mode 100644 index 000000000..b38da1309 --- /dev/null +++ b/content-gen/src/backend/settings.py @@ -0,0 +1,326 @@ +""" +Application settings for the Intelligent Content Generation Accelerator. + +Uses Pydantic settings management with environment variable configuration. +Includes brand guidelines as solution parameters for content strategy +and compliance validation. +""" + +import json +import logging +import os +from typing import List, Optional + +from pydantic import BaseModel, Field, model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing_extensions import Self + +DOTENV_PATH = os.environ.get( + "DOTENV_PATH", os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env") +) +MINIMUM_SUPPORTED_AZURE_OPENAI_PREVIEW_API_VERSION = "2025-01-01-preview" + + +def parse_comma_separated(value: str) -> List[str]: + """Parse a comma-separated string into a list.""" + if isinstance(value, str) and len(value) > 0: + return [item.strip() for item in value.split(",") if item.strip()] + return [] + + +class _UiSettings(BaseSettings): + """UI configuration settings.""" + model_config = SettingsConfigDict( + env_prefix="UI_", env_file=DOTENV_PATH, extra="ignore", env_ignore_empty=True + ) + + app_name: str = "Content Generation Accelerator" + title: str = "Content Generation" + logo: Optional[str] = None + chat_logo: Optional[str] = None + chat_title: str = "Marketing Content Generator" + chat_description: str = "AI-powered multimodal content generation for marketing campaigns." + favicon: str = "/favicon.ico" + show_share_button: bool = False + + +class _ChatHistorySettings(BaseSettings): + """CosmosDB chat history configuration.""" + model_config = SettingsConfigDict( + env_prefix="AZURE_COSMOSDB_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + + database: str + account: str + account_key: Optional[str] = None + conversations_container: str + products_container: str = "products" + enable_feedback: bool = True + + +class _AzureOpenAISettings(BaseSettings): + """Azure OpenAI configuration for GPT-5 and DALL-E 3.""" + model_config = SettingsConfigDict( + env_prefix="AZURE_OPENAI_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + + gpt_model: str = Field(default="gpt-5", alias="AZURE_OPENAI_GPT_MODEL") + model: str = "gpt-5" + dalle_model: str = Field(default="dall-e-3", alias="AZURE_OPENAI_DALLE_MODEL") + dalle_endpoint: Optional[str] = Field(default=None, alias="AZURE_OPENAI_DALLE_ENDPOINT") + resource: Optional[str] = None + endpoint: Optional[str] = None + temperature: float = 0.7 + top_p: float = 0.95 + max_tokens: int = 2000 + stream: bool = True + api_version: str = "2024-06-01" + preview_api_version: str = "2024-02-01" + + # Image generation settings + image_size: str = "1024x1024" + image_quality: str = "hd" + + @model_validator(mode="after") + def ensure_endpoint(self) -> Self: + if self.endpoint: + return self + elif self.resource: + self.endpoint = f"https://{self.resource}.openai.azure.com" + return self + raise ValueError("AZURE_OPENAI_ENDPOINT or AZURE_OPENAI_RESOURCE is required") + + +class _AzureAISettings(BaseSettings): + """Azure AI Foundry Agent settings.""" + model_config = SettingsConfigDict( + env_prefix="AZURE_AI_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + agent_endpoint: Optional[str] = None + agent_model_deployment_name: Optional[str] = None + agent_api_version: Optional[str] = None + + +class _StorageSettings(BaseSettings): + """Azure Blob Storage configuration.""" + model_config = SettingsConfigDict( + env_prefix="AZURE_BLOB_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + + account_name: str = Field(default="", alias="AZURE_BLOB_ACCOUNT_NAME") + product_images_container: str = "product-images" + generated_images_container: str = "generated-images" + + +class _CosmosSettings(BaseSettings): + """Azure Cosmos DB configuration.""" + model_config = SettingsConfigDict( + env_prefix="AZURE_COSMOS_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + + endpoint: str = Field(default="", alias="AZURE_COSMOS_ENDPOINT") + database_name: str = Field(default="content-generation", alias="AZURE_COSMOS_DATABASE_NAME") + products_container: str = "products" + conversations_container: str = "conversations" + + +class _SearchSettings(BaseSettings): + """Azure AI Search configuration.""" + model_config = SettingsConfigDict( + env_prefix="AZURE_AI_SEARCH_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + + endpoint: str = Field(default="", alias="AZURE_AI_SEARCH_ENDPOINT") + products_index: str = Field(default="products", alias="AZURE_AI_SEARCH_PRODUCTS_INDEX") + images_index: str = Field(default="product-images", alias="AZURE_AI_SEARCH_IMAGE_INDEX") + admin_key: Optional[str] = Field(default=None, alias="AZURE_AI_SEARCH_ADMIN_KEY") + + +class _BrandGuidelinesSettings(BaseSettings): + """ + Brand guidelines stored as solution parameters. + + These are injected into all agent instructions for content strategy + and compliance validation. + """ + model_config = SettingsConfigDict( + env_prefix="BRAND_", + env_file=DOTENV_PATH, + extra="ignore", + env_ignore_empty=True, + ) + + # Voice and tone + tone: str = "Professional yet approachable" + voice: str = "Innovative, trustworthy, customer-focused" + + # Content restrictions (stored as comma-separated strings) + prohibited_words_str: str = Field(default="", alias="BRAND_PROHIBITED_WORDS") + required_disclosures_str: str = Field(default="", alias="BRAND_REQUIRED_DISCLOSURES") + + # Visual guidelines + primary_color: str = "#0078D4" + secondary_color: str = "#107C10" + image_style: str = "Modern, clean, minimalist with bright lighting" + typography: str = "Sans-serif, bold headlines, readable body text" + + # Compliance rules + max_headline_length: int = 60 + max_body_length: int = 500 + require_cta: bool = True + + @property + def prohibited_words(self) -> List[str]: + """Parse prohibited words from comma-separated string.""" + return parse_comma_separated(self.prohibited_words_str) + + @property + def required_disclosures(self) -> List[str]: + """Parse required disclosures from comma-separated string.""" + return parse_comma_separated(self.required_disclosures_str) + + def get_compliance_prompt(self) -> str: + """Generate compliance rules text for agent instructions.""" + return f""" +## Brand Compliance Rules + +### Voice and Tone +- Tone: {self.tone} +- Voice: {self.voice} + +### Content Restrictions +- Prohibited words: {', '.join(self.prohibited_words) if self.prohibited_words else 'None specified'} +- Required disclosures: {', '.join(self.required_disclosures) if self.required_disclosures else 'None required'} +- Maximum headline length: {self.max_headline_length} characters +- Maximum body length: {self.max_body_length} characters +- CTA required: {'Yes' if self.require_cta else 'No'} + +### Visual Guidelines +- Primary brand color: {self.primary_color} +- Secondary brand color: {self.secondary_color} +- Image style: {self.image_style} +- Typography: {self.typography} + +### Compliance Severity Levels +- ERROR: Legal/regulatory violations that MUST be fixed before content can be used +- WARNING: Brand guideline deviations that should be reviewed +- INFO: Style suggestions for improvement (optional) + +When validating content, categorize each violation with the appropriate severity level. +""" + + def get_text_generation_prompt(self) -> str: + """Generate brand guidelines for text content generation.""" + return f""" +## Brand Voice Guidelines + +Write content that embodies these characteristics: +- Tone: {self.tone} +- Voice: {self.voice} + +### Writing Rules +- Keep headlines under {self.max_headline_length} characters +- Keep body copy under {self.max_body_length} characters +- {'Always include a clear call-to-action' if self.require_cta else 'CTA is optional'} +- NEVER use these words: {', '.join(self.prohibited_words) if self.prohibited_words else 'No restrictions'} +- Include these disclosures when applicable: {', '.join(self.required_disclosures) if self.required_disclosures else 'None required'} +""" + + def get_image_generation_prompt(self) -> str: + """Generate brand guidelines for image content generation.""" + return f""" +## Brand Visual Guidelines + +Create images that follow these guidelines: +- Style: {self.image_style} +- Primary brand color to incorporate: {self.primary_color} +- Secondary accent color: {self.secondary_color} +- Professional, high-quality imagery suitable for marketing +- Bright, optimistic lighting +- Clean composition with 30% negative space +- No competitor products or logos +- Diverse representation if people are shown +""" + + +class _BaseSettings(BaseSettings): + """Base application settings.""" + model_config = SettingsConfigDict( + env_file=DOTENV_PATH, + extra="ignore", + arbitrary_types_allowed=True, + env_ignore_empty=True, + ) + auth_enabled: bool = False + sanitize_answer: bool = False + solution_name: Optional[str] = Field(default=None) + azure_client_id: Optional[str] = Field(default=None, alias="AZURE_CLIENT_ID") + + +class _AppSettings(BaseModel): + """Main application settings container.""" + base_settings: _BaseSettings = _BaseSettings() + azure_openai: _AzureOpenAISettings = _AzureOpenAISettings() + azure_ai: _AzureAISettings = _AzureAISettings() + brand_guidelines: _BrandGuidelinesSettings = _BrandGuidelinesSettings() + ui: Optional[_UiSettings] = _UiSettings() + + # Constructed properties + chat_history: Optional[_ChatHistorySettings] = None + blob: Optional[_StorageSettings] = None + cosmos: Optional[_CosmosSettings] = None + search: Optional[_SearchSettings] = None + + @model_validator(mode="after") + def set_chat_history_settings(self) -> Self: + try: + self.chat_history = _ChatHistorySettings() + except Exception: + self.chat_history = None + return self + + @model_validator(mode="after") + def set_storage_settings(self) -> Self: + try: + self.blob = _StorageSettings() + except Exception: + self.blob = None + return self + + @model_validator(mode="after") + def set_cosmos_settings(self) -> Self: + try: + self.cosmos = _CosmosSettings() + except Exception: + self.cosmos = None + return self + + @model_validator(mode="after") + def set_search_settings(self) -> Self: + try: + self.search = _SearchSettings() + except Exception: + self.search = None + return self + + +# Global settings instance +app_settings = _AppSettings() diff --git a/content-gen/src/frontend/index.html b/content-gen/src/frontend/index.html new file mode 100644 index 000000000..eb3027bd2 --- /dev/null +++ b/content-gen/src/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Content Generation Accelerator + + +
+ + + diff --git a/content-gen/src/frontend/package-lock.json b/content-gen/src/frontend/package-lock.json new file mode 100644 index 000000000..854be9bbf --- /dev/null +++ b/content-gen/src/frontend/package-lock.json @@ -0,0 +1,6365 @@ +{ + "name": "content-generation-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "content-generation-frontend", + "version": "1.0.0", + "dependencies": { + "@fluentui/react-components": "^9.54.0", + "@fluentui/react-icons": "^2.0.245", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.5.2", + "vite": "^5.3.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/devtools": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz", + "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==", + "license": "MIT", + "peerDependencies": { + "@floating-ui/dom": "^1.0.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fluentui/keyboard-keys": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz", + "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/priority-overflow": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.2.1.tgz", + "integrity": "sha512-WH5dv54aEqWo/kKQuADAwjv66W6OUMFllQMjpdkrktQp7pu4JXtmF60iYcp9+iuIX9iCeW01j8gNTU08MQlfIQ==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react-accordion": { + "version": "9.8.14", + "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.8.14.tgz", + "integrity": "sha512-jTcfYDRUotRhUEjE1LeG1Qm10515CQUKxHWQhppBYhq7yAZcS5jOms5tMZHtHs0EQsWv3nMgUYYqoOqAsU0jDQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-alert": { + "version": "9.0.0-beta.129", + "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.129.tgz", + "integrity": "sha512-afS5Mvf9EH5je3ZOnF96GNaXL5nA/eI69AhO4nsbsvc1RaO/CkEt9+6iVyGy2zeqbQgpsP9UkNwEYyToQ1CrzA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.9.12", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-icons": "^2.0.239", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-aria": { + "version": "9.17.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.6.tgz", + "integrity": "sha512-O421keKMgf9BkHH15kTnKGFuCFKN3ukydJLEfSQJmOfdAHyJMzAul8/zMvkd4vmMr84+PtZUD1+Tylk4NvpN4g==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-utilities": "^9.25.4", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-avatar": { + "version": "9.9.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.9.12.tgz", + "integrity": "sha512-dlJ5mOKCDChMAECFhpcPHoQicA28ATWQXLtz26hAuVJH2/gC/6mZ0j7drIVl9YECqT/ZbZ3/hpVeZu/S/FVrOA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.4.11", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-popover": "^9.12.12", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-tooltip": "^9.8.11", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-badge": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.4.11.tgz", + "integrity": "sha512-u2gTg+QeD5uaieAwE89n8MLg2MyZN/kGMx3hJewFKtq3SzvU4xcgcna2Gp4UgpaA3pnGZsJjjjDIHwsv4EyO9Q==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-breadcrumb": { + "version": "9.3.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.3.12.tgz", + "integrity": "sha512-cT5xmYQbAYH7HslJu6O5WvSYzsBvaQ54Q6yIPgV5kCo5n3M6OSrJ0Ga6Zbfqid/GnY4G60FfjOvbfHNNhmx2Sw==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-link": "^9.7.0", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-button": { + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.6.12.tgz", + "integrity": "sha512-seI9L9O0fCHzlfKD/via1qqzaLFeiFKQeR1/97nXL06reC3DqLSCeiZP3UTxFljFE1CYZQRJfk1wH/D6j0ZCTA==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-card": { + "version": "9.5.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.5.6.tgz", + "integrity": "sha512-hCY6VWrKqq+y0yqUkqgkpTN5TVJSU5ZlKtZU+Sed+TlnKlojkS6cYRvsnWdAKwyFLJF9ZYTn+uos9Vi0wQyjtg==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-text": "^9.6.11", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-carousel": { + "version": "9.8.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.8.12.tgz", + "integrity": "sha512-Gjn6cd67FodcjfU2MQTBI2xjijzgy54TdQA8vxObZ27I6y9OHeDR07PWTqaCkX8mcBR8ilTxVD5bQ+zuqfb66g==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-tooltip": "^9.8.11", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1", + "embla-carousel": "^8.5.1", + "embla-carousel-autoplay": "^8.5.1", + "embla-carousel-fade": "^8.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-checkbox": { + "version": "9.5.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.5.11.tgz", + "integrity": "sha512-M8DTBQK0Z7+HKfRx4mjypH0fEagKK7YMNhGMy18aW3iYWeooA0ut81MzsRM5feqhl+Q8v4VJ0aN9qHNqshkD5g==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-color-picker": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.11.tgz", + "integrity": "sha512-L1ZKJAyioey3glmzMrpawUrzsdu/Nz0m6nVMOznJVuw0vu0BfQuMh/1/0QOoGYXFEbsc4+gSGSCnah4X0EJIsQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.3.4", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-combobox": { + "version": "9.16.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.16.12.tgz", + "integrity": "sha512-SimZpXzTGyDAGHQZmzUl9AsrIOlLDinTbvEwELEYh9X+yE33SZatcPwdpCmBXldBOs/eh+xOuNSOwgerJ3T3qQ==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-positioning": "^9.20.10", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-components": { + "version": "9.72.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.72.7.tgz", + "integrity": "sha512-tuC8ZMBQicF4p+f9MJv9cVYZUSktQVreAGJq/YJxQ0Ts1mO2rnAuIBkBFlgjnjyebDiAO1FoAAz/wW99hrIh6A==", + "license": "MIT", + "dependencies": { + "@fluentui/react-accordion": "^9.8.14", + "@fluentui/react-alert": "9.0.0-beta.129", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-avatar": "^9.9.12", + "@fluentui/react-badge": "^9.4.11", + "@fluentui/react-breadcrumb": "^9.3.12", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-card": "^9.5.6", + "@fluentui/react-carousel": "^9.8.12", + "@fluentui/react-checkbox": "^9.5.11", + "@fluentui/react-color-picker": "^9.2.11", + "@fluentui/react-combobox": "^9.16.12", + "@fluentui/react-dialog": "^9.16.3", + "@fluentui/react-divider": "^9.4.11", + "@fluentui/react-drawer": "^9.10.9", + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-image": "^9.3.11", + "@fluentui/react-infobutton": "9.0.0-beta.107", + "@fluentui/react-infolabel": "^9.4.12", + "@fluentui/react-input": "^9.7.11", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-link": "^9.7.0", + "@fluentui/react-list": "^9.6.6", + "@fluentui/react-menu": "^9.20.5", + "@fluentui/react-message-bar": "^9.6.14", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-nav": "^9.3.14", + "@fluentui/react-overflow": "^9.6.5", + "@fluentui/react-persona": "^9.5.12", + "@fluentui/react-popover": "^9.12.12", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-positioning": "^9.20.10", + "@fluentui/react-progress": "^9.4.11", + "@fluentui/react-provider": "^9.22.11", + "@fluentui/react-radio": "^9.5.11", + "@fluentui/react-rating": "^9.3.11", + "@fluentui/react-search": "^9.3.11", + "@fluentui/react-select": "^9.4.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-skeleton": "^9.4.11", + "@fluentui/react-slider": "^9.5.11", + "@fluentui/react-spinbutton": "^9.5.11", + "@fluentui/react-spinner": "^9.7.11", + "@fluentui/react-swatch-picker": "^9.4.11", + "@fluentui/react-switch": "^9.4.11", + "@fluentui/react-table": "^9.19.5", + "@fluentui/react-tabs": "^9.10.7", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-tag-picker": "^9.7.12", + "@fluentui/react-tags": "^9.7.12", + "@fluentui/react-teaching-popover": "^9.6.12", + "@fluentui/react-text": "^9.6.11", + "@fluentui/react-textarea": "^9.6.11", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-toast": "^9.7.9", + "@fluentui/react-toolbar": "^9.6.12", + "@fluentui/react-tooltip": "^9.8.11", + "@fluentui/react-tree": "^9.15.6", + "@fluentui/react-utilities": "^9.25.4", + "@fluentui/react-virtualizer": "9.0.0-alpha.107", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-context-selector": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.12.tgz", + "integrity": "sha512-4hj+rv+4Uwn9EeDyXD1YCEpVkm0iMLG403QAGd5vZZhcgB2tg/iazewKeTff+HMRkusx+lWBYzBEGcRohY/FiA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.25.4", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0", + "scheduler": ">=0.19.0" + } + }, + "node_modules/@fluentui/react-dialog": { + "version": "9.16.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.16.3.tgz", + "integrity": "sha512-aUnErTbSf2oqrqbQOCrjXp/12qHVfnxCR71/5hXJLME7BtYZ/m2lvs5r9MTjQSXBy8ar4G5jobS/+XJ0Lq3XqA==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-divider": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.4.11.tgz", + "integrity": "sha512-aESagOX6l7Ja9lb+3zJa6V5m1mjFnI4NEu8TccAu1VUlMZxX6flbMBJplgjN76dJjcHgs8uoa5xxxD74WNZBXg==", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-drawer": { + "version": "9.10.9", + "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.10.9.tgz", + "integrity": "sha512-tlHZBkILOHnA7Lg2v/vzmOvTNrPYJnPJAqiceuFlUZWncIWWAUfpw4Teh5V0wGNr6/yC/HjUD5xnynvIhr/ZuA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-dialog": "^9.16.3", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-field": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.4.11.tgz", + "integrity": "sha512-kF93G+LGEKaFJcEAUHJKZUc1xeV/q+JTygYVnEDkPbQ/4j+l+J3rVuHL8U7bhE+8cJG3wDP8jt4jqHsDgKyn5w==", + "license": "MIT", + "dependencies": { + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.315", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.315.tgz", + "integrity": "sha512-IITWAQGgU7I32eHPDHi+TUCUF6malP27wZLUV3bqjGVF/x/lfxvTIx8yqv/cxuwF3+ITGFDpl+278ZYJtOI7ww==", + "license": "MIT", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-image": { + "version": "9.3.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.3.11.tgz", + "integrity": "sha512-aLpz0/C6T0Uit6SmyhOJjYBvndZzfvmKv1vg+JRnE0aHS5jSUPoCLI6apxyMC6/LcqqTBklpqK3AD9kYpUxfqQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-infobutton": { + "version": "9.0.0-beta.107", + "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.107.tgz", + "integrity": "sha512-BcI4e+Oj1B/Qk4CMd0O9H0YF+IL4nhK8xuzI5bsZ5mdCaXiwIBgy5RyP8HVSq3y+Ml4XD2IRwufplcxF2cgTOA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.237", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-popover": "^9.12.12", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-infolabel": { + "version": "9.4.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.12.tgz", + "integrity": "sha512-inXlz5EAwQHKsGyB3wc5WmgQ1F9zc18x0HRd/otc2R7Oo1yRW5hXQCG5K5A9/wUge2pRiQVcBCsIurggmCNUhA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-popover": "^9.12.12", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-input": { + "version": "9.7.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.7.11.tgz", + "integrity": "sha512-ae/5ttJf25+J8akeEXpXRFqUAePPt2Moyfx4Tj0u7ZgG1U9IFbcBsshKEHAmIaygueXf6KdRyOduh1CF6a/D2w==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-jsx-runtime": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.3.3.tgz", + "integrity": "sha512-KOy85JqR6MSmp7OKUk/IPleaRlUSWF247AM2Ksu9uEKzDBQ2MO3sYUt8X9457GZjIuKLn5J2Vk127W/Khe+/Bg==", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.25.4", + "@swc/helpers": "^0.5.1", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "react": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-label": { + "version": "9.3.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.3.11.tgz", + "integrity": "sha512-9LORj4JQJCbp2J5ftW7ZjDxzD3Y4BkszX3Y7L1mK8DPRVAKOuGiakbH7U0q7ClGOMhCinWIQJjKAwzPZLo7xgA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-link": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.7.0.tgz", + "integrity": "sha512-NQ5Jhe5WBYfANSmIcl6fE/oBeh7G4iAq1FU9L/hyva5dxQ9OtiOpU5wxqVFLKEID/r144rhdtOZPL5AcAuJKdg==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-list": { + "version": "9.6.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.6.tgz", + "integrity": "sha512-t0ret56WXP86rDfnhuRrWg/DuS2zZkomB/Nu444rVygE8hsjPUTm5DXx7JKy+sGKVLyFbtsbXNMICkbxhGSSRA==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-checkbox": "^9.5.11", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-menu": { + "version": "9.20.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.20.5.tgz", + "integrity": "sha512-vshb/OXBZxvk+ghdmdVb2mJ/LJBYjlwpZRhWGJ8ZU0hmPTh74m5jTFWditSk8aL9oMvVuIo0MYLQyUJyJsFoqg==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-positioning": "^9.20.10", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-message-bar": { + "version": "9.6.14", + "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.14.tgz", + "integrity": "sha512-UR4Uvkx4VHQyS04T5ikf9gYOH52dloo1vjmK+pFKiqRzZhflHEXID9R1AZFuuZ572KUMXnxRlyEevpXnWqE70w==", + "license": "MIT", + "dependencies": { + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-link": "^9.7.0", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-motion": { + "version": "9.11.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.11.4.tgz", + "integrity": "sha512-rLxz6DSAtp3O+W+mJnov2qXtvZkIgcC1BQOAyUH6tl6u2YmsC1/zRKWhVsf/WUgZwqu3G4jlq15ptyuCITAcDA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-utilities": "^9.25.4", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-motion-components-preview": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.14.1.tgz", + "integrity": "sha512-+2MK7d2g3mD+6Z3o9/fitO+V4u5OKGeRUoUjwlU1v56JHP43hj+NCJynoe4Cym8FeSwTyipks6hvdqBF4W+jtw==", + "license": "MIT", + "dependencies": { + "@fluentui/react-motion": "*", + "@fluentui/react-utilities": "*", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-nav": { + "version": "9.3.14", + "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.14.tgz", + "integrity": "sha512-0Lylul5g/9y3Cay5qHLtzW4SB9kdkTmvjHSffPJZDKE/Wv7GBbDypBxoB+f2L1K4f0qzRJ1NvIiwatH4hAsUDA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-divider": "^9.4.11", + "@fluentui/react-drawer": "^9.10.9", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-tooltip": "^9.8.11", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-overflow": { + "version": "9.6.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.6.5.tgz", + "integrity": "sha512-4MlXASDodkwk4QWhUPLgMbUPwDYAOAWDnQGJz4q6Hs9eZvx83dSpWdWjkmQ6mwjYf2HwooMkqsjR/kAFvg+ipg==", + "license": "MIT", + "dependencies": { + "@fluentui/priority-overflow": "^9.2.1", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-persona": { + "version": "9.5.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.5.12.tgz", + "integrity": "sha512-ja3t1o6XDJWCJnOVDTM48G7bFPAbNxcsQKwAPfiuROVu8ODbTQefutCHl0Hno40AsftQk6N4zGbKcn7BYSZ09Q==", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.9.12", + "@fluentui/react-badge": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-popover": { + "version": "9.12.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.12.12.tgz", + "integrity": "sha512-IytuasB4b4lCnEhFC0OC66a3mzBSePLpg78/BceKYepuG7IC6iGuCwYartqSQCSUlSU12rT02/V0rqCO81f4Nw==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-positioning": "^9.20.10", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-portal": { + "version": "9.8.8", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.8.tgz", + "integrity": "sha512-RVvhWYfcwIUYXiokgFw3oxb7Q6xox2e7jcsgFtheDm2X/BHT6WJigW4OaCjOkvugkBEYQkwgIpL9iS2QG3HMFA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-positioning": { + "version": "9.20.10", + "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.20.10.tgz", + "integrity": "sha512-mjuiqh+urV5SzAP2NfzUzsvtWut0aNcO9m/jIuz374iTVGRfDNeVIl7aPI4yK5sdCDR6dGALiNMTFHpjz1YXyw==", + "license": "MIT", + "dependencies": { + "@floating-ui/devtools": "^0.2.3", + "@floating-ui/dom": "^1.6.12", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-progress": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.4.11.tgz", + "integrity": "sha512-L0Yh2D0vLPJX0jYfc9VHf8c/idW+e/oRxYNXfrTrvtW1bX80bAmrXWgdRPr/VEtvbJh//2ol2TRmTTQsn2ECNQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-provider": { + "version": "9.22.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.11.tgz", + "integrity": "sha512-XrinA7DVEqsPHeN9XljwTENiIQypiF9cmDYXHN9Emsz6Od4hnmsbt4pnR4Xsf+GcSxVtxkIImfgwtS0aENzYbQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/core": "^1.16.0", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-radio": { + "version": "9.5.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.5.11.tgz", + "integrity": "sha512-tMxCcqRSSYqYr6hy1dKkzS6LymRc8wM089vr4eBLPQCGCvi3OCd6P7XH8aIcXnzxE3+v03Gs7E/wbzi2CXN6gA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-rating": { + "version": "9.3.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.3.11.tgz", + "integrity": "sha512-9Bl/sESNbFTbz8peGt9vxLxHDO0AWvS12oMiQ80S1GQOt1ua4S9/SKC83OvyVLEdpBDpBkXTelNz5whczcWexQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-search": { + "version": "9.3.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.3.11.tgz", + "integrity": "sha512-inLoPgbGnupfwhBxFS59mF/ThsntusfYp9TaaTB3SJmqfEEx6YXi5soxszzrXsNvrqpgEoCGIduRpEICuUz5pw==", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-input": "^9.7.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-select": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.4.11.tgz", + "integrity": "sha512-/mcdl/lkKccT+GKXu22y2/ANeLhFNUdjkOX+0rvBdl3u49xkqS9Y4Bi0zM1EhhTV2jE8+yjMjzPDzfzJaXVK1A==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-shared-contexts": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.0.tgz", + "integrity": "sha512-r52B+LUevs930pe45pFsppM9XNvY+ojgRgnDE+T/6aiwR/Mo4YoGrtjhLEzlQBeTGuySICTeaAiXfuH6Keo5Dg==", + "license": "MIT", + "dependencies": { + "@fluentui/react-theme": "^9.2.0", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "react": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-skeleton": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.4.11.tgz", + "integrity": "sha512-nw6NlTBXS7lNSxsebLuADYQi9gJ83jFBFsFq+AGIpAoZLBOCHOhk8/XwV3vYtPwVrKcZtOtXqh9NdCqTR3OAIA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-slider": { + "version": "9.5.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.5.11.tgz", + "integrity": "sha512-kxZKklJbcG/521muQaIDMdcftoClbwV7yMOcu8PMG+VXsaIuoandoBleBYdzM2XdpY62iK6vUPAMZWBZh3B5Ng==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-spinbutton": { + "version": "9.5.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.5.11.tgz", + "integrity": "sha512-pYR3RkJfks+0WV47KoDKD04D0pTHtT+lu3AeOpBlIswxtsb1gZEDmTrEHHNeLDKKVhWMWNoEPlxfXuX9tOh7yA==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-spinner": { + "version": "9.7.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.7.11.tgz", + "integrity": "sha512-MhmAisICa3BzBNQH9CnLI5NVPTTXFo1Yaey8kbQPU+gVVF4vIGORB7M1MXSHFxZvojtFpBixiVHqRwh9mowJww==", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-swatch-picker": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.4.11.tgz", + "integrity": "sha512-M/ZfHqo63F69y2ymEQDDN/BZuI3afeW3U+omyGZZoHts3rVCjPk6sKFemTRpUhGgGfxBdexWEPithZx3dk0IPA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-switch": { + "version": "9.4.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.4.11.tgz", + "integrity": "sha512-/WDcoVFQ3I2fe5FTINfyVTIW6wuTgM5QkJgcwbU7HTANq/+wJ2f8wzywoI4x16cJOckBdy+ByDpW7uJ/Uvs8RA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-label": "^9.3.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-table": { + "version": "9.19.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.5.tgz", + "integrity": "sha512-In9egEdytjFd6N1RBZd5+3UgdXvEVDP7rz+/I79J10ui2+Nb7r9ah68m5CQB15AKA8F5XFDWPEGvGG3Tmuq4Jg==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-avatar": "^9.9.12", + "@fluentui/react-checkbox": "^9.5.11", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-radio": "^9.5.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-tabs": { + "version": "9.10.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.10.7.tgz", + "integrity": "sha512-Kfq6GxZXEKsMdGKmHWNMcEYOYHxl5+fXJOH6ZRgeR2FkHUsPUUe2BHaFnOMRSvCwzECvhOMYs+Ekqt7JzW3BWQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-tabster": { + "version": "9.26.10", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.10.tgz", + "integrity": "sha512-KrddtwbnbgYVAnOkx1pQsMMgq7Kfi+lMRrUrDDJ9Y5X6wiXiajbWRRxYgKiOJc3MpeDCaTCEtjOWNG92vcinMw==", + "license": "MIT", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1", + "keyborg": "^2.6.0", + "tabster": "^8.5.5" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-tag-picker": { + "version": "9.7.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.7.12.tgz", + "integrity": "sha512-OJucCDub6b3ceGL6v2UXL+SD3x6nJMbmJ70v38BmrA9t3fNcDvn6RnsfHhF2O0pRGGUOrXbK7vDwVhUAG4Py8w==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-combobox": "^9.16.12", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-positioning": "^9.20.10", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-tags": "^9.7.12", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-tags": { + "version": "9.7.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.7.12.tgz", + "integrity": "sha512-G7pxP0GGa6J/7mYvB9ycOmD9Jpm6ByUz6JsJI4OBL9UnhenUVTtE7ZKJ9GJ0SiG0GVxS152aSlOR7NLHV7mCqw==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-avatar": "^9.9.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-teaching-popover": { + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.12.tgz", + "integrity": "sha512-Ugo5SQ3yzSlxUWkeeEdumTWTw662KDh3UPc6RGhU0Jq13skpmsClSJL678BZwsYdAaJXvvG9Bi4PjPeezeB/SA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-popover": "^9.12.12", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-text": { + "version": "9.6.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.11.tgz", + "integrity": "sha512-U7EiCesOWjkALf7LM6sy+yvE59Px3c6f27jg4aa21UMo61HCVNbjKV8Lz6GzEftEvv++/EZ25yZBiQcKgh/5iA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-textarea": { + "version": "9.6.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.6.11.tgz", + "integrity": "sha512-5ds8u8hzSqj8cOy0e7HJWjUMq1aO0MIJiaNt/SyIxoZFvsklj/2yaMRVXpWxr3GvX5bzScvFoBY53gPdLKtE/g==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-theme": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.0.tgz", + "integrity": "sha512-Q0zp/MY1m5RjlkcwMcjn/PQRT2T+q3bgxuxWbhgaD07V+tLzBhGROvuqbsdg4YWF/IK21zPfLhmGyifhEu0DnQ==", + "license": "MIT", + "dependencies": { + "@fluentui/tokens": "1.0.0-alpha.22", + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react-toast": { + "version": "9.7.9", + "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.9.tgz", + "integrity": "sha512-PaFh2CwVK4tgvRzBMb46ODHsB+ZYSYE8mx735vqgIG8Oj1AL3wZ5Y9TrjJGxn/lppZgtnwLgt4GQ+GI7MM+e+g==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-toolbar": { + "version": "9.6.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.6.12.tgz", + "integrity": "sha512-AuOZvp6Jcc/Sngk0OddTsHlJVU/u9mVEw6JDhsCYiwKeq04kdgfco1sjSTGjDhJbf1SnkhmyR6YN16SrpVQWtA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-divider": "^9.4.11", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-radio": "^9.5.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-tooltip": { + "version": "9.8.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.8.11.tgz", + "integrity": "sha512-ke7Hbom3dtC3f9QjJG/F7QfNfukwTtAhoYLmwwQnXYTh/CIVxoC2rVh4c/V8jUD0lnjNPBZZ5ttVUopWljHuFg==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-portal": "^9.8.8", + "@fluentui/react-positioning": "^9.20.10", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-tree": { + "version": "9.15.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.6.tgz", + "integrity": "sha512-L/uc+SgwXW8DXgSZsyIg5tQkixfrGllANg0I2578WRlfOkERehkg1eSW8Uib/Mbk+W3tB0I8CL20ifoSTL7Ztw==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-aria": "^9.17.6", + "@fluentui/react-avatar": "^9.9.12", + "@fluentui/react-button": "^9.6.12", + "@fluentui/react-checkbox": "^9.5.11", + "@fluentui/react-context-selector": "^9.2.12", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-motion": "^9.11.4", + "@fluentui/react-motion-components-preview": "^0.14.1", + "@fluentui/react-radio": "^9.5.11", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-tabster": "^9.26.10", + "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-utilities": { + "version": "9.25.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.25.4.tgz", + "integrity": "sha512-vvEIFTfqkcBnKNJhlm8csdGNtOWDWDkqAM4tGlW7jLlFrhNkOfDsqdNuBElENPNJ1foHyVTF5ZSr20kVoKWPjQ==", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.8", + "@fluentui/react-shared-contexts": "^9.26.0", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "react": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/react-virtualizer": { + "version": "9.0.0-alpha.107", + "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.107.tgz", + "integrity": "sha512-zpTVzJB2BUNv7QdTUlLSBMCbt/EfALRuls/u/8FYaO4PGOFVeS3equytyxSOizz9zJZVhm8sjdp326DEQNiaPA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.3.3", + "@fluentui/react-shared-contexts": "^9.26.0", + "@fluentui/react-utilities": "^9.25.4", + "@griffel/react": "^1.5.32", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + } + }, + "node_modules/@fluentui/tokens": { + "version": "1.0.0-alpha.22", + "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.22.tgz", + "integrity": "sha512-i9fgYyyCWFRdUi+vQwnV6hp7wpLGK4p09B+O/f2u71GBXzPuniubPYvrIJYtl444DD6shLjYToJhQ1S6XTFwLg==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@griffel/core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.19.2.tgz", + "integrity": "sha512-WkB/QQkjy9dE4vrNYGhQvRRUHFkYVOuaznVOMNTDT4pS9aTJ9XPrMTXXlkpcwaf0D3vNKoerj4zAwnU2lBzbOg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.3.0", + "csstype": "^3.1.3", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.32", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.32.tgz", + "integrity": "sha512-jN3SmSwAUcWFUQuQ9jlhqZ5ELtKY21foaUR0q1mJtiAeSErVgjkpKJyMLRYpvaFGWrDql0Uz23nXUogXbsS2wQ==", + "license": "MIT", + "dependencies": { + "@griffel/core": "^1.19.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.3.0.tgz", + "integrity": "sha512-bHwD3sUE84Xwv4dH011gOKe1jul77M1S6ZFN9Tnq8pvZ48UMdY//vtES6fv7GRS5wXYT4iqxQPBluAiYAfkpmw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", + "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.261", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.261.tgz", + "integrity": "sha512-cmyHEWFqEt3ICUNF93ShneOF47DHoSDbLb7E/AonsWcbzg95N+kPXeLNfkdzgTT/vEUcoW76fxbLBkeYtfoM8A==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-autoplay": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz", + "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/embla-carousel-fade": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-fade/-/embla-carousel-fade-8.6.0.tgz", + "integrity": "sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyborg": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz", + "integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==", + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tabster": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.5.6.tgz", + "integrity": "sha512-2vfrRGrx8O9BjdrtSlVA5fvpmbq5HQBRN13XFRg6LAvZ1Fr3QdBnswgT4YgFS5Bhoo5nxwgjRaRueI2Us/dv7g==", + "license": "MIT", + "dependencies": { + "keyborg": "2.6.0", + "tslib": "^2.8.1" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.40.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/content-gen/src/frontend/package.json b/content-gen/src/frontend/package.json new file mode 100644 index 000000000..bc10996b4 --- /dev/null +++ b/content-gen/src/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "content-generation-frontend", + "version": "1.0.0", + "description": "Frontend for Intelligent Content Generation Accelerator", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "@fluentui/react-components": "^9.54.0", + "@fluentui/react-icons": "^2.0.245", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^9.0.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.5.2", + "vite": "^5.3.2" + } +} diff --git a/content-gen/src/frontend/src/App.tsx b/content-gen/src/frontend/src/App.tsx new file mode 100644 index 000000000..18382f221 --- /dev/null +++ b/content-gen/src/frontend/src/App.tsx @@ -0,0 +1,303 @@ +import { useState, useCallback } from 'react'; +import { + Title1, + Subtitle1, + Divider, + tokens, +} from '@fluentui/react-components'; +import { + Sparkle24Regular, +} from '@fluentui/react-icons'; +import { v4 as uuidv4 } from 'uuid'; + +import { ChatPanel } from './components/ChatPanel'; +import { BriefConfirmation } from './components/BriefConfirmation'; +import { ContentPreview } from './components/ContentPreview'; +import { ProductSelector } from './components/ProductSelector'; +import type { ChatMessage, CreativeBrief, Product, GeneratedContent } from './types'; + +function App() { + const [conversationId] = useState(() => uuidv4()); + const [userId] = useState('demo-user'); + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + // Brief confirmation flow + const [pendingBrief, setPendingBrief] = useState(null); + const [confirmedBrief, setConfirmedBrief] = useState(null); + + // Product selection + const [selectedProducts, setSelectedProducts] = useState([]); + + // Generated content + const [generatedContent, setGeneratedContent] = useState(null); + + const handleSendMessage = useCallback(async (content: string) => { + const userMessage: ChatMessage = { + id: uuidv4(), + role: 'user', + content, + timestamp: new Date().toISOString(), + }; + + setMessages(prev => [...prev, userMessage]); + setIsLoading(true); + + try { + // Import dynamically to avoid SSR issues + const { streamChat, parseBrief } = await import('./api'); + + // Check if this looks like a creative brief + const briefKeywords = ['campaign', 'marketing', 'target audience', 'objective', 'deliverable']; + const isBriefLike = briefKeywords.some(kw => content.toLowerCase().includes(kw)); + + if (isBriefLike && !confirmedBrief) { + // Parse as a creative brief + const parsed = await parseBrief(content); + setPendingBrief(parsed.brief); + + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'I\'ve parsed your creative brief. Please review and confirm the details before we proceed.', + agent: 'PlanningAgent', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } else { + // Stream chat response + let fullContent = ''; + let currentAgent = ''; + let messageAdded = false; + + for await (const response of streamChat(content, conversationId, userId)) { + if (response.type === 'agent_response') { + fullContent = response.content; + currentAgent = response.agent || ''; + + // Add message when final OR when requiring user input (interactive response) + if ((response.is_final || response.requires_user_input) && !messageAdded) { + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: fullContent, + agent: currentAgent, + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + messageAdded = true; + } + } else if (response.type === 'error') { + // Handle error responses + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: response.content || 'An error occurred while processing your request.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, errorMessage]); + messageAdded = true; + } + } + } + } catch (error) { + console.error('Error sending message:', error); + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Sorry, there was an error processing your request. Please try again.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }, [conversationId, userId, confirmedBrief]); + + const handleBriefConfirm = useCallback(async (brief: CreativeBrief) => { + try { + const { confirmBrief } = await import('./api'); + await confirmBrief(brief, conversationId, userId); + setConfirmedBrief(brief); + setPendingBrief(null); + + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Great! Your creative brief has been confirmed. Now you can select products to feature and generate content.', + agent: 'TriageAgent', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } catch (error) { + console.error('Error confirming brief:', error); + } + }, [conversationId, userId]); + + const handleBriefCancel = useCallback(() => { + setPendingBrief(null); + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'No problem. Please provide your creative brief again or ask me any questions.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + }, []); + + const handleGenerateContent = useCallback(async () => { + if (!confirmedBrief) return; + + setIsLoading(true); + try { + const { streamGenerateContent } = await import('./api'); + + for await (const response of streamGenerateContent( + confirmedBrief, + selectedProducts, + true, + conversationId + )) { + if (response.is_final && response.type !== 'error') { + try { + const rawContent = JSON.parse(response.content); + + // Parse text_content if it's a string (from orchestrator) + let textContent = rawContent.text_content; + if (typeof textContent === 'string') { + try { + textContent = JSON.parse(textContent); + } catch { + // Keep as string if not valid JSON + } + } + + // Build image_url: prefer blob URL, fallback to base64 data URL + let imageUrl: string | undefined; + if (rawContent.image_url) { + imageUrl = rawContent.image_url; + } else if (rawContent.image_base64) { + imageUrl = `data:image/png;base64,${rawContent.image_base64}`; + } + + const content: GeneratedContent = { + text_content: typeof textContent === 'object' ? { + headline: textContent?.headline, + body: textContent?.body, + cta_text: textContent?.cta, + tagline: textContent?.tagline, + } : undefined, + image_content: (imageUrl || rawContent.image_prompt) ? { + image_url: imageUrl, + prompt_used: rawContent.image_prompt, + alt_text: rawContent.image_revised_prompt || 'Generated marketing image', + } : undefined, + violations: rawContent.violations || [], + requires_modification: rawContent.requires_modification || false, + }; + setGeneratedContent(content); + + // Add a message to chat showing content was generated + const assistantMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: `Content generated successfully! ${textContent?.headline ? `Headline: "${textContent.headline}"` : ''}`, + agent: 'ContentAgent', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, assistantMessage]); + } catch (parseError) { + console.error('Error parsing generated content:', parseError); + } + } else if (response.type === 'error') { + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: `Error generating content: ${response.content}`, + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, errorMessage]); + } + } + } catch (error) { + console.error('Error generating content:', error); + const errorMessage: ChatMessage = { + id: uuidv4(), + role: 'assistant', + content: 'Sorry, there was an error generating content. Please try again.', + timestamp: new Date().toISOString(), + }; + setMessages(prev => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }, [confirmedBrief, selectedProducts, conversationId]); + + return ( +
+ {/* Header */} +
+ +
+ Content Generation Accelerator + + AI-powered marketing content creation + +
+
+ + + + {/* Main Content */} +
+ {/* Chat Panel */} +
+ +
+ + {/* Sidebar */} +
+ {/* Brief Confirmation */} + {pendingBrief && ( + + )} + + {/* Product Selector */} + {confirmedBrief && !generatedContent && ( + + )} + + {/* Generated Content Preview */} + {generatedContent && ( + + )} +
+
+
+ ); +} + +export default App; diff --git a/content-gen/src/frontend/src/api/index.ts b/content-gen/src/frontend/src/api/index.ts new file mode 100644 index 000000000..f00227fde --- /dev/null +++ b/content-gen/src/frontend/src/api/index.ts @@ -0,0 +1,277 @@ +/** + * API service for interacting with the Content Generation backend + */ + +import type { + CreativeBrief, + Product, + ChatMessage, + AgentResponse, + BrandGuidelines, + ParsedBriefResponse, + Conversation, +} from '../types'; + +const API_BASE = '/api'; + +/** + * Parse a free-text creative brief into structured format + */ +export async function parseBrief(briefText: string): Promise { + const response = await fetch(`${API_BASE}/brief/parse`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ brief_text: briefText }), + }); + + if (!response.ok) { + throw new Error(`Failed to parse brief: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Confirm a parsed creative brief + */ +export async function confirmBrief( + brief: CreativeBrief, + conversationId?: string, + userId?: string +): Promise<{ status: string; conversation_id: string; brief: CreativeBrief }> { + const response = await fetch(`${API_BASE}/brief/confirm`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + brief, + conversation_id: conversationId, + user_id: userId || 'anonymous', + }), + }); + + if (!response.ok) { + throw new Error(`Failed to confirm brief: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Stream chat messages from the agent orchestration + */ +export async function* streamChat( + message: string, + conversationId?: string, + userId?: string +): AsyncGenerator { + const response = await fetch(`${API_BASE}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message, + conversation_id: conversationId, + user_id: userId || 'anonymous', + }), + }); + + if (!response.ok) { + throw new Error(`Chat request failed: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + return; + } + try { + yield JSON.parse(data) as AgentResponse; + } catch { + console.error('Failed to parse SSE data:', data); + } + } + } + } +} + +/** + * Generate content from a confirmed brief + */ +export async function* streamGenerateContent( + brief: CreativeBrief, + products?: Product[], + generateImages: boolean = true, + conversationId?: string +): AsyncGenerator { + const response = await fetch(`${API_BASE}/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + brief, + products: products || [], + generate_images: generateImages, + conversation_id: conversationId, + }), + }); + + if (!response.ok) { + throw new Error(`Content generation failed: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + return; + } + try { + yield JSON.parse(data) as AgentResponse; + } catch { + console.error('Failed to parse SSE data:', data); + } + } + } + } +} + +/** + * Get products from the catalog + */ +export async function getProducts(params?: { + category?: string; + sub_category?: string; + search?: string; + limit?: number; +}): Promise<{ products: Product[]; count: number }> { + const searchParams = new URLSearchParams(); + if (params?.category) searchParams.set('category', params.category); + if (params?.sub_category) searchParams.set('sub_category', params.sub_category); + if (params?.search) searchParams.set('search', params.search); + if (params?.limit) searchParams.set('limit', params.limit.toString()); + + const response = await fetch(`${API_BASE}/products?${searchParams}`); + + if (!response.ok) { + throw new Error(`Failed to get products: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Get a single product by SKU + */ +export async function getProduct(sku: string): Promise { + const response = await fetch(`${API_BASE}/products/${sku}`); + + if (!response.ok) { + throw new Error(`Failed to get product: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Upload a product image + */ +export async function uploadProductImage( + sku: string, + file: File +): Promise<{ image_url: string; image_description: string }> { + const formData = new FormData(); + formData.append('image', file); + + const response = await fetch(`${API_BASE}/products/${sku}/image`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`Failed to upload image: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Get brand guidelines + */ +export async function getBrandGuidelines(): Promise { + const response = await fetch(`${API_BASE}/brand-guidelines`); + + if (!response.ok) { + throw new Error(`Failed to get brand guidelines: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Get user conversations + */ +export async function getConversations( + userId: string, + limit?: number +): Promise<{ conversations: Conversation[]; count: number }> { + const searchParams = new URLSearchParams(); + searchParams.set('user_id', userId); + if (limit) searchParams.set('limit', limit.toString()); + + const response = await fetch(`${API_BASE}/conversations?${searchParams}`); + + if (!response.ok) { + throw new Error(`Failed to get conversations: ${response.statusText}`); + } + + return response.json(); +} + +/** + * Get a specific conversation + */ +export async function getConversation( + conversationId: string, + userId: string +): Promise { + const response = await fetch( + `${API_BASE}/conversations/${conversationId}?user_id=${userId}` + ); + + if (!response.ok) { + throw new Error(`Failed to get conversation: ${response.statusText}`); + } + + return response.json(); +} diff --git a/content-gen/src/frontend/src/components/BriefConfirmation.tsx b/content-gen/src/frontend/src/components/BriefConfirmation.tsx new file mode 100644 index 000000000..b9c2078b0 --- /dev/null +++ b/content-gen/src/frontend/src/components/BriefConfirmation.tsx @@ -0,0 +1,154 @@ +import { useState } from 'react'; +import { + Card, + CardHeader, + Button, + Input, + Textarea, + Text, + Title3, + tokens, +} from '@fluentui/react-components'; +import { + Checkmark24Regular, + Dismiss24Regular, + Edit24Regular, +} from '@fluentui/react-icons'; +import type { CreativeBrief } from '../types'; + +interface BriefConfirmationProps { + brief: CreativeBrief; + onConfirm: (brief: CreativeBrief) => void; + onCancel: () => void; + onEdit: (brief: CreativeBrief) => void; +} + +export function BriefConfirmation({ + brief, + onConfirm, + onCancel, + onEdit, +}: BriefConfirmationProps) { + const [isEditing, setIsEditing] = useState(false); + const [editedBrief, setEditedBrief] = useState(brief); + + const handleFieldChange = (field: keyof CreativeBrief, value: string) => { + setEditedBrief(prev => ({ ...prev, [field]: value })); + }; + + const handleSaveEdit = () => { + onEdit(editedBrief); + setIsEditing(false); + }; + + const briefFields: { key: keyof CreativeBrief; label: string; multiline?: boolean }[] = [ + { key: 'overview', label: 'Overview', multiline: true }, + { key: 'objectives', label: 'Objectives', multiline: true }, + { key: 'target_audience', label: 'Target Audience' }, + { key: 'key_message', label: 'Key Message', multiline: true }, + { key: 'tone_and_style', label: 'Tone & Style' }, + { key: 'deliverable', label: 'Deliverable' }, + { key: 'timelines', label: 'Timelines' }, + { key: 'visual_guidelines', label: 'Visual Guidelines', multiline: true }, + { key: 'cta', label: 'Call to Action' }, + ]; + + return ( + + Confirm Creative Brief} + action={ + !isEditing ? ( + + ) : undefined + } + /> + + + Please review the parsed brief and confirm or edit before proceeding. + + +
+ {briefFields.map(({ key, label, multiline }) => ( +
+ + {label} + + {isEditing ? ( + multiline ? ( +