@atmem/architect 0.0.0-stage → 0.2.0-alpha.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONSTITUTION.md +144 -0
- package/README.md +33 -2
- package/agent-skills/discovery/SKILL.md +48 -0
- package/agent-skills/discovery/topics.json +139 -0
- package/agent-skills/generation-capabilities.json +760 -0
- package/agent-skills/service-lifecycle.json +21 -0
- package/agent-skills/specialists/architecture/SKILL.md +15 -0
- package/agent-skills/specialists/aws/SKILL.md +17 -0
- package/agent-skills/specialists/azure/SKILL.md +7 -0
- package/agent-skills/specialists/composition/SKILL.md +17 -0
- package/agent-skills/specialists/devops/SKILL.md +10 -0
- package/agent-skills/specialists/finops/SKILL.md +7 -0
- package/agent-skills/specialists/gcp/SKILL.md +7 -0
- package/agent-skills/specialists/ibm/SKILL.md +7 -0
- package/agent-skills/specialists/manifest.json +13 -0
- package/agent-skills/specialists/pulumi/SKILL.md +7 -0
- package/agent-skills/specialists/security/SKILL.md +7 -0
- package/agent-skills/specialists/terraform/SKILL.md +14 -0
- package/agent-skills/upstream/aws/LICENSE +175 -0
- package/agent-skills/upstream/aws/SKILL.md +125 -0
- package/agent-skills/upstream/azure/LICENSE +21 -0
- package/agent-skills/upstream/azure/SKILL.md +387 -0
- package/agent-skills/upstream/gcp/LICENSE +202 -0
- package/agent-skills/upstream/gcp/SKILL.md +159 -0
- package/agent-skills/upstream/manifest.json +57 -0
- package/agent-skills/upstream/pulumi/LICENSE +201 -0
- package/agent-skills/upstream/pulumi/SKILL.md +501 -0
- package/agent-skills/upstream/terraform/LICENSE +373 -0
- package/agent-skills/upstream/terraform/SKILL.md +316 -0
- package/agent-skills/workflows/workflows.json +601 -0
- package/cli/architect.mjs +3 -0
- package/cli/runtime.mjs +115 -0
- package/dist/assets/html2canvas.esm-QH1iLAAe.js +22 -0
- package/dist/assets/index-B97MkVMs.css +1 -0
- package/dist/assets/index-qtCM2D7O.js +21271 -0
- package/dist/assets/index.es-C6C0sneB.js +18 -0
- package/dist/assets/purify.es-CAi2OJFm.js +3 -0
- package/dist/assets/visual-export-DjbjgQbA.js +173 -0
- package/dist/favicon.svg +2 -0
- package/dist/fonts/DM-Sans-OFL.txt +93 -0
- package/dist/fonts/Manrope-OFL.txt +93 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/index.html +4 -0
- package/dist/licenses/aws-architect-LICENSE.txt +17 -0
- package/dist/licenses/aws-mcp-LICENSE.txt +175 -0
- package/dist/licenses/aws-mcp-NOTICE.txt +1 -0
- package/dist/licenses/cloud-emulation-notices.txt +36619 -0
- package/dist/licenses/dependency-notices.txt +6379 -0
- package/dist/licenses/emulation-notices.txt +717 -0
- package/dist/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/dist/licenses/memory-dependency-notices.txt +3319 -0
- package/dist/licenses/openarchflow-LICENSE.txt +21 -0
- package/dist/licenses/python-dependency-notices.txt +11951 -0
- package/docs/upstream-lock.json +26 -0
- package/knowledge/engineering-practices.json +536 -0
- package/package.json +30 -4
- package/packaging/uv-release-lock.json +54 -0
- package/public/licenses/aws-architect-LICENSE.txt +17 -0
- package/public/licenses/aws-mcp-LICENSE.txt +175 -0
- package/public/licenses/aws-mcp-NOTICE.txt +1 -0
- package/public/licenses/cloud-emulation-notices.txt +36619 -0
- package/public/licenses/dependency-notices.txt +6379 -0
- package/public/licenses/emulation-notices.txt +717 -0
- package/public/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/public/licenses/memory-dependency-notices.txt +3319 -0
- package/public/licenses/openarchflow-LICENSE.txt +21 -0
- package/public/licenses/python-dependency-notices.txt +11951 -0
- package/scripts/terraform-render.mjs +8 -0
- package/server/__init__.py +0 -0
- package/server/abstraction.py +28 -0
- package/server/agent_runtime.py +153 -0
- package/server/app.py +257 -0
- package/server/architect.py +112 -0
- package/server/architecture_activity.py +67 -0
- package/server/attachments.py +101 -0
- package/server/cloud-emulation-requirements.txt +29 -0
- package/server/codex_app_server.py +219 -0
- package/server/codex_engineering_backend.py +266 -0
- package/server/codex_worker_policy.py +170 -0
- package/server/contracts.py +315 -0
- package/server/copilot_worker.py +16 -0
- package/server/dependency_mirror.py +54 -0
- package/server/design_patch.py +43 -0
- package/server/emulation-requirements.txt +8 -0
- package/server/emulation_adapters.py +20 -0
- package/server/emulation_routes.py +98 -0
- package/server/emulation_worker.py +351 -0
- package/server/engineering_artifacts.py +215 -0
- package/server/engineering_assets/application-runtime.json +4372 -0
- package/server/engineering_assets/architecture_work.schema.json +79 -0
- package/server/engineering_assets/materialize_runtime.py +62 -0
- package/server/engineering_assets/prepare_tools.py +96 -0
- package/server/engineering_assets/sql_validation.py +59 -0
- package/server/engineering_contexts.py +120 -0
- package/server/engineering_delivery.py +210 -0
- package/server/engineering_finalization.py +210 -0
- package/server/engineering_handoff.py +163 -0
- package/server/engineering_jobs.py +206 -0
- package/server/engineering_knowledge.py +75 -0
- package/server/engineering_reconciliation.py +159 -0
- package/server/engineering_routes.py +469 -0
- package/server/engineering_sessions.py +273 -0
- package/server/engineering_tools.py +166 -0
- package/server/generation_lint.py +18 -0
- package/server/local_providers.py +181 -0
- package/server/local_validation.py +236 -0
- package/server/memory-requirements.lock +181 -0
- package/server/memory-requirements.txt +6 -0
- package/server/memory_routes.py +82 -0
- package/server/memory_service.py +273 -0
- package/server/memory_worker.py +66 -0
- package/server/openai_voice.py +80 -0
- package/server/packaged_app.py +9 -0
- package/server/pricing.py +84 -0
- package/server/process_control_observation.py +31 -0
- package/server/prompts.py +123 -0
- package/server/public_faq_emulation_worker.py +44 -0
- package/server/requirements.lock +1618 -0
- package/server/requirements.txt +68 -0
- package/server/runtime_dependency_locks.py +27 -0
- package/server/runtime_evidence.py +93 -0
- package/server/settings.py +14 -0
- package/server/skills.py +103 -0
- package/server/storage_emulation_worker.py +160 -0
- package/server/structured_emulation_worker.py +48 -0
- package/server/terraform_routes.py +118 -0
- package/server/upstream/__init__.py +0 -0
- package/server/upstream/agent/__init__.py +1 -0
- package/server/upstream/agent/aws_architect.py +600 -0
- package/server/upstream/agent/config/__init__.py +1 -0
- package/server/upstream/agent/config/agent_config.py +82 -0
- package/server/upstream/agent/config/prompts.py +138 -0
- package/server/upstream/agent/rate_limiter.py +62 -0
- package/server/upstream/utils/__init__.py +1 -0
- package/server/upstream/utils/file_writer.py +43 -0
- package/server/validation_applicability.py +85 -0
- package/server/validation_jobs.py +252 -0
- package/server/validation_workers.py +48 -0
- package/server/voice_routes.py +91 -0
- package/server/voice_worker.py +26 -0
- package/specs/028-convergence-evaluation/evidence/20261110-ibm-invoice-native/registry-structured/qualification.json +72 -0
- package/specs/028-convergence-evaluation/evidence/20261110-invoice-runtime/durable-current/receipt.json +61 -0
- package/specs/029-general-purpose-engine/evidence/20261111-runtime-dependencies/sdk/receipt.json +22 -0
- package/src/ArchitectureActivity.jsx +54 -0
- package/src/ArchitectureWorkPanel.jsx +46 -0
- package/src/ArtifactChecks.jsx +49 -0
- package/src/BlockRequirements.jsx +13 -0
- package/src/BrainActivity.jsx +11 -0
- package/src/ChatMessage.jsx +19 -0
- package/src/ChatResizeHandle.jsx +6 -0
- package/src/CodePreview.jsx +8 -0
- package/src/ComponentReview.jsx +31 -0
- package/src/DeliveryViews.jsx +59 -0
- package/src/EmulationPanel.jsx +44 -0
- package/src/EngagementViews.jsx +385 -0
- package/src/EngineeringPanel.jsx +151 -0
- package/src/ImplementationDelivery.jsx +55 -0
- package/src/JourneyViews.jsx +400 -0
- package/src/MemoryPanel.jsx +46 -0
- package/src/OpenAIVoicePanel.jsx +65 -0
- package/src/OutputsView.jsx +65 -0
- package/src/TerraformView.jsx +57 -0
- package/src/WorkbenchDialogs.jsx +532 -0
- package/src/activity-presentation.mjs +28 -0
- package/src/ai-qualification-scenarios.mjs +31 -0
- package/src/approval-application-files.mjs +32 -0
- package/src/approval-applications.mjs +82 -0
- package/src/approval-spec-kit.mjs +30 -0
- package/src/architecture-activity.css +28 -0
- package/src/architecture-activity.mjs +63 -0
- package/src/architecture-work.mjs +176 -0
- package/src/artifact-work.mjs +103 -0
- package/src/async-documents.mjs +191 -0
- package/src/atflow.css +1140 -0
- package/src/atforma.css +748 -0
- package/src/atmem-tokens.css +79 -0
- package/src/aws-preflight.mjs +60 -0
- package/src/aws-public-faq.mjs +102 -0
- package/src/aws-validation-scenarios.mjs +10 -0
- package/src/aws-validation.mjs +48 -0
- package/src/azure-plan-mocks.mjs +25 -0
- package/src/azure-structured.mjs +133 -0
- package/src/bedrock-summary.mjs +152 -0
- package/src/bounded-rag.mjs +60 -0
- package/src/brand.css +9 -0
- package/src/canvas-view.mjs +28 -0
- package/src/capability-composition.mjs +44 -0
- package/src/chat-attachments.mjs +13 -0
- package/src/cloud-provider-locks.mjs +9 -0
- package/src/cloud-storage-scenarios.mjs +2 -0
- package/src/component-review.mjs +55 -0
- package/src/composition-spec-kit.mjs +30 -0
- package/src/consultation-context.mjs +15 -0
- package/src/consultation.mjs +52 -0
- package/src/container-contracts.mjs +5 -0
- package/src/container-inspection.mjs +75 -0
- package/src/container-runtime.mjs +30 -0
- package/src/container-security-inspection.mjs +49 -0
- package/src/container-terraform.mjs +109 -0
- package/src/conversation.mjs +17 -0
- package/src/core.mjs +863 -0
- package/src/cross-cloud-storage.mjs +100 -0
- package/src/delivery-capabilities.mjs +25 -0
- package/src/delivery.mjs +58 -0
- package/src/deployment-ownership.mjs +10 -0
- package/src/deployment-region.mjs +11 -0
- package/src/derive.mjs +31 -0
- package/src/design-build-flow.mjs +10 -0
- package/src/diagram-attention.mjs +24 -0
- package/src/discovery.mjs +359 -0
- package/src/document.mjs +170 -0
- package/src/emulation-feedback.mjs +61 -0
- package/src/emulation-guidance.mjs +37 -0
- package/src/engagement.mjs +274 -0
- package/src/engineering-client.mjs +73 -0
- package/src/engineering-defaults.mjs +17 -0
- package/src/engineering.css +31 -0
- package/src/enterprise-application.mjs +76 -0
- package/src/enterprise-aws-host-infrastructure.mjs +62 -0
- package/src/enterprise-aws-host-tests.mjs +15 -0
- package/src/enterprise-aws-infrastructure-tests.mjs +38 -0
- package/src/enterprise-aws-infrastructure.mjs +138 -0
- package/src/enterprise-google-infrastructure.mjs +84 -0
- package/src/enterprise-google-tests.mjs +25 -0
- package/src/estimate.mjs +188 -0
- package/src/existing-references.mjs +18 -0
- package/src/external-identities.mjs +17 -0
- package/src/faq-lambda-build.mjs +28 -0
- package/src/gcp-structured.mjs +104 -0
- package/src/generation-provider.mjs +7 -0
- package/src/generation-quality.mjs +69 -0
- package/src/handoff.mjs +141 -0
- package/src/ibm-registry.mjs +12 -0
- package/src/ibm-structured.mjs +103 -0
- package/src/identifiers.mjs +15 -0
- package/src/implementation-map.mjs +14 -0
- package/src/implementation-outcome.mjs +8 -0
- package/src/incomplete-terraform.mjs +9 -0
- package/src/integrations.ts +137 -0
- package/src/interface-contracts.mjs +30 -0
- package/src/invoice-application.mjs +66 -0
- package/src/invoice-aws-operations.mjs +136 -0
- package/src/invoice-aws.mjs +143 -0
- package/src/invoice-azure-operations.mjs +147 -0
- package/src/invoice-azure.mjs +134 -0
- package/src/invoice-builder.mjs +35 -0
- package/src/invoice-dependencies.mjs +43 -0
- package/src/invoice-gcp-operations.mjs +178 -0
- package/src/invoice-gcp.mjs +104 -0
- package/src/invoice-ibm-operations.mjs +278 -0
- package/src/invoice-ibm.mjs +142 -0
- package/src/invoice-operations.mjs +143 -0
- package/src/invoice-plan.mjs +45 -0
- package/src/invoice-region.mjs +6 -0
- package/src/invoice-spec-kit.mjs +61 -0
- package/src/invoice-state.mjs +7 -0
- package/src/invoice-terraform.mjs +29 -0
- package/src/invoice-tests.mjs +122 -0
- package/src/journey-workspace.css +72 -0
- package/src/journey.mjs +87 -0
- package/src/levels.mjs +145 -0
- package/src/local-agent.mjs +184 -0
- package/src/main.jsx +2458 -0
- package/src/model-policy-preflight.mjs +34 -0
- package/src/openai-voice.mjs +102 -0
- package/src/output-bundle.mjs +11 -0
- package/src/ownership-inspection.mjs +28 -0
- package/src/pack.mjs +201 -0
- package/src/pricing.mjs +31 -0
- package/src/production-state.mjs +8 -0
- package/src/project-copy.mjs +10 -0
- package/src/project-file.mjs +160 -0
- package/src/project-links.mjs +3 -0
- package/src/protected-state.mjs +43 -0
- package/src/public-faq-builder.mjs +19 -0
- package/src/public-faq-clouds.mjs +251 -0
- package/src/public-faq.mjs +122 -0
- package/src/pulumi.mjs +123 -0
- package/src/python-container.mjs +25 -0
- package/src/python-runtime.mjs +50 -0
- package/src/rag-handler.mjs +43 -0
- package/src/rag-web-assets.mjs +52 -0
- package/src/rag-web.mjs +68 -0
- package/src/records.mjs +177 -0
- package/src/resources.mjs +163 -0
- package/src/review.mjs +252 -0
- package/src/runtime/EnterpriseDocument.java +89 -0
- package/src/runtime/approved_http_destination.py +72 -0
- package/src/runtime/aws-lambda-qualification-lock.json +7 -0
- package/src/runtime/aws-python-lock.json +82 -0
- package/src/runtime/aws-web-python-lock.json +887 -0
- package/src/runtime/aws_json_model.py +42 -0
- package/src/runtime/azure-python-lock.json +863 -0
- package/src/runtime/azure-sources.json +8 -0
- package/src/runtime/azure-test-lock.json +23 -0
- package/src/runtime/azure_acceptance.py +192 -0
- package/src/runtime/azure_structured.py +153 -0
- package/src/runtime/bounded_rag.py +126 -0
- package/src/runtime/catalog_admin.py +70 -0
- package/src/runtime/cloud-sql-proxy-lock.json +8 -0
- package/src/runtime/cloud-sql-proxy-security-review.json +30 -0
- package/src/runtime/connector_cli.py +50 -0
- package/src/runtime/connector_sync.py +70 -0
- package/src/runtime/durable-python-lock.json +247 -0
- package/src/runtime/durable_workflow.py +325 -0
- package/src/runtime/enterprise-application-manifest.json +110 -0
- package/src/runtime/enterprise-application-sources.json +266 -0
- package/src/runtime/enterprise-auth-python-lock.json +592 -0
- package/src/runtime/enterprise-aws-host-bootstrap-source.json +1 -0
- package/src/runtime/enterprise-aws-host-probe-source.json +1 -0
- package/src/runtime/enterprise-aws-task-template.json +235 -0
- package/src/runtime/enterprise-document-lock.json +18 -0
- package/src/runtime/enterprise-document.Dockerfile +15 -0
- package/src/runtime/enterprise-runtime-aws-python-lock.json +1115 -0
- package/src/runtime/enterprise-runtime-azure-python-lock.json +1106 -0
- package/src/runtime/enterprise-runtime-gcp-python-lock.json +1060 -0
- package/src/runtime/enterprise-runtime-ibm-python-lock.json +1096 -0
- package/src/runtime/enterprise-runtime-python-lock.json +1058 -0
- package/src/runtime/enterprise-runtime.Dockerfile +15 -0
- package/src/runtime/enterprise-web-app.js +1 -0
- package/src/runtime/enterprise-web-entry.js +105 -0
- package/src/runtime/enterprise-web-lock.json +29 -0
- package/src/runtime/enterprise-web-notices.txt +229 -0
- package/src/runtime/enterprise-web.css +4 -0
- package/src/runtime/enterprise-web.html +8 -0
- package/src/runtime/enterprise_audit.py +91 -0
- package/src/runtime/enterprise_audit_schema.sql +25 -0
- package/src/runtime/enterprise_auth_schema.sql +67 -0
- package/src/runtime/enterprise_auth_store.py +241 -0
- package/src/runtime/enterprise_aws_host_bootstrap.py +120 -0
- package/src/runtime/enterprise_aws_host_probe.py +105 -0
- package/src/runtime/enterprise_aws_hosts.py +136 -0
- package/src/runtime/enterprise_aws_jobs.py +300 -0
- package/src/runtime/enterprise_aws_objects.py +93 -0
- package/src/runtime/enterprise_aws_transfer.py +83 -0
- package/src/runtime/enterprise_azure_batch_api.py +128 -0
- package/src/runtime/enterprise_azure_host.py +599 -0
- package/src/runtime/enterprise_azure_jobs.py +294 -0
- package/src/runtime/enterprise_azure_objects.py +234 -0
- package/src/runtime/enterprise_azure_pool.py +161 -0
- package/src/runtime/enterprise_azure_transfer.py +122 -0
- package/src/runtime/enterprise_configuration.py +136 -0
- package/src/runtime/enterprise_document.py +116 -0
- package/src/runtime/enterprise_document_cli.py +32 -0
- package/src/runtime/enterprise_document_contract.py +54 -0
- package/src/runtime/enterprise_document_job.py +109 -0
- package/src/runtime/enterprise_document_jobs.py +122 -0
- package/src/runtime/enterprise_document_jobs_schema.sql +23 -0
- package/src/runtime/enterprise_extraction_cache.py +186 -0
- package/src/runtime/enterprise_extraction_schema.sql +17 -0
- package/src/runtime/enterprise_google_jobs.py +275 -0
- package/src/runtime/enterprise_google_transfer.py +85 -0
- package/src/runtime/enterprise_http.py +212 -0
- package/src/runtime/enterprise_identity.py +52 -0
- package/src/runtime/enterprise_knowledge.py +152 -0
- package/src/runtime/enterprise_models.py +94 -0
- package/src/runtime/enterprise_native.py +181 -0
- package/src/runtime/enterprise_oauth.py +132 -0
- package/src/runtime/enterprise_operator.py +160 -0
- package/src/runtime/enterprise_parser_service.py +136 -0
- package/src/runtime/enterprise_parser_transport.py +68 -0
- package/src/runtime/enterprise_queries.py +255 -0
- package/src/runtime/enterprise_queries_schema.sql +27 -0
- package/src/runtime/enterprise_query_service.py +111 -0
- package/src/runtime/enterprise_secrets.py +110 -0
- package/src/runtime/enterprise_sessions.py +95 -0
- package/src/runtime/enterprise_sources.py +260 -0
- package/src/runtime/enterprise_web.py +39 -0
- package/src/runtime/enterprise_worker.py +68 -0
- package/src/runtime/export_contract_acceptance.py +18 -0
- package/src/runtime/faq-output-contract.json +40 -0
- package/src/runtime/faq-web-app.js +41 -0
- package/src/runtime/faq-web.css +1 -0
- package/src/runtime/faq-web.html +6 -0
- package/src/runtime/gcp-sources.json +9 -0
- package/src/runtime/gcp_acceptance.py +110 -0
- package/src/runtime/gcp_structured.py +150 -0
- package/src/runtime/http_acceptance.py +39 -0
- package/src/runtime/iap-web-app.js +36 -0
- package/src/runtime/iap-web-source.json +1 -0
- package/src/runtime/ibm-python-lock.json +856 -0
- package/src/runtime/ibm-sources.json +9 -0
- package/src/runtime/ibm_acceptance.py +133 -0
- package/src/runtime/ibm_structured.py +113 -0
- package/src/runtime/invoice-azapi-provider-lock.json +7 -0
- package/src/runtime/invoice-ibm-provider-lock.json +7 -0
- package/src/runtime/invoice-parser-lock.json +1199 -0
- package/src/runtime/invoice-parser-python-lock.json +56 -0
- package/src/runtime/invoice-parser.Dockerfile +190 -0
- package/src/runtime/invoice-sources.json +58 -0
- package/src/runtime/invoice-test-sources.json +24 -0
- package/src/runtime/invoice-web.css +2 -0
- package/src/runtime/invoice-web.html +12 -0
- package/src/runtime/invoice-web.js +112 -0
- package/src/runtime/invoice_configuration.py +17 -0
- package/src/runtime/invoice_document.py +95 -0
- package/src/runtime/invoice_extraction.py +96 -0
- package/src/runtime/invoice_http.py +129 -0
- package/src/runtime/invoice_identity.py +5 -0
- package/src/runtime/invoice_maintenance.py +40 -0
- package/src/runtime/invoice_native.py +77 -0
- package/src/runtime/invoice_object_store.py +153 -0
- package/src/runtime/invoice_operations.py +108 -0
- package/src/runtime/invoice_operator.py +79 -0
- package/src/runtime/invoice_operator_core.py +39 -0
- package/src/runtime/invoice_operator_job.py +147 -0
- package/src/runtime/invoice_parser_cli.py +17 -0
- package/src/runtime/invoice_parser_identity.py +39 -0
- package/src/runtime/invoice_parser_service.py +89 -0
- package/src/runtime/invoice_parser_transport.py +89 -0
- package/src/runtime/invoice_processing.py +24 -0
- package/src/runtime/invoice_retention.py +16 -0
- package/src/runtime/invoice_runtime_secrets.py +43 -0
- package/src/runtime/invoice_stage.py +80 -0
- package/src/runtime/invoice_web.py +39 -0
- package/src/runtime/invoice_worker.py +50 -0
- package/src/runtime/lambda_faq.py +5 -0
- package/src/runtime/parser-requirements.lock +4 -0
- package/src/runtime/policy_review.py +18 -0
- package/src/runtime/postgres-qualification-lock.json +21 -0
- package/src/runtime/public-faq-sources.json +22 -0
- package/src/runtime/public_catalog_admin.py +122 -0
- package/src/runtime/public_catalog_store.py +125 -0
- package/src/runtime/public_faq.py +144 -0
- package/src/runtime/public_faq_app.py +49 -0
- package/src/runtime/python-container-lock.json +18 -0
- package/src/runtime/rag-sources.json +10 -0
- package/src/runtime/rag_acceptance.py +60 -0
- package/src/runtime/rag_aws_adapter.py +95 -0
- package/src/runtime/rds-ca-source.json +9 -0
- package/src/runtime/rds-global-ca.pem +2888 -0
- package/src/runtime/state-contract-check-source.json +4 -0
- package/src/runtime/state_contract_check.py +338 -0
- package/src/runtime/structured-source.json +1 -0
- package/src/runtime/structured-web-app.js +71 -0
- package/src/runtime/structured-web-source.json +1 -0
- package/src/runtime/structured_http.py +105 -0
- package/src/runtime/structured_output.py +48 -0
- package/src/runtime/web-python-lock.json +802 -0
- package/src/runtime/web_user_admin.py +44 -0
- package/src/runtime/workflow_application.py +62 -0
- package/src/runtime/workflow_bindings.py +128 -0
- package/src/runtime/workflow_configuration.py +40 -0
- package/src/runtime/workflow_connection.py +105 -0
- package/src/runtime/workflow_contract.py +54 -0
- package/src/runtime/workflow_database.py +94 -0
- package/src/runtime/workflow_errors.py +6 -0
- package/src/runtime/workflow_http.py +178 -0
- package/src/runtime/workflow_identity.py +69 -0
- package/src/runtime/workflow_payload.py +118 -0
- package/src/runtime/workflow_process_stage.py +88 -0
- package/src/runtime/workflow_profile.py +91 -0
- package/src/runtime/workflow_review.py +106 -0
- package/src/runtime/workflow_role_bootstrap.py +104 -0
- package/src/runtime/workflow_schema.sql +72 -0
- package/src/runtime/workflow_worker.py +142 -0
- package/src/runtime-settings.mjs +15 -0
- package/src/s3-access.mjs +14 -0
- package/src/source-contracts.mjs +17 -0
- package/src/spec-kit.mjs +186 -0
- package/src/state-custody.mjs +32 -0
- package/src/state-inspection.mjs +45 -0
- package/src/state-spec-kit.mjs +13 -0
- package/src/storage-tests.mjs +25 -0
- package/src/structured-api.mjs +133 -0
- package/src/structured-contract.mjs +19 -0
- package/src/structured-operation-policy.mjs +2 -0
- package/src/structured-web.mjs +17 -0
- package/src/style.css +1 -0
- package/src/system-description.mjs +26 -0
- package/src/terraform-admission.mjs +66 -0
- package/src/terraform-composition.mjs +83 -0
- package/src/terraform-expressions.mjs +46 -0
- package/src/terraform-validation-coverage.mjs +11 -0
- package/src/terraform.mjs +128 -0
- package/src/text-statistics.mjs +51 -0
- package/src/upstream/openarchflow/format-payload.ts +26 -0
- package/src/upstream/openarchflow/import/mermaid.ts +205 -0
- package/src/upstream/openarchflow/import/sql-ddl.ts +109 -0
- package/src/upstream/openarchflow/layout.ts +54 -0
- package/src/upstream/openarchflow/providers/aws.ts +196 -0
- package/src/upstream/openarchflow/providers/azure.ts +126 -0
- package/src/upstream/openarchflow/providers/cloudnative.ts +71 -0
- package/src/upstream/openarchflow/providers/generic.ts +227 -0
- package/src/upstream/openarchflow/providers/integration.ts +22 -0
- package/src/upstream/openarchflow/providers/types.ts +2 -0
- package/src/upstream/openarchflow/templates.ts +937 -0
- package/src/upstream-catalog.mjs +10 -0
- package/src/useChatScroll.jsx +36 -0
- package/src/useEmulation.jsx +36 -0
- package/src/validation-receipt.mjs +10 -0
- package/src/visual-export.mjs +92 -0
- package/src/voice-conversation.mjs +117 -0
- package/src/work-intake.mjs +24 -0
- package/src/workflow-runtime-inventory.mjs +3 -0
package/src/main.jsx
ADDED
|
@@ -0,0 +1,2458 @@
|
|
|
1
|
+
import {ChatResizeHandle} from './ChatResizeHandle.jsx';
|
|
2
|
+
import {copyProject} from './project-copy.mjs';
|
|
3
|
+
import './journey-workspace.css';
|
|
4
|
+
import {synchronizeEngineeringIntent,applyArchitectureChange} from './architecture-work.mjs';
|
|
5
|
+
import {applyArtifactWork} from './artifact-work.mjs';
|
|
6
|
+
import {ArchitectureWorkPanel} from './ArchitectureWorkPanel.jsx';
|
|
7
|
+
import {hasArchitectureDesign} from './design-build-flow.mjs';
|
|
8
|
+
import {ArchitectureTurn,EarlierArchitectureActivity} from './ArchitectureActivity.jsx';
|
|
9
|
+
import {recordArchitectureTurn,finishArchitectureTurn,architectureChatEntries} from './architecture-activity.mjs';
|
|
10
|
+
import {version as appVersion} from "../package.json";
|
|
11
|
+
import {restoreQueuedMessages,nextIntakeBatch,restoreIntakePaused} from './work-intake.mjs';
|
|
12
|
+
import {emulationGuidance,guidanceMessage} from './emulation-guidance.mjs';
|
|
13
|
+
import {readCanvasView,saveCanvasView,arrangeOverview} from './canvas-view.mjs';
|
|
14
|
+
import {BlockRequirements} from './BlockRequirements.jsx';
|
|
15
|
+
import {componentReviews,markComponentReviewed,applyComponentReview,editBlockRequirement,reviewIssueLabel} from './component-review.mjs';
|
|
16
|
+
import {ComponentReview,ReviewBadge} from './ComponentReview.jsx';
|
|
17
|
+
import {newQuestion} from './records.mjs';
|
|
18
|
+
import {useChatScroll} from './useChatScroll.jsx';
|
|
19
|
+
import {CONTAINER_SERVICES} from './container-contracts.mjs';
|
|
20
|
+
import {verifiedReceipt} from './validation-receipt.mjs';
|
|
21
|
+
import { conversationMessages, withConversation } from "./conversation.mjs";
|
|
22
|
+
import {ChatMessage} from "./ChatMessage.jsx";
|
|
23
|
+
import {readAttachments,ATTACHMENT_ACCEPT} from "./chat-attachments.mjs";
|
|
24
|
+
import {BrainActivity} from "./BrainActivity.jsx";
|
|
25
|
+
import {useEmulation} from './useEmulation.jsx';
|
|
26
|
+
import {EmulationPanel} from './EmulationPanel.jsx';
|
|
27
|
+
import {EngineeringPanel} from './EngineeringPanel.jsx';
|
|
28
|
+
import {emulationPrompt,guardEmulationCorrection,groundEmulationMessage,emulationCoachReady} from './emulation-feedback.mjs';
|
|
29
|
+
import {projectIdFromUrl} from './project-links.mjs';
|
|
30
|
+
import { diagramAttention } from "./diagram-attention.mjs";
|
|
31
|
+
import { VoiceDictation } from "./voice-conversation.mjs";
|
|
32
|
+
import {OpenAIVoicePanel} from './OpenAIVoicePanel.jsx';
|
|
33
|
+
import { OutputsView } from "./OutputsView.jsx";
|
|
34
|
+
import { outputBundle } from "./output-bundle.mjs";
|
|
35
|
+
import { TerraformView } from "./TerraformView.jsx";
|
|
36
|
+
import { journey, advanceJourney, DESIGN_SECTIONS,journeySectionId,completedStages, designSection, journeyAction } from "./journey.mjs";
|
|
37
|
+
import "@xyflow/react/dist/style.css";
|
|
38
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
39
|
+
import { createRoot } from "react-dom/client";
|
|
40
|
+
import {
|
|
41
|
+
ReactFlow,
|
|
42
|
+
ReactFlowProvider,
|
|
43
|
+
Background,
|
|
44
|
+
Handle,
|
|
45
|
+
Position,
|
|
46
|
+
applyNodeChanges,
|
|
47
|
+
applyEdgeChanges,
|
|
48
|
+
addEdge,
|
|
49
|
+
useReactFlow,
|
|
50
|
+
} from "@xyflow/react";
|
|
51
|
+
import {
|
|
52
|
+
AlertTriangle,
|
|
53
|
+
ArrowUp,
|
|
54
|
+
Paperclip,
|
|
55
|
+
StickyNote,
|
|
56
|
+
Scan,
|
|
57
|
+
ArrowDownToLine,
|
|
58
|
+
Check,
|
|
59
|
+
ChevronRight,
|
|
60
|
+
GitBranch,
|
|
61
|
+
Maximize2,
|
|
62
|
+
MoreHorizontal,
|
|
63
|
+
PanelRightClose,
|
|
64
|
+
PanelRightOpen,
|
|
65
|
+
Play,
|
|
66
|
+
Plus,
|
|
67
|
+
Search,
|
|
68
|
+
Trash2,
|
|
69
|
+
Undo2,
|
|
70
|
+
Redo2,
|
|
71
|
+
RotateCcw,
|
|
72
|
+
X,
|
|
73
|
+
Database,
|
|
74
|
+
Activity,
|
|
75
|
+
Sparkles,
|
|
76
|
+
ShieldCheck,
|
|
77
|
+
CheckCheck,
|
|
78
|
+
Code2,
|
|
79
|
+
Cloud,
|
|
80
|
+
LayoutTemplate,
|
|
81
|
+
ZoomIn,
|
|
82
|
+
ZoomOut,
|
|
83
|
+
Radio,
|
|
84
|
+
History,
|
|
85
|
+
Workflow,
|
|
86
|
+
FlaskConical,
|
|
87
|
+
Package,
|
|
88
|
+
Gauge,
|
|
89
|
+
UserCheck,
|
|
90
|
+
AppWindow,
|
|
91
|
+
PencilRuler,
|
|
92
|
+
ScanSearch,
|
|
93
|
+
Calculator,
|
|
94
|
+
Rocket,
|
|
95
|
+
BrainCircuit,
|
|
96
|
+
Waypoints,
|
|
97
|
+
Mic,
|
|
98
|
+
Square,
|
|
99
|
+
Eraser,
|
|
100
|
+
LayoutGrid,
|
|
101
|
+
ArrowLeft,
|
|
102
|
+
FolderOpen,
|
|
103
|
+
Moon,
|
|
104
|
+
Sun,
|
|
105
|
+
} from "lucide-react";
|
|
106
|
+
import { roleOf } from "./review.mjs";
|
|
107
|
+
import {
|
|
108
|
+
CATALOG,
|
|
109
|
+
PROVIDERS,
|
|
110
|
+
pdmTemplate,
|
|
111
|
+
checkProject,
|
|
112
|
+
orderProject,
|
|
113
|
+
executeBlock,
|
|
114
|
+
validateImport,
|
|
115
|
+
propose,
|
|
116
|
+
makePlan,
|
|
117
|
+
} from "./core.mjs";
|
|
118
|
+
import { layerOf, proposalDiff, systemView } from "./levels.mjs";
|
|
119
|
+
import { consult, keepEngagement, newDecision, recommendationDecision } from "./engagement.mjs";
|
|
120
|
+
import { adrMarkdown, OPTION_NAMES } from "./pack.mjs";
|
|
121
|
+
import { deploymentDocument } from "./document.mjs";
|
|
122
|
+
import { withDerived } from "./derive.mjs";
|
|
123
|
+
import { MemoryPanel, ContextUsed } from "./MemoryPanel.jsx";
|
|
124
|
+
import { defaultAssessment } from "./engagement.mjs";
|
|
125
|
+
import { setUsage } from "./estimate.mjs";
|
|
126
|
+
import { applyPriceQuotes } from "./pricing.mjs";
|
|
127
|
+
import { applyDecisionUpdates, applyRequirementDrafts, proposeCommentRevisions } from "./consultation.mjs";
|
|
128
|
+
import { terraformSketch } from "./terraform.mjs";
|
|
129
|
+
import { WORKFLOWS, conceptSignature, essentialsKnown, nextQuestion, opening, progress, sketch, understand } from "./discovery.mjs";
|
|
130
|
+
import { review } from "./review.mjs";
|
|
131
|
+
import { estimate } from "./estimate.mjs";
|
|
132
|
+
import { DeployView, EstimateView, ProjectsDialog, ReviewView, TestCard } from "./JourneyViews.jsx";
|
|
133
|
+
import { newRequirement } from "./records.mjs";
|
|
134
|
+
import { DiscoverView, OptionsView, DecisionsView, MeasuresEditor, RequirementsView } from "./EngagementViews.jsx";
|
|
135
|
+
import { fromProjectFile, parseProjectFile, toProjectFile, MAX_FILE_BYTES } from "./project-file.mjs";
|
|
136
|
+
import { linkedTo, slugSubject } from "./records.mjs";
|
|
137
|
+
import "./atmem-tokens.css";
|
|
138
|
+
import "./brand.css";
|
|
139
|
+
import "./atflow.css";
|
|
140
|
+
import {
|
|
141
|
+
UPSTREAM_CATALOG,
|
|
142
|
+
autoLayout,
|
|
143
|
+
importUpstreamProject,
|
|
144
|
+
prettyPayload,
|
|
145
|
+
} from "./integrations.ts";
|
|
146
|
+
import {
|
|
147
|
+
Connections,
|
|
148
|
+
TemplateDialog,
|
|
149
|
+
ImportDialog,
|
|
150
|
+
PulumiDialog,
|
|
151
|
+
FrontPanel,
|
|
152
|
+
} from "./WorkbenchDialogs.jsx";
|
|
153
|
+
import { discoverLocalAgent, projects as projectStore, requestArchitect } from "./local-agent.mjs";
|
|
154
|
+
import { EFFORT_LABEL } from "./WorkbenchDialogs.jsx";
|
|
155
|
+
import { pulumiScaffold } from "./pulumi.mjs";
|
|
156
|
+
import { zipSync, strToU8 } from "fflate";
|
|
157
|
+
|
|
158
|
+
// Each browser starts fresh; earlier projects are opened deliberately (⋯ → Open project file).
|
|
159
|
+
const STORAGE = "atforma-workspace-v3";
|
|
160
|
+
// A new project starts empty: the user describes what they want first.
|
|
161
|
+
const emptyProject = () => ({ id: crypto.randomUUID(), version: 1, revision: 1, name: "New project", provider: "tools", requirements: "", nodes: [], edges: [] });
|
|
162
|
+
const ALL_CATALOG = [...CATALOG, ...UPSTREAM_CATALOG];
|
|
163
|
+
const KIND_ICON = {
|
|
164
|
+
source: Database,
|
|
165
|
+
predict: Activity,
|
|
166
|
+
agent: Sparkles,
|
|
167
|
+
guard: ShieldCheck,
|
|
168
|
+
output: CheckCheck,
|
|
169
|
+
tool: Code2,
|
|
170
|
+
resource: Cloud,
|
|
171
|
+
stream: Radio,
|
|
172
|
+
history: History,
|
|
173
|
+
features: Workflow,
|
|
174
|
+
train: FlaskConical,
|
|
175
|
+
registry: Package,
|
|
176
|
+
monitor: Gauge,
|
|
177
|
+
};
|
|
178
|
+
// Scenario, label, block kind it needs.
|
|
179
|
+
// Role → AWS-architecture-style colour and icon, so the board reads like infrastructure.
|
|
180
|
+
const ROLE_STYLE = {
|
|
181
|
+
data: ["#7aa116", Database],
|
|
182
|
+
ingest: ["#8c4fff", Waypoints],
|
|
183
|
+
index: ["#c925d1", ScanSearch],
|
|
184
|
+
llm: ["#01a88d", BrainCircuit],
|
|
185
|
+
model: ["#01a88d", Activity],
|
|
186
|
+
monitor: ["#e7157b", Gauge],
|
|
187
|
+
guard: ["#dd344c", ShieldCheck],
|
|
188
|
+
human: ["#5f6b7a", UserCheck],
|
|
189
|
+
app: ["#1f777c", AppWindow],
|
|
190
|
+
output: ["#1f777c", CheckCheck],
|
|
191
|
+
};
|
|
192
|
+
const LAYER_COLOR = { data: "#7aa116", ml: "#01a88d", intelligence: "#01a88d", integration: "#e7157b", governance: "#dd344c", delivery: "#1f777c" };
|
|
193
|
+
const MODE_ICON = { design: PencilRuler, review: ShieldCheck, estimate: Calculator, deploy: Rocket };
|
|
194
|
+
const SCENARIOS = [
|
|
195
|
+
["happy", "Normal day", null],
|
|
196
|
+
["sensor-dropout", "Sensor dropout", "stream"],
|
|
197
|
+
["weak-model", "Weak model", "train"],
|
|
198
|
+
["drift", "Drift", "monitor"],
|
|
199
|
+
["ungrounded", "Ungrounded insight", "agent"],
|
|
200
|
+
["missing", "Missing input field", "source"],
|
|
201
|
+
["denied", "Access denied", "guard"],
|
|
202
|
+
["tool-error", "App unavailable", "tool"],
|
|
203
|
+
];
|
|
204
|
+
// Discovery is woven into Design: you talk, the architect draws.
|
|
205
|
+
const MODES = [
|
|
206
|
+
["design", "Design"],
|
|
207
|
+
["review", "Review"],
|
|
208
|
+
["estimate", "Estimate"],
|
|
209
|
+
["deploy", "Delivery draft"],
|
|
210
|
+
];
|
|
211
|
+
const PROMPTS = {
|
|
212
|
+
design: ["Compare all four clouds"],
|
|
213
|
+
review: ["Review risks", "How do I test this?"],
|
|
214
|
+
estimate: ["What drives cost?"],
|
|
215
|
+
deploy: ["What evidence do we need to scale?"],
|
|
216
|
+
};
|
|
217
|
+
const STATUS_GLYPH = {
|
|
218
|
+
passed: <Check size={13} />,
|
|
219
|
+
failed: <X size={13} />,
|
|
220
|
+
running: <BrainActivity size={24} />,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Terraform mark from Simple Icons (CC0), https://simpleicons.org/?q=terraform.
|
|
224
|
+
function TerraformIcon(){return <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M1.44 0v7.575l6.561 3.79V3.787zm21.12 4.227l-6.561 3.791v7.574l6.56-3.787zM8.72 4.23v7.575l6.561 3.787V8.018zm0 8.405v7.575L15.28 24v-7.578z"/></svg>;}
|
|
225
|
+
|
|
226
|
+
// The AtMem.ai mark, same artwork as atmem.ai/brand/icon-on-light.svg.
|
|
227
|
+
// Ink follows currentColor; the dot follows the theme (see .atmem-dot).
|
|
228
|
+
function Mark({ size = 20 }) {
|
|
229
|
+
return (
|
|
230
|
+
<svg className="atmem-mark" width={size} height={size} viewBox="-154.35 -151.35 308.7 308.7" aria-hidden="true">
|
|
231
|
+
<g transform="translate(0 2)">
|
|
232
|
+
<path d="M-91.7 74.24 L0 -88 L91.7 74.24 M-66.7 30 H66.7" fill="none" stroke="currentColor" strokeWidth="26" strokeLinejoin="round" />
|
|
233
|
+
<path
|
|
234
|
+
d="M-142 96a38 38 0 1 0 76 0a38 38 0 1 0 -76 0ZM-116 96a12 12 0 1 0 24 0a12 12 0 1 0 -24 0ZM66 96a38 38 0 1 0 76 0a38 38 0 1 0 -76 0ZM92 96a12 12 0 1 0 24 0a12 12 0 1 0 -24 0Z"
|
|
235
|
+
fill="currentColor"
|
|
236
|
+
fillRule="evenodd"
|
|
237
|
+
/>
|
|
238
|
+
<circle className="atmem-dot" cx="0" cy="-88" r="40" />
|
|
239
|
+
</g>
|
|
240
|
+
</svg>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Which architect the user chose; absent on first visit, so we ask before anything else.
|
|
245
|
+
const CONNECTION = "atforma-connection-v1";
|
|
246
|
+
function readConnection() {
|
|
247
|
+
try {
|
|
248
|
+
const c = JSON.parse(localStorage.getItem(CONNECTION) || "null");
|
|
249
|
+
// Older builds stored "offline"; the architect now always needs a real model.
|
|
250
|
+
return c && typeof c.provider === "string" && c.provider !== "offline"
|
|
251
|
+
? { provider: c.provider, model: String(c.model || ""), ...(c.effort ? { effort: String(c.effort) } : {}) }
|
|
252
|
+
: null;
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function saveConnection(c) {
|
|
258
|
+
try {
|
|
259
|
+
localStorage.setItem(CONNECTION, JSON.stringify({ provider: c.provider, model: c.model, effort: c.effort }));
|
|
260
|
+
} catch {}
|
|
261
|
+
}
|
|
262
|
+
function readSaved() {
|
|
263
|
+
try {
|
|
264
|
+
const raw = localStorage.getItem(STORAGE);
|
|
265
|
+
const p = validateImport(raw ? JSON.parse(raw) : emptyProject());
|
|
266
|
+
return withDerived(p.id ? p : { ...p, id: crypto.randomUUID() });
|
|
267
|
+
} catch {
|
|
268
|
+
return validateImport(emptyProject());
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function handlePosition(side, fallback) {
|
|
273
|
+
return Position[side?.[0]?.toUpperCase() + side?.slice(1)] || fallback;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function BlockNode({ data, selected }) {
|
|
277
|
+
const p = PROVIDERS[data.provider] || PROVIDERS.tools;
|
|
278
|
+
const [color, RoleIcon] = ROLE_STYLE[roleOf({ data })] || ["#5f6b7a", KIND_ICON[data.kind] || Cloud];
|
|
279
|
+
return (
|
|
280
|
+
<div
|
|
281
|
+
className={`blk kind-${data.kind} ${data.origin === "existing" ? "existing" : ""} ${selected ? "selected" : ""} ${data.dim ? "dim" : ""} state-${data.runState || "idle"}`}
|
|
282
|
+
style={{ "--provider": p.color, "--role": color }}
|
|
283
|
+
>
|
|
284
|
+
{data.kind !== "source" && (
|
|
285
|
+
<Handle type="target" position={handlePosition(data.inputPosition, Position.Left)} />
|
|
286
|
+
)}
|
|
287
|
+
<span className="blk-icon">
|
|
288
|
+
<RoleIcon size={18} />
|
|
289
|
+
</span>
|
|
290
|
+
<span className="blk-text">
|
|
291
|
+
<strong>{data.label}</strong>
|
|
292
|
+
{data.description && <span className="blk-why">{data.description}</span>}
|
|
293
|
+
{(data.origin === "existing" || data.provider !== "tools") && (
|
|
294
|
+
<small>
|
|
295
|
+
<i className="pdot" />
|
|
296
|
+
{data.origin === "existing" ? "your system" : p.name}
|
|
297
|
+
</small>
|
|
298
|
+
)}
|
|
299
|
+
{data.blockReview&&<ReviewBadge item={data.blockReview} onClick={data.onReview}/>}
|
|
300
|
+
</span>
|
|
301
|
+
<span className="blk-state">{STATUS_GLYPH[data.runState]}</span>
|
|
302
|
+
{data.kind !== "output" && (
|
|
303
|
+
<Handle type="source" position={handlePosition(data.outputPosition, Position.Right)} />
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function LayerNode({ data, selected }) {
|
|
310
|
+
return (
|
|
311
|
+
<div className={`layer ${selected ? "selected" : ""} state-${data.status}`} style={{ "--role": LAYER_COLOR[data.layer] || "#5f6b7a" }}>
|
|
312
|
+
<Handle type="target" position={Position.Left} isConnectable={false} />
|
|
313
|
+
<div className="layer-head">
|
|
314
|
+
<strong>{data.label}</strong>
|
|
315
|
+
<span className="blk-state">{STATUS_GLYPH[data.status]}</span>
|
|
316
|
+
</div>
|
|
317
|
+
<small>
|
|
318
|
+
{data.blocks.length} block{data.blocks.length === 1 ? "" : "s"}
|
|
319
|
+
{data.status !== "idle" && ` · ${data.done}/${data.blocks.length} passed`}
|
|
320
|
+
</small>
|
|
321
|
+
{data.reviewSummary&&<button className="topology-review-summary nodrag" onClick={e=>{e.stopPropagation();data.onReview?.();}} aria-label={`Open block review for ${data.label}`}>{data.reviewSummary.issues.length?reviewIssueLabel(data.reviewSummary.issues):`${data.reviewSummary.reviewed}/${data.blocks.length} designs reviewed`}</button>}
|
|
322
|
+
<button className="topology-drill-hint nodrag" onClick={e=>{e.stopPropagation();data.onExplore?.();}}>Explore components <ChevronRight size={12}/></button>
|
|
323
|
+
<span className="layer-providers">
|
|
324
|
+
{data.providers.map((id) => (
|
|
325
|
+
<span key={id} title={PROVIDERS[id].name} style={{ background: PROVIDERS[id].color }} />
|
|
326
|
+
))}
|
|
327
|
+
</span>
|
|
328
|
+
<Handle type="source" position={Position.Right} isConnectable={false} />
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A comment: a sticky note on the board. Never part of the system; arrows may point from it.
|
|
334
|
+
function NoteNode({ id, data, selected }) {
|
|
335
|
+
const [text, setText] = useState(data.label);
|
|
336
|
+
const box = useRef(null);
|
|
337
|
+
useEffect(() => { if (document.activeElement !== box.current) setText(data.label); }, [data.label]);
|
|
338
|
+
// A comment you just added is ready to type into.
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
if (data.fresh) setTimeout(() => box.current?.focus(), 30);
|
|
341
|
+
}, [data.fresh]);
|
|
342
|
+
return (
|
|
343
|
+
<div className={`note sticky-note ${selected ? "selected" : ""}`}>
|
|
344
|
+
<Handle type="target" position={Position.Left} />
|
|
345
|
+
<textarea
|
|
346
|
+
ref={box}
|
|
347
|
+
className="nodrag"
|
|
348
|
+
aria-label="Comment"
|
|
349
|
+
placeholder="Write a comment…"
|
|
350
|
+
value={text}
|
|
351
|
+
onChange={(e) => setText(e.target.value)}
|
|
352
|
+
onBlur={() => text !== data.label && data.onText?.(id, text)}
|
|
353
|
+
/>
|
|
354
|
+
<Handle type="source" position={Position.Right} />
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const nodeTypes = { resource: BlockNode, layer: LayerNode, note: NoteNode };
|
|
359
|
+
|
|
360
|
+
function Palette({ onAdd, onClose, disabled }) {
|
|
361
|
+
const [query, setQuery] = useState(""),
|
|
362
|
+
[provider, setProvider] = useState("all"),
|
|
363
|
+
[scope, setScope] = useState("ai");
|
|
364
|
+
const items = ALL_CATALOG.filter(
|
|
365
|
+
(c) =>
|
|
366
|
+
(scope === "all" ||
|
|
367
|
+
(scope === "ai" &&
|
|
368
|
+
(c.kind !== "resource" ||
|
|
369
|
+
/AI|Machine Learning|Analytics/i.test(c.category || ""))) ||
|
|
370
|
+
(scope === "run" && c.kind !== "resource")) &&
|
|
371
|
+
(provider === "all" || c.provider === provider) &&
|
|
372
|
+
`${c.label} ${c.description} ${PROVIDERS[c.provider].name}`
|
|
373
|
+
.toLowerCase()
|
|
374
|
+
.includes(query.toLowerCase()),
|
|
375
|
+
);
|
|
376
|
+
return (
|
|
377
|
+
<div className="palette" role="dialog" aria-label="Add block">
|
|
378
|
+
<div className="palette-search">
|
|
379
|
+
<Search size={14} />
|
|
380
|
+
<input
|
|
381
|
+
autoFocus
|
|
382
|
+
aria-label="Search blocks"
|
|
383
|
+
placeholder="Search blocks"
|
|
384
|
+
value={query}
|
|
385
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
386
|
+
onKeyDown={(e) => {
|
|
387
|
+
if (e.key === "Escape") onClose();
|
|
388
|
+
if (e.key === "Enter" && items[0]) onAdd(items[0]);
|
|
389
|
+
}}
|
|
390
|
+
/>
|
|
391
|
+
<select aria-label="Block scope" value={scope} onChange={(e) => setScope(e.target.value)}>
|
|
392
|
+
<option value="ai">AI</option>
|
|
393
|
+
<option value="run">Runnable</option>
|
|
394
|
+
<option value="all">All</option>
|
|
395
|
+
</select>
|
|
396
|
+
<button type="button" className="icon" aria-label="Close component picker" title="Close component picker" onClick={onClose}><X size={16}/></button>
|
|
397
|
+
</div>
|
|
398
|
+
<div className="chips">
|
|
399
|
+
{["all", ...Object.keys(PROVIDERS)].map((id) => (
|
|
400
|
+
<button
|
|
401
|
+
key={id}
|
|
402
|
+
className={provider === id ? "on" : ""}
|
|
403
|
+
onClick={() => setProvider(id)}
|
|
404
|
+
>
|
|
405
|
+
{id === "all" ? "All" : id === "tools" ? "Tools" : id === "gcp" ? "Google" : PROVIDERS[id].name}
|
|
406
|
+
</button>
|
|
407
|
+
))}
|
|
408
|
+
</div>
|
|
409
|
+
<div className="palette-list">
|
|
410
|
+
{items.map((item) => (
|
|
411
|
+
<button
|
|
412
|
+
key={item.id}
|
|
413
|
+
className="palette-item"
|
|
414
|
+
draggable={!disabled}
|
|
415
|
+
disabled={disabled}
|
|
416
|
+
onDragStart={(e) => e.dataTransfer.setData("application/atforma-block", item.id)}
|
|
417
|
+
onClick={() => onAdd(item)}
|
|
418
|
+
>
|
|
419
|
+
{React.createElement(KIND_ICON[item.kind] || Cloud, { size: 15 })}
|
|
420
|
+
<span>{item.label}</span>
|
|
421
|
+
<span className="dot" title={PROVIDERS[item.provider].name} style={{ background: PROVIDERS[item.provider].color }} />
|
|
422
|
+
</button>
|
|
423
|
+
))}
|
|
424
|
+
{!items.length && <p className="muted pad">No blocks match.</p>}
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function Linked({ project, id, hideNeeds = false }) {
|
|
431
|
+
const { needs, assumptions, questions } = linkedTo(project, id);
|
|
432
|
+
if (!needs.length && !assumptions.length && !questions.length) return null;
|
|
433
|
+
return (
|
|
434
|
+
<div className="linked">
|
|
435
|
+
{!hideNeeds&&needs.map((r) => (
|
|
436
|
+
<p key={r.id}>
|
|
437
|
+
<strong>Requirement</strong> {r.statement}
|
|
438
|
+
{r.measure && <span className="mono"> · {r.measure.metric} {r.measure.op} {String(r.measure.target)}{r.measure.unit}</span>}
|
|
439
|
+
</p>
|
|
440
|
+
))}
|
|
441
|
+
{assumptions.map((a) => (
|
|
442
|
+
<p key={a.id}>
|
|
443
|
+
<strong>Assumption</strong> {a.statement} <span className="muted">· {a.madeAt}</span>
|
|
444
|
+
</p>
|
|
445
|
+
))}
|
|
446
|
+
{questions.map((q) => (
|
|
447
|
+
<p key={q.id}>
|
|
448
|
+
<strong>{q.status==='answered'?'Answered question':'Open question'}</strong> {q.question}{q.status==='answered'&&q.answer&&<span className="recorded-answer">Answer: {q.answer}</span>}
|
|
449
|
+
</p>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function BlockDetail({ project, node, run, probe, busy, reviewItems, onReviewConfirm, onReviewDiscuss, onReviewQuestion, onReviewResolve, onRequirementSave, onWorkModelChange, onChange, onRemove, onOpen, onBack }) {
|
|
456
|
+
const trace = run?.steps.find((s) => s.id === node.id);
|
|
457
|
+
const inputs = project.edges.filter((e) => e.target === node.id);
|
|
458
|
+
const outputs = project.edges.filter((e) => e.source === node.id);
|
|
459
|
+
const nameOf = (id) => project.nodes.find((n) => n.id === id)?.data.label;
|
|
460
|
+
const set = (patch) => onChange({ ...node, data: { ...node.data, ...patch } });
|
|
461
|
+
const setConfig = (patch) => set({ config: { ...node.data.config, ...patch } });
|
|
462
|
+
const p = PROVIDERS[node.data.provider];
|
|
463
|
+
return (
|
|
464
|
+
<section className="detail" aria-label="Block detail">
|
|
465
|
+
<div className="detail-navigation">
|
|
466
|
+
<button className="back" onClick={onBack} aria-label="Back to the board">
|
|
467
|
+
<ArrowLeft size={15} /> Back to board <kbd>Esc</kbd>
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
<div className="detail-col">
|
|
471
|
+
<h3>Inputs</h3>
|
|
472
|
+
{inputs.length ? (
|
|
473
|
+
inputs.map((e) => (
|
|
474
|
+
<button key={e.id} className="terminal" onClick={() => onOpen(e.source)}>
|
|
475
|
+
<span>{nameOf(e.source)}</span>
|
|
476
|
+
<small>{e.label || "data"}</small>
|
|
477
|
+
</button>
|
|
478
|
+
))
|
|
479
|
+
) : (
|
|
480
|
+
<p className="muted">None — this block starts the flow.</p>
|
|
481
|
+
)}
|
|
482
|
+
{trace && (
|
|
483
|
+
<>
|
|
484
|
+
<h4>Received</h4>
|
|
485
|
+
<pre>{prettyPayload(trace.input)}</pre>
|
|
486
|
+
</>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
<div className="detail-core" style={{ "--provider": p.color }}>
|
|
490
|
+
<div className="detail-title">
|
|
491
|
+
<span className="blk-provider" />
|
|
492
|
+
<input
|
|
493
|
+
aria-label="Block name"
|
|
494
|
+
disabled={busy}
|
|
495
|
+
value={node.data.label}
|
|
496
|
+
onChange={(e) => set({ label: e.target.value })}
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
499
|
+
<p className="muted">{node.data.description || "No description."}</p>
|
|
500
|
+
<BlockRequirements project={project} nodeId={node.id} busy={busy} onSave={onRequirementSave}/>
|
|
501
|
+
{project.extensions?.architectureWork?.artifactMappings?.length>0&&<ArchitectureWorkPanel project={project} onChange={onWorkModelChange} busy={busy}/>}
|
|
502
|
+
<ComponentReview key={node.id} item={reviewItems.find(r=>r.id===node.id)} items={reviewItems} busy={busy} onOpen={onOpen} onConfirm={onReviewConfirm} onDiscuss={onReviewDiscuss} onQuestion={onReviewQuestion} onResolve={onReviewResolve} onDetailAnswer={patch=>onChange({...node,data:{...node.data,...patch,...(patch.config?{config:{...node.data.config,...patch.config}}:{})}})}/>
|
|
503
|
+
<details className="component-settings"><summary>Component settings</summary><div className="fields">
|
|
504
|
+
<label>
|
|
505
|
+
Provider
|
|
506
|
+
<select disabled={busy} value={node.data.provider} onChange={(e) => set({ provider: e.target.value })}>
|
|
507
|
+
{Object.entries(PROVIDERS).map(([id, v]) => (
|
|
508
|
+
<option key={id} value={id}>{v.name}</option>
|
|
509
|
+
))}
|
|
510
|
+
</select>
|
|
511
|
+
</label>
|
|
512
|
+
<label>
|
|
513
|
+
Behaviour
|
|
514
|
+
<select
|
|
515
|
+
aria-label="Execution behaviour"
|
|
516
|
+
disabled={busy}
|
|
517
|
+
value={node.data.kind}
|
|
518
|
+
onChange={(e) => set({ kind: e.target.value })}
|
|
519
|
+
>
|
|
520
|
+
<option value="resource">Architecture component</option>
|
|
521
|
+
<option value="source">Source fixture</option>
|
|
522
|
+
<option value="predict">Model fixture</option>
|
|
523
|
+
<option value="agent">Agent fixture</option>
|
|
524
|
+
<option value="guard">Policy fixture</option>
|
|
525
|
+
<option value="output">Assertions</option>
|
|
526
|
+
<option value="tool">Tool / app fixture</option>
|
|
527
|
+
<option value="stream">Sensor stream</option>
|
|
528
|
+
<option value="history">Maintenance history</option>
|
|
529
|
+
<option value="features">Feature pipeline</option>
|
|
530
|
+
<option value="train">Training pipeline</option>
|
|
531
|
+
<option value="registry">Model registry</option>
|
|
532
|
+
<option value="monitor">Model monitor</option>
|
|
533
|
+
</select>
|
|
534
|
+
</label>
|
|
535
|
+
<label>
|
|
536
|
+
Region
|
|
537
|
+
<input
|
|
538
|
+
disabled={busy}
|
|
539
|
+
value={node.data.config?.region || ""}
|
|
540
|
+
onChange={(e) => setConfig({ region: e.target.value })}
|
|
541
|
+
/>
|
|
542
|
+
</label>
|
|
543
|
+
<label>
|
|
544
|
+
Data class
|
|
545
|
+
<select
|
|
546
|
+
disabled={busy}
|
|
547
|
+
value={node.data.config?.classification || "Internal"}
|
|
548
|
+
onChange={(e) => setConfig({ classification: e.target.value })}
|
|
549
|
+
>
|
|
550
|
+
<option>Public</option>
|
|
551
|
+
<option>Internal</option>
|
|
552
|
+
<option>Confidential</option>
|
|
553
|
+
</select>
|
|
554
|
+
</label>
|
|
555
|
+
<label>
|
|
556
|
+
Status
|
|
557
|
+
<select aria-label="Block status" disabled={busy} value={node.data.origin === "existing" ? "existing" : "proposed"} onChange={(e) => set({ origin: e.target.value })}>
|
|
558
|
+
<option value="proposed">Proposed</option>
|
|
559
|
+
<option value="existing">Client's existing system</option>
|
|
560
|
+
</select>
|
|
561
|
+
</label>
|
|
562
|
+
{node.data.origin === "existing" && <label className="field-wide">
|
|
563
|
+
Existing system handling
|
|
564
|
+
<select aria-label="Existing system handling" disabled={busy} value={node.data.deploymentMode === "create" ? "create" : node.data.binding === "existing-service" ? "reference" : "contract"} onChange={e => set(e.target.value === "create" ? {deploymentMode:"create",implementation:"container-service",binding:undefined} : {deploymentMode:undefined,binding:e.target.value === "reference" ? "existing-service" : undefined,...(node.data.deploymentMode === "create" ? {implementation:undefined}:{})})}>
|
|
565
|
+
<option value="contract">Typed integration contract</option>
|
|
566
|
+
<option value="reference">Reference only — preserve existing service</option>
|
|
567
|
+
{CONTAINER_SERVICES[node.data.provider]?.service === node.data.service && <option value="create">Existing application — create a new private deployment</option>}
|
|
568
|
+
</select>
|
|
569
|
+
<small>{node.data.deploymentMode === "create" ? "Creates a new private container deployment from your supplied image. Application behavior and connections require tests." : "Preserves an existing deployment without creating or importing it. Its connections require tests."}</small>
|
|
570
|
+
</label>}
|
|
571
|
+
<label className="check">
|
|
572
|
+
<input type="checkbox" aria-label="In prototype scope" disabled={busy} checked={!!node.data.poc} onChange={(e) => set({ poc: e.target.checked })} />
|
|
573
|
+
In prototype scope
|
|
574
|
+
</label>
|
|
575
|
+
</div></details>
|
|
576
|
+
<Linked project={project} id={node.id} hideNeeds/>
|
|
577
|
+
{node.data.kind === "output" && (
|
|
578
|
+
<MeasuresEditor measures={node.data.measures} disabled={busy} onChange={(measures) => set({ measures })} />
|
|
579
|
+
)}
|
|
580
|
+
{trace ? (
|
|
581
|
+
<div className={`trace ${trace.status}`}>
|
|
582
|
+
{STATUS_GLYPH[trace.status]} {trace.status[0].toUpperCase() + trace.status.slice(1)} · {trace.duration} ms
|
|
583
|
+
{trace.error && <p className="error-box">{trace.error}</p>}
|
|
584
|
+
</div>
|
|
585
|
+
) : (
|
|
586
|
+
<p className="muted">Not run yet. Run a test to see payloads.</p>
|
|
587
|
+
)}
|
|
588
|
+
<button className="btn ghost danger" disabled={busy} onClick={onRemove}>
|
|
589
|
+
<Trash2 size={13} /> Remove block
|
|
590
|
+
</button>
|
|
591
|
+
</div>
|
|
592
|
+
<div className={`detail-col ${probe ? "probed" : ""}`}>
|
|
593
|
+
<h3>Outputs</h3>
|
|
594
|
+
{outputs.length ? (
|
|
595
|
+
outputs.map((e) => (
|
|
596
|
+
<button key={e.id} className="terminal" onClick={() => onOpen(e.target)}>
|
|
597
|
+
<span>{nameOf(e.target)}</span>
|
|
598
|
+
<small>{e.label || "data"}</small>
|
|
599
|
+
</button>
|
|
600
|
+
))
|
|
601
|
+
) : (
|
|
602
|
+
<p className="muted">None — this block ends the flow.</p>
|
|
603
|
+
)}
|
|
604
|
+
{trace && (
|
|
605
|
+
<>
|
|
606
|
+
<h4>Produced{probe ? ` · probe on “${probe.label || "data"}”` : ""}</h4>
|
|
607
|
+
<pre>{prettyPayload(trace.output)}</pre>
|
|
608
|
+
</>
|
|
609
|
+
)}
|
|
610
|
+
</div>
|
|
611
|
+
</section>
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const brief = (p) => !!p.brief?.application?.trim();
|
|
616
|
+
// The concept to draw right away: a first sketch once the idea is recognised, and a redraw
|
|
617
|
+
// on the stated cloud while the design is still a sketch. Anything richer is the model's job.
|
|
618
|
+
function conceptFor(was, now) {
|
|
619
|
+
if (!now.workflow) return null;
|
|
620
|
+
const proposal = sketch(now);
|
|
621
|
+
if (!now.nodes.length) return proposal;
|
|
622
|
+
// Keep refining only the concept we drew ourselves; once the user edits the board, it's theirs.
|
|
623
|
+
const untouched = now.conceptSig && conceptSignature(now) === now.conceptSig;
|
|
624
|
+
return untouched && conceptSignature(proposal.next) !== now.conceptSig ? proposal : null;
|
|
625
|
+
}
|
|
626
|
+
// Small, factual signals of what the harness did with a message.
|
|
627
|
+
function harnessSignals(was, now) {
|
|
628
|
+
const grew = (k) => (now[k]?.length || 0) - (was[k]?.length || 0);
|
|
629
|
+
const out = [];
|
|
630
|
+
if (now.workflow && now.workflow !== was.workflow) out.push(`skill: ${WORKFLOWS.find((w) => w.id === now.workflow)?.name}`);
|
|
631
|
+
const topics = Object.keys(now.brief || {}).filter((k) => now.brief[k]?.trim() && !was.brief?.[k]?.trim());
|
|
632
|
+
if (topics.length) out.push(`noted ${topics.join(", ")}`);
|
|
633
|
+
if (grew("facts") > 0) out.push(`+${grew("facts")} fact${grew("facts") > 1 ? "s" : ""}`);
|
|
634
|
+
if (grew("needs") > 0) out.push(`+${grew("needs")} requirement${grew("needs") > 1 ? "s" : ""}`);
|
|
635
|
+
if (grew("assumptions") > 0) out.push("assumption logged");
|
|
636
|
+
if (grew("openQuestions") > 0) out.push("open question");
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
const welcome = (project) => ({
|
|
640
|
+
role: "assistant",
|
|
641
|
+
text: project.nodes.length || project.brief?.application?.trim()
|
|
642
|
+
? `Good to see you again — let's keep going.\n\n${opening(project)}`
|
|
643
|
+
: `Hi! What would you like to build? For example, an assistant that answers questions using your company’s documents, or a chatbot for your team. “Not sure” is always a fine answer along the way.`,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const THEME = "atmem-architect-theme";
|
|
647
|
+
// Light is the default; dark only when the user chose it.
|
|
648
|
+
function savedTheme() {
|
|
649
|
+
try {
|
|
650
|
+
return localStorage.getItem(THEME) === "dark" ? "dark" : "light";
|
|
651
|
+
} catch {
|
|
652
|
+
return "light";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
document.documentElement.dataset.theme = savedTheme();
|
|
656
|
+
|
|
657
|
+
function App() {
|
|
658
|
+
const [project, setProject] = useState(readSaved);
|
|
659
|
+
const projectRef = useRef(project);
|
|
660
|
+
projectRef.current = project;
|
|
661
|
+
const [initialCanvasView] = useState(()=>readCanvasView(project));
|
|
662
|
+
const [topologyPositions,setTopologyPositions] = useState(initialCanvasView.positions);
|
|
663
|
+
const [mode, setMode] = useState("design"),
|
|
664
|
+
[peek, setPeek] = useState(null),
|
|
665
|
+
[picked, setPicked] = useState(() => new Set()),
|
|
666
|
+
[saved, setSaved] = useState({ projects: [], bin: [], folder: "" }),
|
|
667
|
+
[ctx, setCtx] = useState(null),
|
|
668
|
+
[drawLive, setDrawLive] = useState(() => {
|
|
669
|
+
try {
|
|
670
|
+
return localStorage.getItem("atforma-draw-live") !== "off";
|
|
671
|
+
} catch {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
}),
|
|
675
|
+
[designView, setDesignView] = useState("board"),
|
|
676
|
+
[level, setLevel] = useState(initialCanvasView.level),
|
|
677
|
+
[focus, setFocus] = useState(initialCanvasView.focus),
|
|
678
|
+
[selected, setSelected] = useState(initialCanvasView.selected),
|
|
679
|
+
[messages, setMessages] = useState(() => conversationMessages(projectRef.current, [welcome(projectRef.current)])),
|
|
680
|
+
[reviewView, setReviewView] = useState("findings"),
|
|
681
|
+
[input, setInput] = useState(""),
|
|
682
|
+
[pending, setPending] = useState(null),
|
|
683
|
+
[busy, setBusy] = useState(false),
|
|
684
|
+
[scenario, setScenario] = useState("happy"),
|
|
685
|
+
[run, setRun] = useState(null),
|
|
686
|
+
[runStates, setRunStates] = useState({}),
|
|
687
|
+
[history, setHistory] = useState([]),
|
|
688
|
+
[future, setFuture] = useState([]),
|
|
689
|
+
[toast, setToast] = useState(""),
|
|
690
|
+
[modal, setModal] = useState(() => (readConnection() ? null : "welcome")),
|
|
691
|
+
[menu, setMenu] = useState(false),
|
|
692
|
+
[palette, setPalette] = useState(false),
|
|
693
|
+
[issuesOpen, setIssuesOpen] = useState(false),
|
|
694
|
+
[chatOpen, setChatOpen] = useState(true),
|
|
695
|
+
[theme, setTheme] = useState(savedTheme),
|
|
696
|
+
[environment, setEnvironment] = useState("staging"),
|
|
697
|
+
[storageError, setStorageError] = useState(false),
|
|
698
|
+
[view, setView] = useState("diagram"),
|
|
699
|
+
[probe, setProbe] = useState(null),
|
|
700
|
+
[snapshot, setSnapshot] = useState(null),
|
|
701
|
+
[measured, setMeasured] = useState({});
|
|
702
|
+
const messagesRef=useRef(messages);messagesRef.current=messages;
|
|
703
|
+
const [session, setSession] = useState(null),
|
|
704
|
+
[connection, setConnection] = useState(readConnection),
|
|
705
|
+
[agentBusy, setAgentBusy] = useState(false);
|
|
706
|
+
const [workspaceView,setWorkspaceView]=useState('canvas'),[buildRequest,setBuildRequest]=useState(null);
|
|
707
|
+
const [memorySync,setMemorySync]=useState(null);
|
|
708
|
+
const currentMemory=memorySync?.projectId===project.id?memorySync:null;
|
|
709
|
+
const memoryLabel=!session?'AtMem memory':currentMemory?.enabled===false?'AtMem off':currentMemory?.status==='unavailable'?'AtMem unavailable':currentMemory?.pending>0||currentMemory?.status==='sync_failed'?'AtMem sync pending':currentMemory?.status==='ready'?'AtMem on':'AtMem memory';
|
|
710
|
+
const buildView=false;
|
|
711
|
+
const hasDesign=hasArchitectureDesign(project);
|
|
712
|
+
function chooseWorkspace(value){if(value==='build'){setMode('review');setDesignView('board');setReviewView('findings');}setWorkspaceView('canvas');}
|
|
713
|
+
function buildCurrentDesign(intent){
|
|
714
|
+
if(!hasArchitectureDesign(projectRef.current)||busy||agentBusy||pending)return;
|
|
715
|
+
setBuildRequest({id:crypto.randomUUID(),projectId:projectRef.current.id,...(intent==='test'?{intent:'test'}:{})});chooseWorkspace('build');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
useEffect(()=>{saveCanvasView(project,{level,focus,selected,positions:topologyPositions});},[project.id,level,focus,selected,topologyPositions]);
|
|
719
|
+
const emulation=useEmulation(project,environment,session);
|
|
720
|
+
const emulationWorking=emulation.running||(agentBusy&&!!emulation.result);
|
|
721
|
+
const [chatWidth,setChatWidth]=useState(()=>{try{const n=Number(localStorage.getItem('atmem-architect-chat-width'));return Number.isFinite(n)&&n>=320&&n<=900?n:360;}catch{return 360;}});
|
|
722
|
+
function resizeChat(width){setChatWidth(width);try{localStorage.setItem('atmem-architect-chat-width',String(width));}catch{}}
|
|
723
|
+
const [dismissedStarts,setDismissedStarts]=useState(()=>{try{const ids=JSON.parse(localStorage.getItem('atforma-dismissed-starts-v1')||'[]');return Array.isArray(ids)?ids.filter(id=>typeof id==='string').slice(-64):[];}catch{return [];}});
|
|
724
|
+
function dismissStart(){setDismissedStarts(ids=>{const next=[...ids.filter(id=>id!==project.id),project.id].slice(-64);try{localStorage.setItem('atforma-dismissed-starts-v1',JSON.stringify(next));}catch{}return next;});}
|
|
725
|
+
const [emulationOpen,setEmulationOpen]=useState(false),[treeOpen,setTreeOpen]=useState(false);
|
|
726
|
+
useEffect(()=>{if(!treeOpen)return;const close=e=>{if(e.type==="keydown"?e.key==="Escape":!e.target.closest(".component-navigator,.palette"))setTreeOpen(false);};document.addEventListener("pointerdown",close);document.addEventListener("keydown",close);return()=>{document.removeEventListener("pointerdown",close);document.removeEventListener("keydown",close);};},[treeOpen]);
|
|
727
|
+
const consultationAttempts = useRef(new Set());
|
|
728
|
+
const agentAbort = useRef(null);
|
|
729
|
+
const messageInput = useRef(null);
|
|
730
|
+
const intakeRef=useRef(restoreQueuedMessages(messages,project.id));
|
|
731
|
+
const [intake,setIntake]=useState(intakeRef.current),[intakePaused,setIntakePausedState]=useState(()=>restoreIntakePaused(messages)||intakeRef.current.length>0);
|
|
732
|
+
const [intakeIssue,setIntakeIssue]=useState(null);
|
|
733
|
+
const intakePausedRef=useRef(intakePaused);
|
|
734
|
+
function setIntakePaused(value){intakePausedRef.current=value;setIntakePausedState(value);}
|
|
735
|
+
const carryIntent=useRef(null);
|
|
736
|
+
function updateIntake(next){intakeRef.current=next;setIntake(next);}
|
|
737
|
+
function setRequestState(ids,state){setMessages(list=>list.map(m=>ids.includes(m.requestId)?{...m,workState:state}:m));}
|
|
738
|
+
useEffect(()=>{
|
|
739
|
+
setIntakePaused(restoreIntakePaused(messages)||restoreQueuedMessages(messages,project.id).length>0);
|
|
740
|
+
setIntakeIssue(null);
|
|
741
|
+
const restored=restoreQueuedMessages(messages,project.id).filter(q=>!intakeRef.current.some(old=>old.id===q.id));
|
|
742
|
+
if(restored.length)updateIntake([...intakeRef.current,...restored]);
|
|
743
|
+
},[project.id]);
|
|
744
|
+
useEffect(()=>{
|
|
745
|
+
if(buildView||agentBusy||agentAbort.current||busy||pending||intakePausedRef.current||!session||!connection)return;
|
|
746
|
+
const batch=nextIntakeBatch(intakeRef.current,project.id);
|
|
747
|
+
setIntakeIssue(batch?.blocked?batch.reason:null);if(!batch||batch.blocked)return;
|
|
748
|
+
updateIntake(intakeRef.current.filter(q=>!batch.ids.includes(q.id)));
|
|
749
|
+
setRequestState(batch.ids,'processing');
|
|
750
|
+
const history=carryIntent.current?.projectId===project.id?[{role:'user',text:carryIntent.current.text},...messages.slice(-10)]:messages;
|
|
751
|
+
void send(batch.message,'chat',null,null,{alreadyLogged:true,requestIds:batch.ids,attachments:batch.attachments,history});
|
|
752
|
+
},[buildView,agentBusy,busy,pending,intakePaused,intake,session,connection,project.id]);
|
|
753
|
+
const voice = useRef(null),openaiVoice=useRef(null);
|
|
754
|
+
const [voiceState, setVoiceState] = useState("off");
|
|
755
|
+
const [voiceError, setVoiceError] = useState("");
|
|
756
|
+
const voiceActive = !["off", "error"].includes(voiceState);
|
|
757
|
+
useEffect(() => () => voice.current?.cancel(), []);
|
|
758
|
+
function startVoice() {
|
|
759
|
+
if(openaiVoice.current?.stop({preservePending:true})?.retained)return notify("I kept the recognized words in your draft. Review them before starting local dictation.");
|
|
760
|
+
if (busy) return notify("Wait for the current test to finish before recording.");
|
|
761
|
+
if (!session) return notify("Connect the local helper before starting dictation.");
|
|
762
|
+
let consent = false;
|
|
763
|
+
try { consent = localStorage.getItem("atmem-local-dictation") === "yes"; } catch {}
|
|
764
|
+
if (!consent) {
|
|
765
|
+
consent = window.confirm("Use your microphone for local dictation? Audio is processed on this computer by the local helper and is not saved. Words appear in the message box; only pressing Send posts them to your selected architect.");
|
|
766
|
+
if (!consent) return;
|
|
767
|
+
try { localStorage.setItem("atmem-local-dictation", "yes"); } catch {}
|
|
768
|
+
}
|
|
769
|
+
setVoiceError("");voice.current?.cancel();
|
|
770
|
+
let activeSession = session;
|
|
771
|
+
fetch('/api/voice/prepare',{method:'POST',headers:{Authorization:`Bearer ${session.token}`}}).catch(()=>{});
|
|
772
|
+
voice.current = new VoiceDictation({
|
|
773
|
+
base: input, onState: setVoiceState, onError: setVoiceError, onDraft: setInput,
|
|
774
|
+
transcribe: async (audio, signal) => {
|
|
775
|
+
const request = () => fetch("/api/voice/transcribe", {
|
|
776
|
+
method: "POST", headers: { "Content-Type": "audio/wav", Authorization: `Bearer ${activeSession.token}` },
|
|
777
|
+
body: audio, signal,
|
|
778
|
+
});
|
|
779
|
+
let response = await request();
|
|
780
|
+
if (response.status === 401 && !signal.aborted) {
|
|
781
|
+
const fresh = await refreshSession();
|
|
782
|
+
if (fresh) { activeSession = fresh; response = await request(); }
|
|
783
|
+
}
|
|
784
|
+
const result = await response.json();
|
|
785
|
+
if (!response.ok) throw Error(result.detail || "Local dictation could not finish. Nothing has been sent.");
|
|
786
|
+
return result.text;
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
void voice.current.start();
|
|
790
|
+
}
|
|
791
|
+
function stopVoice() { voice.current?.cancel();openaiVoice.current?.interrupt(); }
|
|
792
|
+
useEffect(()=>{if(buildView)stopVoice();},[buildView]);
|
|
793
|
+
async function postMessage() {
|
|
794
|
+
if (busy) return;
|
|
795
|
+
if (voice.current?.running || voice.current?.finishing) {
|
|
796
|
+
const draft = await voice.current.finish();
|
|
797
|
+
if (draft !== null) void send(draft);
|
|
798
|
+
} else void send();
|
|
799
|
+
}
|
|
800
|
+
const { screenToFlowPosition, fitView, zoomIn, zoomOut } = useReactFlow();
|
|
801
|
+
const [attention, setAttention] = useState(null);
|
|
802
|
+
const attentionRef = useRef(null);
|
|
803
|
+
const fileRef = useRef(),fileImportAsCopy=useRef(false),requirementsInput=useRef();
|
|
804
|
+
const attachmentInput=useRef();
|
|
805
|
+
const [attachments,setAttachments]=useState([]),[attachmentError,setAttachmentError]=useState(''),[readingAttachments,setReadingAttachments]=useState(false);
|
|
806
|
+
const attachmentReadLock=useRef(false);
|
|
807
|
+
async function attachFiles(files){
|
|
808
|
+
if(!files.length||attachmentReadLock.current)return;
|
|
809
|
+
attachmentReadLock.current=true;const projectAtStart=projectRef.current.id;
|
|
810
|
+
setReadingAttachments(true);setAttachmentError('');
|
|
811
|
+
try {const next=await readAttachments([...files],attachments);if(projectRef.current.id===projectAtStart)setAttachments(previous=>[...previous,...next]);}
|
|
812
|
+
catch(error){setAttachmentError(error.message);}
|
|
813
|
+
finally{attachmentReadLock.current=false;setReadingAttachments(false);}
|
|
814
|
+
}
|
|
815
|
+
const chatScroll = useChatScroll(project.id,chatOpen);
|
|
816
|
+
const latestArchitectureTurn=Array.isArray(project.extensions?.architectureActivity?.turns)?project.extensions.architectureActivity.turns.at(-1):null;
|
|
817
|
+
const runToken = useRef(0);
|
|
818
|
+
const commitGroup = useRef(false);
|
|
819
|
+
const notify = useCallback((t) => setToast(t), []);
|
|
820
|
+
// In Build the prototype card and run bar cover part of the board; fit around them.
|
|
821
|
+
const testing = mode === "review" && reviewView === "test";
|
|
822
|
+
// The notes card sits on the board's left; fit the drawing beside it, never under it.
|
|
823
|
+
const notesShown = false; // notes live in the chat header, never over the board
|
|
824
|
+
const padding = testing
|
|
825
|
+
? { top: "70px", right: "330px", bottom: "90px", left: "80px" }
|
|
826
|
+
: notesShown
|
|
827
|
+
? { top: "70px", right: "40px", bottom: "70px", left: "430px" }
|
|
828
|
+
: 0.25;
|
|
829
|
+
const refit = useCallback(
|
|
830
|
+
// Wait a frame or two so newly drawn blocks are measured before fitting them in.
|
|
831
|
+
(nodes) => setTimeout(() => {
|
|
832
|
+
if (!attentionRef.current) fitView({ padding, maxZoom: 1.1, duration: 200, ...(nodes ? { nodes } : {}) });
|
|
833
|
+
}, 180),
|
|
834
|
+
[fitView, testing, notesShown],
|
|
835
|
+
);
|
|
836
|
+
useEffect(() => {
|
|
837
|
+
refit();
|
|
838
|
+
}, [testing, notesShown, refit, pending]);
|
|
839
|
+
|
|
840
|
+
useEffect(() => {
|
|
841
|
+
if (!attention) return;
|
|
842
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
843
|
+
const zoom = setTimeout(() => {
|
|
844
|
+
if (attention.nodes.length) fitView({ nodes: attention.nodes.map(id => ({ id })), padding: 0.35, maxZoom: 1.4, duration: reduced ? 0 : 550 });
|
|
845
|
+
}, 180);
|
|
846
|
+
const clear = setTimeout(() => {
|
|
847
|
+
attentionRef.current = null;
|
|
848
|
+
setAttention(null);
|
|
849
|
+
}, 3200);
|
|
850
|
+
return () => { clearTimeout(zoom); clearTimeout(clear); };
|
|
851
|
+
}, [attention, fitView]);
|
|
852
|
+
|
|
853
|
+
const refreshSession = useCallback(async () => {
|
|
854
|
+
try {
|
|
855
|
+
const next = await discoverLocalAgent();
|
|
856
|
+
setSession(next);
|
|
857
|
+
return next;
|
|
858
|
+
} catch {
|
|
859
|
+
setSession(null);
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
}, []);
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
refreshSession();
|
|
865
|
+
const timer=setInterval(refreshSession,30000);
|
|
866
|
+
window.addEventListener("focus",refreshSession);
|
|
867
|
+
return ()=>{clearInterval(timer);window.removeEventListener("focus",refreshSession);};
|
|
868
|
+
}, [refreshSession]);
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
if (!toast) return;
|
|
871
|
+
const timer = setTimeout(() => setToast(""), 3500);
|
|
872
|
+
return () => clearTimeout(timer);
|
|
873
|
+
}, [toast]);
|
|
874
|
+
useEffect(() => {
|
|
875
|
+
try {
|
|
876
|
+
const durable = withConversation(project, messages);
|
|
877
|
+
toProjectFile(durable); // Do not persist credentials included in chat.
|
|
878
|
+
localStorage.setItem(STORAGE, JSON.stringify(durable));
|
|
879
|
+
setStorageError(false);
|
|
880
|
+
} catch {
|
|
881
|
+
setStorageError(true);
|
|
882
|
+
}
|
|
883
|
+
}, [project, messages]);
|
|
884
|
+
useEffect(
|
|
885
|
+
() => () => {
|
|
886
|
+
runToken.current++;
|
|
887
|
+
agentAbort.current?.abort();
|
|
888
|
+
},
|
|
889
|
+
[],
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const commit = useCallback(
|
|
893
|
+
(next, remember = true) => {
|
|
894
|
+
if (busy) return;
|
|
895
|
+
const current = projectRef.current;
|
|
896
|
+
if (remember) {
|
|
897
|
+
setHistory((h) => [...h.slice(-29), structuredClone(current)]);
|
|
898
|
+
setFuture([]);
|
|
899
|
+
}
|
|
900
|
+
const updated = withDerived({ ...next, revision: current.revision + 1 });
|
|
901
|
+
projectRef.current = updated;
|
|
902
|
+
setProject(updated);
|
|
903
|
+
setRunStates({});
|
|
904
|
+
setRun(null);
|
|
905
|
+
setProbe(null);
|
|
906
|
+
},
|
|
907
|
+
[busy],
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
// Brief, options, decisions and gate edits are not design changes: no revision bump, runs kept.
|
|
911
|
+
const updateEngagement = useCallback((next) => {
|
|
912
|
+
const updated = withDerived(next);
|
|
913
|
+
projectRef.current = updated;
|
|
914
|
+
setProject(updated);
|
|
915
|
+
}, []);
|
|
916
|
+
const syncEngineeringIntent=useCallback((owner,id,events)=>{const current=projectRef.current,next=synchronizeEngineeringIntent(current,id,events,owner);if(next!==current)updateEngagement(next);},[updateEngagement]);
|
|
917
|
+
const changeArchitectureWork=useCallback((change,owner)=>{const current=projectRef.current,next=change.kind==='artifact'?applyArtifactWork(current,owner,change.verified):applyArchitectureChange(current,owner,change);if(next!==current){toProjectFile(next);updateEngagement(next);}},[updateEngagement]);
|
|
918
|
+
const allIssues = checkProject(project).filter((i) => i.level !== "info");
|
|
919
|
+
// Fixture checks only matter for designs that can be test-run; sketches are reviewed in Review.
|
|
920
|
+
const runnable = project.nodes.some((n) => n.data.kind !== "note") && !project.nodes.some((n) => n.data.kind === "resource");
|
|
921
|
+
const issues = runnable ? allIssues.filter((i) => i.level === "error") : [];
|
|
922
|
+
const selectedNode = project.nodes.find((n) => n.id === selected);
|
|
923
|
+
const currentPassed = run?.status === "passed" && run.revision === project.revision;
|
|
924
|
+
const focusBlocks = useMemo(
|
|
925
|
+
() =>
|
|
926
|
+
focus ? new Set(project.nodes.filter((n) => layerOf(n).id === focus).map((n) => n.id)) : null,
|
|
927
|
+
[focus, project.nodes],
|
|
928
|
+
);
|
|
929
|
+
const focusLabel = focus && project.nodes.map(layerOf).find((l) => l.id === focus)?.label;
|
|
930
|
+
|
|
931
|
+
// Users navigate components and their details; topology stays internal.
|
|
932
|
+
function goFlow(layer = null) {
|
|
933
|
+
setLevel("flow");
|
|
934
|
+
setFocus(layer);
|
|
935
|
+
setProbe(null);
|
|
936
|
+
const ids = layer
|
|
937
|
+
? projectRef.current.nodes.filter((n) => layerOf(n).id === layer).map((n) => ({ id: n.id }))
|
|
938
|
+
: null;
|
|
939
|
+
refit(ids);
|
|
940
|
+
}
|
|
941
|
+
function openBlock(id, edge = null) {
|
|
942
|
+
setSelected(id);
|
|
943
|
+
setProbe(edge);
|
|
944
|
+
setLevel("block");
|
|
945
|
+
const n = projectRef.current.nodes.find((x) => x.id === id);
|
|
946
|
+
if (n && focus && layerOf(n).id !== focus) setFocus(layerOf(n).id);
|
|
947
|
+
}
|
|
948
|
+
function goUp() {
|
|
949
|
+
if (level === "block") goFlow(focus);
|
|
950
|
+
else if (focus) goFlow(null);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
function keys(e) {
|
|
955
|
+
const typing = /INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName);
|
|
956
|
+
if (e.key === "Escape") {
|
|
957
|
+
if (modal || menu || palette || issuesOpen) {
|
|
958
|
+
if (!(modal === "welcome" && !connection)) setModal(null);
|
|
959
|
+
setMenu(false);
|
|
960
|
+
setPalette(false);
|
|
961
|
+
setIssuesOpen(false);
|
|
962
|
+
} else if (agentAbort.current) agentAbort.current.abort();
|
|
963
|
+
else if (!typing&&!buildView) goUp();
|
|
964
|
+
}
|
|
965
|
+
if(buildView)return;
|
|
966
|
+
if (e.key === "/" && !typing) {
|
|
967
|
+
e.preventDefault();
|
|
968
|
+
setPalette(true);
|
|
969
|
+
}
|
|
970
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a" && !typing && mode === "design") {
|
|
971
|
+
e.preventDefault();
|
|
972
|
+
setLevel("flow");
|
|
973
|
+
setPicked(new Set(projectRef.current.nodes.map((n) => n.id)));
|
|
974
|
+
}
|
|
975
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z" && !typing && !modal) {
|
|
976
|
+
e.preventDefault();
|
|
977
|
+
if (e.shiftKey) redo(); else undo();
|
|
978
|
+
}
|
|
979
|
+
if ((e.key === "Delete" || e.key === "Backspace") && !typing && picked.size > 1) {
|
|
980
|
+
e.preventDefault();
|
|
981
|
+
removeNodes(picked);
|
|
982
|
+
}
|
|
983
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
document.querySelector(".runbar .run-button")?.click();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
window.addEventListener("keydown", keys);
|
|
989
|
+
return () => window.removeEventListener("keydown", keys);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
const onNodesChange = useCallback(
|
|
993
|
+
(changes) => {
|
|
994
|
+
// React Flow hides unmeasured nodes; keep sizes outside the design revision.
|
|
995
|
+
const sizes = changes.filter((c) => c.type === "dimensions" && c.dimensions);
|
|
996
|
+
if (sizes.length)
|
|
997
|
+
setMeasured((m) => ({ ...m, ...Object.fromEntries(sizes.map((c) => [c.id, c.dimensions])) }));
|
|
998
|
+
if (busy || level === "system") return;
|
|
999
|
+
const real = changes.filter((c) => c.type !== "select" && c.type !== "dimensions");
|
|
1000
|
+
if (!real.length) return;
|
|
1001
|
+
const dragging = real.some((c) => c.type === "position" && c.dragging);
|
|
1002
|
+
const remember = !commitGroup.current;
|
|
1003
|
+
commitGroup.current = dragging;
|
|
1004
|
+
const current = projectRef.current;
|
|
1005
|
+
commit({ ...current, nodes: applyNodeChanges(real, current.nodes) }, remember);
|
|
1006
|
+
},
|
|
1007
|
+
[commit, busy, level],
|
|
1008
|
+
);
|
|
1009
|
+
const onEdgesChange = useCallback(
|
|
1010
|
+
(changes) => {
|
|
1011
|
+
if (busy || level === "system") return;
|
|
1012
|
+
const real = changes.filter((c) => c.type !== "select");
|
|
1013
|
+
if (real.length)
|
|
1014
|
+
commit({ ...projectRef.current, edges: applyEdgeChanges(real, projectRef.current.edges) });
|
|
1015
|
+
},
|
|
1016
|
+
[commit, busy, level],
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
// New blocks land in the middle of the visible board, nudged so they don't stack.
|
|
1020
|
+
function centre(count) {
|
|
1021
|
+
const box = document.getElementById("canvas")?.getBoundingClientRect();
|
|
1022
|
+
const at = box
|
|
1023
|
+
? screenToFlowPosition({ x: box.left + box.width / 2, y: box.top + box.height / 2 })
|
|
1024
|
+
: { x: 90, y: 90 };
|
|
1025
|
+
return { x: at.x - 105 + (count % 4) * 24, y: at.y - 29 + (count % 4) * 24 };
|
|
1026
|
+
}
|
|
1027
|
+
function noteText(id, text) {
|
|
1028
|
+
const p = projectRef.current;
|
|
1029
|
+
commit({ ...p, nodes: p.nodes.map((n) => (n.id === id ? { ...n, data: { ...n.data, label: text.slice(0, 150), description: text.slice(0, 500) } } : n)) });
|
|
1030
|
+
}
|
|
1031
|
+
function addNote(position, attachTo = null) {
|
|
1032
|
+
const p = projectRef.current;
|
|
1033
|
+
const id = `note-${crypto.randomUUID().slice(0, 8)}`;
|
|
1034
|
+
freshNote.current = id;
|
|
1035
|
+
const note = { id, type: "resource", position, data: { label: "", kind: "note", provider: "tools", description: "", config: { region: "", classification: "Internal", timeout: 5000 }, origin: "proposed" } };
|
|
1036
|
+
commit({ ...p, nodes: [...p.nodes, note], edges: attachTo ? [...p.edges, { id: `${id}-${attachTo}`, source: id, target: attachTo, type: "smoothstep", label: "" }] : p.edges });
|
|
1037
|
+
if (level !== "flow") setLevel("flow");
|
|
1038
|
+
}
|
|
1039
|
+
function clearBoard() {
|
|
1040
|
+
const p = projectRef.current;
|
|
1041
|
+
if (!p.nodes.length) return;
|
|
1042
|
+
commit({ ...p, nodes: [], edges: [] });
|
|
1043
|
+
setPicked(new Set());
|
|
1044
|
+
setSelected(null);
|
|
1045
|
+
notify("Board cleared — Undo brings it back.");
|
|
1046
|
+
}
|
|
1047
|
+
function removeNodes(ids) {
|
|
1048
|
+
const p = projectRef.current;
|
|
1049
|
+
commit({ ...p, nodes: p.nodes.filter((n) => !ids.has(n.id)), edges: p.edges.filter((e) => !ids.has(e.source) && !ids.has(e.target)) });
|
|
1050
|
+
setPicked(new Set());
|
|
1051
|
+
setSelected(null);
|
|
1052
|
+
}
|
|
1053
|
+
// What the user changed by hand since the model last saw the board.
|
|
1054
|
+
// What the model has seen; starts as the board as it was when the page opened.
|
|
1055
|
+
const seen = useRef(null);
|
|
1056
|
+
if (!seen.current) seen.current = structuredClone(project);
|
|
1057
|
+
const paletteAt = useRef(null);
|
|
1058
|
+
const freshNote = useRef(null);
|
|
1059
|
+
function canvasChanges(now) {
|
|
1060
|
+
const was = seen.current;
|
|
1061
|
+
if (!was) return [];
|
|
1062
|
+
const before = new Map(was.nodes.map((n) => [n.id, n])),
|
|
1063
|
+
after = new Map(now.nodes.map((n) => [n.id, n]));
|
|
1064
|
+
const name = (n) => (n.data.kind === "note" ? `note “${n.data.label}”` : n.data.label);
|
|
1065
|
+
const out = [];
|
|
1066
|
+
for (const n of now.nodes) if (!before.has(n.id)) out.push(`added ${name(n)}`);
|
|
1067
|
+
for (const n of was.nodes) if (!after.has(n.id)) out.push(`removed ${name(n)}`);
|
|
1068
|
+
for (const n of now.nodes) if (before.has(n.id) && before.get(n.id).data.label !== n.data.label) out.push(`renamed “${before.get(n.id).data.label}” to “${n.data.label}”`);
|
|
1069
|
+
const wire = (e) => `${e.source}>${e.target}`;
|
|
1070
|
+
const had = new Set(was.edges.map(wire)),
|
|
1071
|
+
has = new Set(now.edges.map(wire));
|
|
1072
|
+
const label = (id) => (after.get(id) || before.get(id))?.data.label || id;
|
|
1073
|
+
for (const e of now.edges) if (!had.has(wire(e))) out.push(`drew an arrow ${label(e.source)} → ${label(e.target)}`);
|
|
1074
|
+
for (const e of was.edges) if (!has.has(wire(e))) out.push(`removed the arrow ${label(e.source)} → ${label(e.target)}`);
|
|
1075
|
+
return out.slice(0, 40);
|
|
1076
|
+
}
|
|
1077
|
+
function addComponent(item, position) {
|
|
1078
|
+
if (busy) return;
|
|
1079
|
+
const next = structuredClone(project);
|
|
1080
|
+
const id = crypto.randomUUID();
|
|
1081
|
+
next.nodes.push({
|
|
1082
|
+
id,
|
|
1083
|
+
type: "resource",
|
|
1084
|
+
position: position || centre(next.nodes.length),
|
|
1085
|
+
data: {
|
|
1086
|
+
...item,
|
|
1087
|
+
config: { region: "To be selected", classification: "Internal", timeout: 5000 },
|
|
1088
|
+
origin: "proposed",
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
commit(next);
|
|
1092
|
+
setSelected(id);
|
|
1093
|
+
setPalette(false);
|
|
1094
|
+
if (level !== "flow") goFlow(null);
|
|
1095
|
+
notify(`${item.label} added — wire its ports.`);
|
|
1096
|
+
}
|
|
1097
|
+
function updateNode(node) {
|
|
1098
|
+
commit({ ...project, nodes: project.nodes.map((n) => (n.id === node.id ? node : n)) });
|
|
1099
|
+
}
|
|
1100
|
+
function removeNode(id) {
|
|
1101
|
+
commit({
|
|
1102
|
+
...project,
|
|
1103
|
+
nodes: project.nodes.filter((n) => n.id !== id),
|
|
1104
|
+
edges: project.edges.filter((e) => e.source !== id && e.target !== id),
|
|
1105
|
+
});
|
|
1106
|
+
setSelected(null);
|
|
1107
|
+
goFlow(focus);
|
|
1108
|
+
}
|
|
1109
|
+
function undo() {
|
|
1110
|
+
if (!history.length || busy || agentBusy) return;
|
|
1111
|
+
const previous = history.at(-1);
|
|
1112
|
+
setFuture((f) => [...f.slice(-29), structuredClone(projectRef.current)]);
|
|
1113
|
+
setHistory((h) => h.slice(0, -1));
|
|
1114
|
+
// Undo takes back the design, never what the user has told us since.
|
|
1115
|
+
commit(keepEngagement(previous, projectRef.current), false);
|
|
1116
|
+
setSelected(null);
|
|
1117
|
+
setPending(null);
|
|
1118
|
+
notify("Board change undone. Redo restores it.");
|
|
1119
|
+
}
|
|
1120
|
+
function redo() {
|
|
1121
|
+
if (!future.length || busy || agentBusy) return;
|
|
1122
|
+
const next = future.at(-1);
|
|
1123
|
+
setHistory((h) => [...h.slice(-29), structuredClone(projectRef.current)]);
|
|
1124
|
+
setFuture((f) => f.slice(0, -1));
|
|
1125
|
+
commit(keepEngagement(next, projectRef.current), false);
|
|
1126
|
+
setSelected(null);
|
|
1127
|
+
setPending(null);
|
|
1128
|
+
notify("Board change redone.");
|
|
1129
|
+
}
|
|
1130
|
+
function download(data, name, type = "application/json") {
|
|
1131
|
+
const url = URL.createObjectURL(
|
|
1132
|
+
new Blob(
|
|
1133
|
+
[data instanceof Uint8Array || typeof data === "string" ? data : JSON.stringify(data, null, 2)],
|
|
1134
|
+
{ type },
|
|
1135
|
+
),
|
|
1136
|
+
);
|
|
1137
|
+
const a = document.createElement("a");
|
|
1138
|
+
a.href = url;
|
|
1139
|
+
a.download = name;
|
|
1140
|
+
a.click();
|
|
1141
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
1142
|
+
}
|
|
1143
|
+
async function importProject(e) {
|
|
1144
|
+
const file = e.target.files?.[0];
|
|
1145
|
+
if (!file) return;
|
|
1146
|
+
try {
|
|
1147
|
+
if (file.size > MAX_FILE_BYTES) throw Error("Project files must be 2 MB or smaller.");
|
|
1148
|
+
const content = await file.text();
|
|
1149
|
+
const raw = JSON.parse(content);
|
|
1150
|
+
if (raw.format || raw.version === 1) {
|
|
1151
|
+
const { project: opened, migrated, from } = parseProjectFile(content);
|
|
1152
|
+
loadDesign(fileImportAsCopy.current?copyProject(opened,crypto.randomUUID()):opened, { own: true });
|
|
1153
|
+
notify(fileImportAsCopy.current?"New project created from the saved design. The original is preserved.":migrated ? `Project opened and upgraded from ${from}.` : "Project opened.");
|
|
1154
|
+
} else {
|
|
1155
|
+
loadDesign(importUpstreamProject(raw));
|
|
1156
|
+
notify("OpenArchFlow design imported.");
|
|
1157
|
+
}
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
notify(error.message);
|
|
1160
|
+
}
|
|
1161
|
+
e.target.value = "";
|
|
1162
|
+
}
|
|
1163
|
+
// A project file brings its own records; templates and imports keep the current ones.
|
|
1164
|
+
function loadDesign(next, { own = false } = {}) {
|
|
1165
|
+
stopVoice();
|
|
1166
|
+
attentionRef.current = null;
|
|
1167
|
+
setAttention(null);
|
|
1168
|
+
if (busy) {
|
|
1169
|
+
notify("Stop the run before replacing the design.");
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
seen.current = structuredClone(next);
|
|
1173
|
+
// A project file brings its own engagement; templates and imports keep the current one.
|
|
1174
|
+
if (own) {
|
|
1175
|
+
// Project boundaries keep separate history; never blend records from different projects.
|
|
1176
|
+
setHistory([]);
|
|
1177
|
+
setFuture([]);
|
|
1178
|
+
projectRef.current = next;
|
|
1179
|
+
setProject(withDerived(next));
|
|
1180
|
+
setRunStates({});
|
|
1181
|
+
setRun(null);
|
|
1182
|
+
setProbe(null);
|
|
1183
|
+
setAttachments([]);setAttachmentError('');
|
|
1184
|
+
setMessages(conversationMessages(next, [welcome(next)]));
|
|
1185
|
+
} else commit(validateImport(keepEngagement(next, projectRef.current)));
|
|
1186
|
+
setSelected(null);
|
|
1187
|
+
setPending(null);
|
|
1188
|
+
setScenario("happy");
|
|
1189
|
+
setView("diagram");
|
|
1190
|
+
setMode("design");
|
|
1191
|
+
setDesignView("board");
|
|
1192
|
+
setModal(null);
|
|
1193
|
+
const canvasView=readCanvasView(next);
|
|
1194
|
+
setLevel(canvasView.level);setFocus(canvasView.focus);setSelected(canvasView.selected);setTopologyPositions(canvasView.positions);
|
|
1195
|
+
refit();
|
|
1196
|
+
if (next.importReport) notify(next.importReport.notice);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const coachedReports=useRef(new WeakMap());
|
|
1200
|
+
const emulationMentorKey=JSON.stringify([connection?.provider,connection?.model]);
|
|
1201
|
+
useEffect(()=>{
|
|
1202
|
+
const report=emulation.result;
|
|
1203
|
+
if(buildView||intakeRef.current.some(q=>q.projectId===project.id)||!emulation.coach||emulation.running||!report||!emulationCoachReady(session,connection)||busy||agentBusy||pending||coachedReports.current.get(report)?.has(emulationMentorKey))return;
|
|
1204
|
+
void discussEmulation(report,'coach');
|
|
1205
|
+
},[buildView,emulation.result,emulation.running,emulation.coach,session,connection,busy,agentBusy,pending]);
|
|
1206
|
+
|
|
1207
|
+
async function discussEmulation(report,intent){
|
|
1208
|
+
if(busy||agentBusy||pending||report!==emulation.result)return;
|
|
1209
|
+
if(intent==='question'){setChatOpen(true);setInput(current=>current||'Help me understand these local check results: ');requestAnimationFrame(()=>messageInput.current?.focus());return;}
|
|
1210
|
+
coachedReports.current.set(report,new Set([...(coachedReports.current.get(report)||[]),emulationMentorKey]));
|
|
1211
|
+
setChatOpen(true);
|
|
1212
|
+
const guidance=emulationGuidance(projectRef.current,report);
|
|
1213
|
+
if(guidance&&!guidance.needsModel){
|
|
1214
|
+
const text=guidanceMessage(guidance);setMessages(m=>m.at(-1)?.role==='assistant'&&m.at(-1)?.text===text?m:[...m,{role:'assistant',text}]);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if(intent!=='coach')setMessages(m=>[...m,{role:'user',text:'Help me with the next test step.'}]);
|
|
1218
|
+
await send(emulationPrompt(projectRef.current,report,intent),'emulation',{mode:'design',view:'emulation'});
|
|
1219
|
+
}
|
|
1220
|
+
async function send(text = input, origin = "chat", destination = null, reviewTarget = null, intakeOptions = {}) {
|
|
1221
|
+
if ((!text.trim() && !(origin === "chat" && attachments.length)) || busy || readingAttachments) return;
|
|
1222
|
+
if(origin!=='chat'&&(agentBusy||agentAbort.current))return;
|
|
1223
|
+
if(text.length>8000){notify('Keep each message within 8,000 characters; your draft is unchanged.');return;}
|
|
1224
|
+
stopVoice();
|
|
1225
|
+
const message = text.trim() || "Please review the attached files and help me use them in this architecture.";
|
|
1226
|
+
const estimateOnly=intakeOptions.estimateOnly===true;
|
|
1227
|
+
const sentAttachments=intakeOptions.attachments||(origin === "chat"?attachments:[]);
|
|
1228
|
+
if(sentAttachments.length>4){notify('Keep up to four files in one update; your draft and files are unchanged.');return;}
|
|
1229
|
+
if(sentAttachments.some(a=>!a.data)){notify('Attach the missing original files again before sending, or remove their references to continue with text.');return;}
|
|
1230
|
+
const requestIds=intakeOptions.requestIds||(origin==='chat'?[crypto.randomUUID()]:[]);
|
|
1231
|
+
const requestProject=projectRef.current.id;
|
|
1232
|
+
if(origin==='chat'&&!intakeOptions.alreadyLogged&&(agentBusy||agentAbort.current||intakeRef.current.some(q=>q.projectId===requestProject))){
|
|
1233
|
+
const id=requestIds[0];updateIntake([...intakeRef.current,{id,projectId:requestProject,text:message,attachments:sentAttachments}]);
|
|
1234
|
+
setMessages(m=>[...m,{role:'user',text:message,attachments:sentAttachments,requestId:id,workState:'queued'}]);
|
|
1235
|
+
setInput('');setAttachments([]);chatScroll.followLatest();return;
|
|
1236
|
+
}
|
|
1237
|
+
if ((origin === "chat" || origin === "block-review")&&!intakeOptions.alreadyLogged) {
|
|
1238
|
+
setMessages((m) => [...m, { role: "user", text: reviewTarget ? `Review ${reviewTarget.label} with me.` : message, attachments:sentAttachments,...(requestIds.length?{requestId:requestIds[0],workState:'processing'}:{}) }]);
|
|
1239
|
+
setInput("");
|
|
1240
|
+
}
|
|
1241
|
+
if(origin==='chat'){setPending(null);if(!intakeOptions.alreadyLogged)setAttachments([]);}
|
|
1242
|
+
if (origin === "chat") setChatOpen(true);
|
|
1243
|
+
if (!connection) {
|
|
1244
|
+
setRequestState(requestIds,'failed');
|
|
1245
|
+
setModal("welcome");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
if (/^compare all four clouds/i.test(message)) {
|
|
1249
|
+
setMode("design");
|
|
1250
|
+
setDesignView("options");
|
|
1251
|
+
}
|
|
1252
|
+
if (!session) {
|
|
1253
|
+
setRequestState(requestIds,'failed');
|
|
1254
|
+
setMessages((m) => [
|
|
1255
|
+
...m,
|
|
1256
|
+
{ role: "assistant", text: "Looks like we've lost the connection, so that message didn't go out. Let's fix it.", fix: true },
|
|
1257
|
+
]);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if(origin==='chat'&&!intakeOptions.alreadyLogged)setIntakePaused(false);
|
|
1261
|
+
const controller = new AbortController();
|
|
1262
|
+
agentAbort.current = controller;
|
|
1263
|
+
chatScroll.followLatest();
|
|
1264
|
+
setAgentBusy(true);
|
|
1265
|
+
const activityId=session.architectureActivityEnabled?crypto.randomUUID():null;
|
|
1266
|
+
try {
|
|
1267
|
+
if(activityId){
|
|
1268
|
+
const next=recordArchitectureTurn(projectRef.current,requestProject,{id:activityId,provider:connection.provider,model:connection.model,effort:connection.effort,origin,requestIds:[...requestIds],startedAt:new Date().toISOString()});
|
|
1269
|
+
toProjectFile(next);
|
|
1270
|
+
updateEngagement(next);
|
|
1271
|
+
}
|
|
1272
|
+
// Keep the discovery record current from the user's words, whatever model replies.
|
|
1273
|
+
const was = projectRef.current;
|
|
1274
|
+
const heard = origin === "chat" ? understand(message, was) : { project: was };
|
|
1275
|
+
updateEngagement(heard.project);
|
|
1276
|
+
const signals = harnessSignals(was, heard.project);
|
|
1277
|
+
// The connected architect draws from the user's idea. No local preset blocks.
|
|
1278
|
+
const edits = canvasChanges(projectRef.current);
|
|
1279
|
+
seen.current = structuredClone(projectRef.current);
|
|
1280
|
+
const result = await requestArchitect({
|
|
1281
|
+
extra: [{type:"discovery",id:"active-view",text:`The user is working in ${destination ? (destination.mode === "design" ? destination.view : destination.mode) : mode === "design" ? designView : mode}. Use the shared diagram and conversation. Prioritise major system decisions over configuration details; ask only when a material user decision is needed. Otherwise add or refine the needed components and their connections. Keep topology reasoning internal; use plain component names and short explanations for the user.`}, ...(edits.length ? [{ type: "discovery", id: "canvas-changes", text: `Since your last reply the user ${edits.join("; ")}. Build on these changes.` }] : [])],
|
|
1282
|
+
session,
|
|
1283
|
+
connection,
|
|
1284
|
+
message,
|
|
1285
|
+
attachments:sentAttachments,
|
|
1286
|
+
project: projectRef.current,
|
|
1287
|
+
history: intakeOptions.history||messages,
|
|
1288
|
+
stage: destination?.mode || mode,
|
|
1289
|
+
origin,
|
|
1290
|
+
signal: controller.signal,
|
|
1291
|
+
activityId,
|
|
1292
|
+
});
|
|
1293
|
+
if(projectRef.current.id!==requestProject)return;
|
|
1294
|
+
if(activityId)updateEngagement(finishArchitectureTurn(projectRef.current,requestProject,activityId,'response_received',result.architectureActivity?.detailsAvailable!==false));
|
|
1295
|
+
if(intakeRef.current.some(q=>q.projectId===requestProject)){
|
|
1296
|
+
carryIntent.current={projectId:requestProject,text:carryIntent.current?.projectId===requestProject?carryIntent.current.text:message};
|
|
1297
|
+
setRequestState(requestIds,'superseded');
|
|
1298
|
+
setMessages(m=>[...m,{role:'assistant',text:'I’ve kept your updates. I’m combining them before changing the design.',...(activityId?{activityId}:{})}]);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
carryIntent.current=null;
|
|
1302
|
+
setRequestState(requestIds,'complete');
|
|
1303
|
+
if (!estimateOnly && !["document","emulation","block-review"].includes(origin) && result.optionAssessment?.length) {
|
|
1304
|
+
const p = projectRef.current, a = p.assessment || defaultAssessment();
|
|
1305
|
+
const scores = structuredClone(a.scores), notes = { ...a.notes };
|
|
1306
|
+
for (const option of result.optionAssessment) {
|
|
1307
|
+
scores[option.provider] = option.scores;
|
|
1308
|
+
notes[option.provider] = `AtMem AI Architect proposal — confirm: ${option.rationale}`;
|
|
1309
|
+
}
|
|
1310
|
+
updateEngagement({ ...p, assessment: { ...a, scores, notes } });
|
|
1311
|
+
signals.push("cloud comparison updated");
|
|
1312
|
+
}
|
|
1313
|
+
if (!estimateOnly && !["document","emulation","block-review"].includes(origin) && result.usageUpdates?.length) {
|
|
1314
|
+
let next = projectRef.current;
|
|
1315
|
+
for (const update of result.usageUpdates) if (message.includes(update.sourceText)) next = setUsage(next, update.key, update.value);
|
|
1316
|
+
updateEngagement(next);
|
|
1317
|
+
}
|
|
1318
|
+
if (origin === "document" && result.requirementUpdates?.length) {
|
|
1319
|
+
updateEngagement(proposeCommentRevisions(projectRef.current,was,result.requirementUpdates));
|
|
1320
|
+
signals.push("suggested wording ready to review in Requirements");
|
|
1321
|
+
} else if (!estimateOnly && origin!=="emulation" && result.requirementUpdates?.length) {
|
|
1322
|
+
const p = projectRef.current;
|
|
1323
|
+
const updates = new Map(result.requirementUpdates.filter(u => message.includes(u.sourceText)).map(u => [u.id,u]));
|
|
1324
|
+
const applicable = new Map([...updates].filter(([id]) => was.needs?.find(r=>r.id===id)?.statement === p.needs?.find(r=>r.id===id)?.statement));
|
|
1325
|
+
updateEngagement({ ...p, needs: (p.needs || []).map(r => applicable.has(r.id) ? { ...r, statement: applicable.get(r.id).statement, source: "user", basis:`User statement: ${applicable.get(r.id).sourceText}`, ...(applicable.get(r.id).rationale?{rationale:applicable.get(r.id).rationale}:{}) } : r),
|
|
1326
|
+
extensions: {...p.extensions,requirementComments:(p.extensions?.requirementComments || []).map(c=> applicable.has(c.requirementId) && applicable.get(c.requirementId).sourceText === c.text ? {...c,status:'reviewed',revisedStatement:applicable.get(c.requirementId).statement} : c)} });
|
|
1327
|
+
signals.push("requirements refined from your answer");
|
|
1328
|
+
}
|
|
1329
|
+
if (!estimateOnly && !["document","emulation","block-review"].includes(origin) && result.decisionUpdates?.length) updateEngagement(applyDecisionUpdates(projectRef.current,result.decisionUpdates,message));
|
|
1330
|
+
if (!["document","emulation","block-review"].includes(origin) && result.priceQuotes?.length) updateEngagement(applyPriceQuotes(projectRef.current, result.priceQuotes));
|
|
1331
|
+
if(['chat','workflow','block-review'].includes(origin)){
|
|
1332
|
+
const p=projectRef.current,q=result.followUp;
|
|
1333
|
+
updateEngagement({...p,lastAsked:q?{id:'architect-question',topic:q.topic||'',question:q.question,why:q.why,beginner:q.example||''}:null,extensions:{...p.extensions,activeConsultation:{hasQuestion:!!q}}});
|
|
1334
|
+
}
|
|
1335
|
+
if(origin==='block-review'&&reviewTarget&&result.componentReview){
|
|
1336
|
+
try {updateEngagement(applyComponentReview(projectRef.current,reviewTarget,result.componentReview));signals.push('block review and linked questions recorded');}
|
|
1337
|
+
catch(error){notify(error.message);}
|
|
1338
|
+
}
|
|
1339
|
+
setMessages((m) => [...m, { role: "assistant", ...(activityId?{activityId}:{}), text: origin==="emulation"?groundEmulationMessage(emulation.result,result.message,projectRef.current):result.message, live: true, signals, contextUsed: result.contextUsed }]);
|
|
1340
|
+
openaiVoice.current?.speak(origin==="emulation"?groundEmulationMessage(emulation.result,result.message,projectRef.current):result.message);
|
|
1341
|
+
if (!estimateOnly && result.proposal && !["document","block-review"].includes(origin)) {
|
|
1342
|
+
const proposal = { ...result.proposal, next: validateImport(result.proposal.next), ...(origin==="emulation"?{emulationCorrection:true}:{}) };
|
|
1343
|
+
if(origin==="emulation")guardEmulationCorrection(was,proposal.next);
|
|
1344
|
+
// "Draw as we talk": the design evolves with the conversation; each step is undoable.
|
|
1345
|
+
if (drawLive && origin !== "preview") applyProposal(proposal, { live: true });
|
|
1346
|
+
else setPending(proposal);
|
|
1347
|
+
}
|
|
1348
|
+
if (!estimateOnly && !["document","preview","emulation","block-review"].includes(origin) && result.requirementDrafts?.length) updateEngagement(applyRequirementDrafts(projectRef.current,result.requirementDrafts));
|
|
1349
|
+
return result.message;
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if(projectRef.current.id!==requestProject)return;
|
|
1352
|
+
if(activityId)updateEngagement(finishArchitectureTurn(projectRef.current,requestProject,activityId,error.name==='AbortError'?'cancelled':'failed'));
|
|
1353
|
+
setRequestState(requestIds,'failed');setIntakePaused(true);
|
|
1354
|
+
setMessages((m) => [
|
|
1355
|
+
...m,
|
|
1356
|
+
error.name === "AbortError"
|
|
1357
|
+
? { role: "assistant", ...(activityId?{activityId}:{}), text: "Stopped. Nothing on the board changed." }
|
|
1358
|
+
: { role: "assistant", ...(activityId?{activityId}:{}), text: `Hmm, that didn't work: ${error.message}`, fix: /connect|sign|model|running|service|failed|limit/i.test(error.message) },
|
|
1359
|
+
]);
|
|
1360
|
+
} finally {
|
|
1361
|
+
if (agentAbort.current === controller) {
|
|
1362
|
+
setAgentBusy(false);
|
|
1363
|
+
agentAbort.current = null;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async function reviewComponent(id){
|
|
1368
|
+
const current=projectRef.current,item=componentReviews(current).find(r=>r.id===id);if(!item)return;
|
|
1369
|
+
setChatOpen(true);
|
|
1370
|
+
return send(`Review this block against its latest requirements: ${item.label} (id: ${id}). Check responsibility, inputs/outputs, ownership, identity, state, failures and dependencies. Prioritise consequential system decisions over ports or packaging. Explain what is sound; ask only material unresolved questions. Preserve all scope and existing systems. Do not claim runtime validation. Current concerns: ${item.issues.map(i=>i.text).join('; ')||'None recorded; inspect for missing decisions.'}. Return componentReview with componentId ${id}, summary and questions. Return followUp only if a user decision is needed; do not return a graph change.`,"block-review",null,item);
|
|
1371
|
+
}
|
|
1372
|
+
// Applying a proposal is a design decision: it is recorded, and Undo restores the previous design.
|
|
1373
|
+
function applyProposal(proposal = pending, { live = false, quiet = false } = {}) {
|
|
1374
|
+
const current = projectRef.current;
|
|
1375
|
+
if (proposal.baseRevision !== current.revision) {
|
|
1376
|
+
notify("The design changed since this proposal. Ask again.");
|
|
1377
|
+
setPending(null);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if(proposal.emulationCorrection){guardEmulationCorrection(current,proposal.next);emulation.scheduleRetest();}
|
|
1381
|
+
// A newly drawn graph needs room for arrow labels. Preserve user-arranged
|
|
1382
|
+
// positions on refinements and any diagram that retains existing node IDs.
|
|
1383
|
+
if (proposal.next.nodes.length && !proposal.next.nodes.some(n => current.nodes.some(existing => existing.id === n.id))) {
|
|
1384
|
+
proposal = { ...proposal, next: autoLayout(proposal.next) };
|
|
1385
|
+
}
|
|
1386
|
+
const decision = {
|
|
1387
|
+
...newDecision(current),
|
|
1388
|
+
subject: `design.${slugSubject(proposal.title)}`,
|
|
1389
|
+
title: proposal.title,
|
|
1390
|
+
status: "Accepted",
|
|
1391
|
+
context: proposal.body || "",
|
|
1392
|
+
decision: `${live ? "Drawn while talking" : "Applied"} as design revision ${current.revision + 1}.`,
|
|
1393
|
+
};
|
|
1394
|
+
const change = proposalDiff(current, proposal.next);
|
|
1395
|
+
const emphasis = diagramAttention(current, proposal.next);
|
|
1396
|
+
attentionRef.current = emphasis;
|
|
1397
|
+
setAttention(emphasis);
|
|
1398
|
+
// Remember the concept we drew so we can keep evolving it until the user takes over.
|
|
1399
|
+
commit({ ...keepEngagement(proposal.next, current), decisions: [...(current.decisions || []), decision], conceptSig: quiet ? conceptSignature(proposal.next) : null });
|
|
1400
|
+
seen.current = structuredClone(proposal.next);
|
|
1401
|
+
const summary = [
|
|
1402
|
+
...change.added.map((l) => `+ ${l}`),
|
|
1403
|
+
...change.changed.map((l) => `~ ${l}`),
|
|
1404
|
+
...change.removed.map((l) => `− ${l}`),
|
|
1405
|
+
].join("\n");
|
|
1406
|
+
if (!quiet) setMessages((m) => [
|
|
1407
|
+
...m,
|
|
1408
|
+
{
|
|
1409
|
+
role: "assistant",
|
|
1410
|
+
text: `${live ? "Drew it" : "Applied"}: ${proposal.title}${summary ? `\n${summary.split("\n").slice(0, 3).join("\n")}${summary.split("\n").length > 3 ? `\n+${summary.split("\n").length - 3} more` : ""}` : ""}`,
|
|
1411
|
+
signals: [`${live ? "drew" : "applied"} ${proposal.next.nodes.length} blocks`, `logged ${decision.id.toUpperCase()}`, "undo ↶"],
|
|
1412
|
+
},
|
|
1413
|
+
]);
|
|
1414
|
+
setPending(null);
|
|
1415
|
+
setSelected(null);
|
|
1416
|
+
setScenario("happy");
|
|
1417
|
+
if (mode !== "design" || designView !== "board") {
|
|
1418
|
+
setMode("design");
|
|
1419
|
+
setDesignView("board");
|
|
1420
|
+
}
|
|
1421
|
+
setLevel(previous=>previous==='block'?'flow':previous);
|
|
1422
|
+
setFocus(null);
|
|
1423
|
+
refit();
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function exportPulumi() {
|
|
1427
|
+
const files = pulumiScaffold(project, environment);
|
|
1428
|
+
download(
|
|
1429
|
+
zipSync(Object.fromEntries(Object.entries(files).map(([name, text]) => [name, strToU8(text)]))),
|
|
1430
|
+
`atmem-ai-architect-${environment}-pulumi.zip`,
|
|
1431
|
+
"application/zip",
|
|
1432
|
+
);
|
|
1433
|
+
notify("Pulumi scaffold exported. Resource implementations are not generated.");
|
|
1434
|
+
}
|
|
1435
|
+
function cancelRun() {
|
|
1436
|
+
runToken.current++;
|
|
1437
|
+
setBusy(false);
|
|
1438
|
+
setRun((r) => (r ? { ...r, status: "cancelled" } : r));
|
|
1439
|
+
setRunStates((s) =>
|
|
1440
|
+
Object.fromEntries(Object.entries(s).map(([id, state]) => [id, state === "running" ? "cancelled" : state])),
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
async function runFlow() {
|
|
1444
|
+
if (busy) return;
|
|
1445
|
+
if (!runnable || issues.length) return;
|
|
1446
|
+
const revision = project.revision,
|
|
1447
|
+
p = structuredClone({ ...project, nodes: project.nodes.filter((n) => n.data.kind !== "note"), edges: project.edges.filter((e) => !project.nodes.some((n) => n.data.kind === "note" && (n.id === e.source || n.id === e.target))) }),
|
|
1448
|
+
token = ++runToken.current;
|
|
1449
|
+
setBusy(true);
|
|
1450
|
+
setRunStates({});
|
|
1451
|
+
const steps = [],
|
|
1452
|
+
outputs = {},
|
|
1453
|
+
states = {};
|
|
1454
|
+
setRun({ status: "running", revision, scenario, steps: [] });
|
|
1455
|
+
let failed = false;
|
|
1456
|
+
try {
|
|
1457
|
+
for (const n of orderProject(p)) {
|
|
1458
|
+
if (token !== runToken.current) return;
|
|
1459
|
+
const parents = p.edges.filter((e) => e.target === n.id);
|
|
1460
|
+
// Several inputs are merged, in wire order.
|
|
1461
|
+
const input = parents.length ? Object.assign({}, ...parents.map((e) => outputs[e.source])) : null;
|
|
1462
|
+
if (parents.some((e) => states[e.source] === "failed" || states[e.source] === "skipped")) {
|
|
1463
|
+
states[n.id] = "skipped";
|
|
1464
|
+
steps.push({ id: n.id, label: n.data.label, status: "skipped", input: null, output: null, error: "Not executed because an upstream step failed.", duration: 0 });
|
|
1465
|
+
setRunStates({ ...states });
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
states[n.id] = "running";
|
|
1469
|
+
setRunStates({ ...states });
|
|
1470
|
+
const started = performance.now();
|
|
1471
|
+
await new Promise((resolve) => setTimeout(resolve, 380));
|
|
1472
|
+
if (token !== runToken.current) return;
|
|
1473
|
+
try {
|
|
1474
|
+
const output = executeBlock(n, input, scenario);
|
|
1475
|
+
outputs[n.id] = output;
|
|
1476
|
+
states[n.id] = "passed";
|
|
1477
|
+
steps.push({ id: n.id, label: n.data.label, status: "passed", input, output, duration: Math.round(performance.now() - started) });
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
failed = true;
|
|
1480
|
+
states[n.id] = "failed";
|
|
1481
|
+
steps.push({ id: n.id, label: n.data.label, status: "failed", input, output: null, error: error.message, duration: Math.round(performance.now() - started) });
|
|
1482
|
+
}
|
|
1483
|
+
setRunStates({ ...states });
|
|
1484
|
+
setRun({ status: "running", revision, scenario, steps: [...steps] });
|
|
1485
|
+
}
|
|
1486
|
+
setRun({ status: failed ? "failed" : "passed", revision, scenario, steps: [...steps] });
|
|
1487
|
+
} finally {
|
|
1488
|
+
if (token === runToken.current) setBusy(false);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Canvas content per level.
|
|
1493
|
+
// A pending proposal is drawn on the board straight away, as a preview, until applied or dismissed.
|
|
1494
|
+
const shown = pending?.next && mode === "design" ? pending.next : project;
|
|
1495
|
+
const previewing = shown !== project;
|
|
1496
|
+
const blockReviews = useMemo(()=>componentReviews(shown),[shown]);
|
|
1497
|
+
const reviewBlock = id => {setMode('design');setDesignView('board');setFocus(null);openBlock(id);};
|
|
1498
|
+
const system = useMemo(() => systemView(shown, runStates), [shown, runStates]);
|
|
1499
|
+
const wireClass = (source) =>
|
|
1500
|
+
`wire ${runStates[source] === "failed" ? "wire-failed" : runStates[source] === "passed" ? "wire-passed" : ""}`;
|
|
1501
|
+
const canvasNodes =
|
|
1502
|
+
level === "system"
|
|
1503
|
+
? system.nodes.map((n) => ({ ...n, position:topologyPositions[n.id]||n.position, data:{...n.data,onExplore:()=>n.data.blocks.length===1?reviewBlock(n.data.blocks[0]):goFlow(n.data.layer),onReview:()=>reviewBlock(blockReviews.find(r=>n.data.blocks.includes(r.id)&&r.issues.length)?.id||n.data.blocks[0]),reviewSummary:{reviewed:blockReviews.filter(r=>n.data.blocks.includes(r.id)&&r.status==='reviewed').length,issues:blockReviews.filter(r=>n.data.blocks.includes(r.id)).flatMap(r=>r.issues)}}, measured: measured[n.id], className: previewing ? "preview" : "" }))
|
|
1504
|
+
: shown.nodes.map((n) => ({
|
|
1505
|
+
...n,
|
|
1506
|
+
type: n.data.kind === "note" ? "note" : n.type,
|
|
1507
|
+
measured: measured[n.id],
|
|
1508
|
+
className: `${previewing ? "preview" : ""} ${attention?.nodes.includes(n.id) ? "diagram-attention" : ""}`,
|
|
1509
|
+
selected: n.id === selected || picked.has(n.id),
|
|
1510
|
+
data: { ...n.data, blockReview:blockReviews.find(r=>r.id===n.id), onReview:()=>reviewBlock(n.id), onText: noteText, fresh: n.id === freshNote.current, runState: runStates[n.id], dim: (focusBlocks && !focusBlocks.has(n.id)) || (testing && !n.data.poc && n.data.kind !== "resource" && project.nodes.some((x) => x.data.poc)) },
|
|
1511
|
+
}));
|
|
1512
|
+
const canvasEdges =
|
|
1513
|
+
level === "system"
|
|
1514
|
+
? system.edges.map((e) => ({ ...e, className: wireClass(e.data.sources[0]), markerEnd: { type: "arrowclosed", width: 14, height: 14 } }))
|
|
1515
|
+
: shown.edges.map((e) => ({
|
|
1516
|
+
...e,
|
|
1517
|
+
className: `${previewing ? "preview" : ""} ${attention?.edges.includes(e.id) ? "diagram-attention" : ""} ${wireClass(e.source)} ${focusBlocks && !(focusBlocks.has(e.source) || focusBlocks.has(e.target)) ? "dim" : ""}`,
|
|
1518
|
+
animated: busy && runStates[e.source] === "passed",
|
|
1519
|
+
markerEnd: { type: "arrowclosed", width: 14, height: 14 },
|
|
1520
|
+
}));
|
|
1521
|
+
const slug = project.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "atflow";
|
|
1522
|
+
const exportDocument = () => {
|
|
1523
|
+
download(deploymentDocument(project, run), `${slug}-deployment-document.md`, "text/markdown");
|
|
1524
|
+
notify("Deployment document generated from the current project.");
|
|
1525
|
+
};
|
|
1526
|
+
const exportFiles = (files, suffix) => {
|
|
1527
|
+
download(zipSync(Object.fromEntries(Object.entries(files).map(([name, content]) => [name, typeof content === "string" ? strToU8(content) : content]))), `${slug}-r${project.revision}-${suffix}.zip`, "application/zip");
|
|
1528
|
+
};
|
|
1529
|
+
const downloadBundle = bundle => {
|
|
1530
|
+
download(zipSync(Object.fromEntries(Object.entries(bundle.files).map(([name,content])=>[name,typeof content === "string"?strToU8(content):content]))),bundle.filename,"application/zip");
|
|
1531
|
+
notify("Reviewed output package downloaded.");
|
|
1532
|
+
};
|
|
1533
|
+
const exportPack = async () => {
|
|
1534
|
+
try {
|
|
1535
|
+
notify("Preparing documents and diagram exports…");
|
|
1536
|
+
downloadBundle(await outputBundle(project,run,environment));
|
|
1537
|
+
} catch(e) { notify(e.message); }
|
|
1538
|
+
};
|
|
1539
|
+
const exportTerraform = async (validation = null) => {
|
|
1540
|
+
try {
|
|
1541
|
+
toProjectFile(project); // Apply the same credential refusal as every project export.
|
|
1542
|
+
const files=terraformSketch(project,environment);
|
|
1543
|
+
const receipt=await verifiedReceipt(files,validation);
|
|
1544
|
+
exportFiles({...files,...(receipt?{'validation-receipt.json':JSON.stringify(receipt,null,2)}:{})}, "terraform");
|
|
1545
|
+
notify("Terraform sketch exported with resource coverage and unfinished work.");
|
|
1546
|
+
} catch (e) { notify(e.message); }
|
|
1547
|
+
};
|
|
1548
|
+
// One short real request proves the sign-in, model ID and local service all work.
|
|
1549
|
+
async function verifyConnection(choice) {
|
|
1550
|
+
let live = session;
|
|
1551
|
+
if (!live) {
|
|
1552
|
+
try {
|
|
1553
|
+
live = await discoverLocalAgent();
|
|
1554
|
+
setSession(live);
|
|
1555
|
+
} catch {
|
|
1556
|
+
throw Error("AtMem AI Architect's local service isn't running. Start it with npm run local, then try again.");
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const result = await requestArchitect({
|
|
1560
|
+
session: live,
|
|
1561
|
+
connection: choice,
|
|
1562
|
+
message: "Connection check from AtMem AI Architect. Reply with one short sentence; no proposal.",
|
|
1563
|
+
project: projectRef.current,
|
|
1564
|
+
history: [],
|
|
1565
|
+
stage: "discover",
|
|
1566
|
+
signal: AbortSignal.timeout(90000),
|
|
1567
|
+
});
|
|
1568
|
+
if (!result?.message) throw Error("The model didn't reply. Check the model ID and sign-in.");
|
|
1569
|
+
}
|
|
1570
|
+
// Sketching is the model's job; the skill's starter sketch is described to it as a starting point.
|
|
1571
|
+
function sketchIt() {
|
|
1572
|
+
send("Please draft a first sketch of the architecture from what I've told you, as a proposal I can review.");
|
|
1573
|
+
}
|
|
1574
|
+
function skipQuestion() {
|
|
1575
|
+
const p = projectRef.current;
|
|
1576
|
+
const skipped = { ...p, asked: [...(p.asked || []), ask?.id].filter(Boolean) };
|
|
1577
|
+
const next = nextQuestion({ ...skipped, lastAsked: null });
|
|
1578
|
+
updateEngagement({ ...skipped, lastAsked: next });
|
|
1579
|
+
setMessages((m) => [...m, { role: "assistant", text: next ? `No problem, we'll come back to it.\n\n${next.question}\n${next.beginner}` : "That's everything I need for now." }]);
|
|
1580
|
+
}
|
|
1581
|
+
// Autosave to the helper (your account on this computer), a moment after each change.
|
|
1582
|
+
const binned = useRef(new Set());
|
|
1583
|
+
useEffect(() => {
|
|
1584
|
+
if (!session || binned.current.has(project.id) || (!project.nodes.length && !project.brief?.application?.trim() && messages.length < 2)) return;
|
|
1585
|
+
const t = setTimeout(async () => {
|
|
1586
|
+
try {
|
|
1587
|
+
const saved=await projectStore(session, "save", { id: project.id, file: toProjectFile(withConversation(project, messages)) });
|
|
1588
|
+
if(saved.memory)setMemorySync({projectId:project.id,...saved.memory});
|
|
1589
|
+
} catch (error) { setMemorySync(current=>({...((current?.projectId===project.id)?current:{}),projectId:project.id,status:'sync_failed'}));notify(`Project conversation could not be saved: ${error.message}`); }
|
|
1590
|
+
}, 800);
|
|
1591
|
+
return () => clearTimeout(t);
|
|
1592
|
+
}, [project, messages, session?.token]);
|
|
1593
|
+
async function saveEngineeringProject(owner,expected){
|
|
1594
|
+
const current=projectRef.current;
|
|
1595
|
+
if(current.id!==owner)throw Error('The project changed while opening engineering work. Return to that project before submitting its draft.');
|
|
1596
|
+
if(expected&&expected!==current)throw Error('The design changed before saving the handoff. Review Delivery again.');
|
|
1597
|
+
const conversation=messagesRef.current;
|
|
1598
|
+
const result=await projectStore(session,'save',{id:owner,file:toProjectFile(withConversation(current,conversation))});
|
|
1599
|
+
if(expected&&(projectRef.current!==expected||messagesRef.current!==conversation))throw Error('The design changed while saving the handoff. Review Delivery again.');
|
|
1600
|
+
return result;
|
|
1601
|
+
}
|
|
1602
|
+
async function refreshProjects() {
|
|
1603
|
+
if (!session) return;
|
|
1604
|
+
try {
|
|
1605
|
+
setSaved(await projectStore(session, "list"));
|
|
1606
|
+
} catch (e) {
|
|
1607
|
+
notify(e.message);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function switchTo(next) {
|
|
1611
|
+
chooseWorkspace('canvas');setBuildRequest(null);
|
|
1612
|
+
next = withDerived(next);
|
|
1613
|
+
const leaving = projectRef.current;
|
|
1614
|
+
if (session && !binned.current.has(leaving.id) && (leaving.nodes.length || leaving.brief?.application?.trim() || messages.length > 1))
|
|
1615
|
+
projectStore(session, "save", { id: leaving.id, file: toProjectFile(withConversation(leaving, messages)) }).catch(() => {});
|
|
1616
|
+
setHistory([]);
|
|
1617
|
+
setFuture([]);
|
|
1618
|
+
projectRef.current = next;
|
|
1619
|
+
seen.current = structuredClone(next);
|
|
1620
|
+
setProject(next);
|
|
1621
|
+
setRun(null);
|
|
1622
|
+
setRunStates({});
|
|
1623
|
+
setPending(null);
|
|
1624
|
+
setSelected(null);
|
|
1625
|
+
setPicked(new Set());
|
|
1626
|
+
const canvasView=readCanvasView(next);
|
|
1627
|
+
setLevel(canvasView.level);setFocus(canvasView.focus);setSelected(canvasView.selected);setTopologyPositions(canvasView.positions);
|
|
1628
|
+
setMode("design");
|
|
1629
|
+
setDesignView("board");
|
|
1630
|
+
setPeek(null);
|
|
1631
|
+
setAttachments([]);setAttachmentError('');
|
|
1632
|
+
setMessages(conversationMessages(next, [welcome(next)]));
|
|
1633
|
+
setInput("");
|
|
1634
|
+
setProbe(null);
|
|
1635
|
+
setSnapshot(null);
|
|
1636
|
+
setCtx(null);
|
|
1637
|
+
refit();
|
|
1638
|
+
}
|
|
1639
|
+
async function openSaved(id) {
|
|
1640
|
+
try {
|
|
1641
|
+
const { project: opened } = fromProjectFile(await projectStore(session, "get", { id }));
|
|
1642
|
+
switchTo(opened);
|
|
1643
|
+
setModal(null);
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
notify(e.message);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const linkedProject = useRef(null);
|
|
1649
|
+
useEffect(() => {
|
|
1650
|
+
const id = projectIdFromUrl(window.location.href);
|
|
1651
|
+
if (!session?.token || !id || linkedProject.current === id) return;
|
|
1652
|
+
let active = true;
|
|
1653
|
+
(async () => {
|
|
1654
|
+
try {
|
|
1655
|
+
const { project: opened } = fromProjectFile(await projectStore(session, "get", { id }));
|
|
1656
|
+
if (!active) return;
|
|
1657
|
+
const leaving = projectRef.current;
|
|
1658
|
+
if (leaving.id !== id && !binned.current.has(leaving.id) && (leaving.nodes.length || leaving.brief?.application?.trim() || messages.length > 1)) {
|
|
1659
|
+
await projectStore(session, "save", { id: leaving.id, file: toProjectFile(withConversation(leaving, messages)) });
|
|
1660
|
+
}
|
|
1661
|
+
if (!active) return;
|
|
1662
|
+
linkedProject.current = id;
|
|
1663
|
+
switchTo(opened);
|
|
1664
|
+
setModal(null);
|
|
1665
|
+
} catch (error) { if (active) notify(error.message); }
|
|
1666
|
+
})();
|
|
1667
|
+
return () => { active = false; };
|
|
1668
|
+
}, [session?.token]);
|
|
1669
|
+
function newProject() {
|
|
1670
|
+
setModal("start-over");
|
|
1671
|
+
}
|
|
1672
|
+
async function binProject(id) {
|
|
1673
|
+
try {
|
|
1674
|
+
binned.current.add(id);
|
|
1675
|
+
await projectStore(session, "trash", { id });
|
|
1676
|
+
if (id === projectRef.current.id) switchTo(validateImport(emptyProject()));
|
|
1677
|
+
await refreshProjects();
|
|
1678
|
+
notify("Moved to the bin. Restore it from Projects any time.");
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
binned.current.delete(id);
|
|
1681
|
+
notify(e.message);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
async function restoreProject(id) {
|
|
1685
|
+
try {
|
|
1686
|
+
await projectStore(session, "restore", { id });
|
|
1687
|
+
binned.current.delete(id);
|
|
1688
|
+
await refreshProjects();
|
|
1689
|
+
} catch (e) {
|
|
1690
|
+
notify(e.message);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
async function purgeProject(id) {
|
|
1694
|
+
try {
|
|
1695
|
+
await projectStore(session, "purge", { id });
|
|
1696
|
+
await refreshProjects();
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
notify(e.message);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const guidance = useMemo(()=>journey(project),[project]);
|
|
1702
|
+
const activeSection = designSection(mode,designView);
|
|
1703
|
+
const nextAction = journeyAction(guidance,activeSection.id);
|
|
1704
|
+
function followJourney(next=guidance){
|
|
1705
|
+
if(busy||agentBusy)return;
|
|
1706
|
+
setChatOpen(true);
|
|
1707
|
+
consultationAttempts.current.add(`${projectRef.current.id}:${projectRef.current.revision}:${next.mode}:${next.view}`);
|
|
1708
|
+
const section=DESIGN_SECTIONS.find(item=>item.id===journeySectionId(next.id));
|
|
1709
|
+
setMode(section?.mode||next.mode);setDesignView(section?.view||next.view);if(section?.mode==='review')setReviewView('findings');
|
|
1710
|
+
send(next.prompt,'workflow',next);
|
|
1711
|
+
}
|
|
1712
|
+
function visitJourney(id,event){
|
|
1713
|
+
const section=DESIGN_SECTIONS.find(item=>item.id===journeySectionId(id));
|
|
1714
|
+
if(!section)return;
|
|
1715
|
+
event?.currentTarget.closest("details")?.removeAttribute("open");
|
|
1716
|
+
setMode(section.mode);setDesignView(section.view);
|
|
1717
|
+
if(section.mode==='review')setReviewView('findings');
|
|
1718
|
+
}
|
|
1719
|
+
async function chooseCloud(provider){
|
|
1720
|
+
if(busy||agentBusy||pending)return;
|
|
1721
|
+
const ok=await send(`Use ${OPTION_NAMES[provider]} for this project.`,"chat",{mode:"design",view:"options"});
|
|
1722
|
+
if(ok){setMode('design');setDesignView('options');setChatOpen(true);}
|
|
1723
|
+
}
|
|
1724
|
+
function continueJourney(){
|
|
1725
|
+
if(!guidance.checkpoint||busy||agentBusy)return;
|
|
1726
|
+
const next=advanceJourney(projectRef.current,guidance.checkpoint);
|
|
1727
|
+
updateEngagement(next);const upcoming=journey(next);
|
|
1728
|
+
if(upcoming.id==='delivery')visitJourney('delivery');else followJourney(upcoming);
|
|
1729
|
+
}
|
|
1730
|
+
const findings = useMemo(() => review(project), [project]);
|
|
1731
|
+
const costs = useMemo(() => estimate(project), [project]);
|
|
1732
|
+
useEffect(() => {
|
|
1733
|
+
if (buildView || !drawLive || !session || !connection || busy || agentBusy || input.trim() || mode !== "design" || designView !== "board" || modal) return;
|
|
1734
|
+
const edits = canvasChanges(project);
|
|
1735
|
+
if (!edits.length || project.nodes.some(n => n.data.kind === "note" && !n.data.label.trim())) return;
|
|
1736
|
+
const timer = setTimeout(() => send("The user changed the shared diagram: " + edits.join("; ") + ". Work with these edits, preserve their comments and blocks, refine only what follows from their intent, and ask one useful next question if needed.", "canvas"), 1200);
|
|
1737
|
+
return () => clearTimeout(timer);
|
|
1738
|
+
}, [buildView,project.nodes,project.edges,drawLive,session,connection,busy,agentBusy,input,mode,designView,modal]);
|
|
1739
|
+
const topics = progress(project);
|
|
1740
|
+
const blockerCount = findings.filter((f) => f.severity === "blocker" && f.status === "open").length;
|
|
1741
|
+
const ask = project.extensions?.activeConsultation ? (project.extensions.activeConsultation.hasQuestion ? project.lastAsked : null) : project.lastAsked?.question ? project.lastAsked : nextQuestion(project);
|
|
1742
|
+
function saveProjectFile() {
|
|
1743
|
+
try {
|
|
1744
|
+
download(JSON.stringify(toProjectFile(withConversation(project, messages)), null, 2), `${slug}.atmem-ai-architect.json`);
|
|
1745
|
+
notify("Project file saved. Open it later from ⋯ → Open project file.");
|
|
1746
|
+
return true;
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
notify(error.message);
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const exportDecisions = () =>
|
|
1753
|
+
download(`# Decisions · ${project.name}\n\n${adrMarkdown(project.decisions)}`, `${slug}-decisions.md`, "text/markdown");
|
|
1754
|
+
function showDesign(cloud) {
|
|
1755
|
+
send(`Propose this design on ${OPTION_NAMES[cloud]}, keeping its structure, so I can compare.`);
|
|
1756
|
+
}
|
|
1757
|
+
function recommend(option) {
|
|
1758
|
+
const d = recommendationDecision(project, option, OPTION_NAMES);
|
|
1759
|
+
updateEngagement({ ...project, decisions: [...(project.decisions || []), d] });
|
|
1760
|
+
setDesignView("decisions");
|
|
1761
|
+
notify(`${d.id.toUpperCase()} drafted. Add an owner and the consequences.`);
|
|
1762
|
+
}
|
|
1763
|
+
const scenarioReady = ([, , needs]) => !needs || project.nodes.some((n) => n.data.kind === needs);
|
|
1764
|
+
const onBoard = testing || (mode === "design" && designView === "board");
|
|
1765
|
+
const diff = pending?.next ? proposalDiff(project, pending.next) : null;
|
|
1766
|
+
|
|
1767
|
+
return (
|
|
1768
|
+
<div style={{"--chat-panel-width":`${chatWidth}px`}} className={`app ${chatOpen&&!buildView ? "" : "chat-closed"} ${buildView?"build-workspace":""}`}>
|
|
1769
|
+
<header className="bar">
|
|
1770
|
+
<button className="brand" onClick={() => setModal("about")} title="About AtMem AI Architect">
|
|
1771
|
+
<Mark />
|
|
1772
|
+
<span>
|
|
1773
|
+
AtMem.ai<span className="brand-divider">|</span>AI Architect
|
|
1774
|
+
</span>
|
|
1775
|
+
</button>
|
|
1776
|
+
<nav className="crumbs" aria-label="Level"><span className="workspace-label">PROJECT</span>
|
|
1777
|
+
<button className="projects-btn" aria-label="Projects" title="Your projects" onClick={() => (refreshProjects(), setModal("projects"))}>
|
|
1778
|
+
<FolderOpen size={22} /> Projects
|
|
1779
|
+
</button>
|
|
1780
|
+
<label className="project-rename"><input
|
|
1781
|
+
className="project-name"
|
|
1782
|
+
title={project.name}
|
|
1783
|
+
aria-label="Project name"
|
|
1784
|
+
value={project.name}
|
|
1785
|
+
onChange={(e) => commit({ ...project, name: e.target.value })}
|
|
1786
|
+
/><PencilRuler size={14} aria-hidden="true"/></label>
|
|
1787
|
+
</nav>
|
|
1788
|
+
<div className="bar-actions">
|
|
1789
|
+
{storageError && <span className="warn-text">Not saved</span>}
|
|
1790
|
+
<button
|
|
1791
|
+
className="icon"
|
|
1792
|
+
title={theme === "dark" ? "Light mode" : "Dark mode"}
|
|
1793
|
+
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
|
1794
|
+
onClick={() => {
|
|
1795
|
+
const next = theme === "dark" ? "light" : "dark";
|
|
1796
|
+
document.documentElement.dataset.theme = next;
|
|
1797
|
+
try {
|
|
1798
|
+
localStorage.setItem(THEME, next);
|
|
1799
|
+
} catch {}
|
|
1800
|
+
setTheme(next);
|
|
1801
|
+
}}
|
|
1802
|
+
>
|
|
1803
|
+
{theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
|
|
1804
|
+
</button>
|
|
1805
|
+
<button className="icon" title="More" aria-label="More" onClick={() => setMenu((m) => !m)}>
|
|
1806
|
+
<MoreHorizontal size={16} />
|
|
1807
|
+
</button>
|
|
1808
|
+
|
|
1809
|
+
</div>
|
|
1810
|
+
{menu && (
|
|
1811
|
+
<div className="menu" role="menu" onClick={() => setMenu(false)}>
|
|
1812
|
+
<button role="menuitem" onClick={() => (refreshProjects(), setModal("projects"))} disabled={busy}><FolderOpen size={16}/>Projects</button>
|
|
1813
|
+
<button role="menuitem" onClick={newProject} disabled={busy}><Plus size={16}/>New project</button>
|
|
1814
|
+
<hr/><button role="menuitem" onClick={() => setPalette(true)} disabled={busy}><Plus size={16}/>Add a block</button>
|
|
1815
|
+
<button role="menuitem" onClick={() => setModal("templates")} disabled={busy}><LayoutTemplate size={16}/>Templates</button>
|
|
1816
|
+
<hr/><button role="menuitem" onClick={() => setModal("import")} disabled={busy}><GitBranch size={16}/>Import diagram</button>
|
|
1817
|
+
<button role="menuitem" onClick={() => setModal("project-file")} disabled={busy}><FolderOpen size={16}/>Open project file</button>
|
|
1818
|
+
<hr/><button role="menuitem" onClick={() => setModal("memory")} disabled={!session}><BrainCircuit size={16}/>AtMem dashboard</button>
|
|
1819
|
+
<button role="menuitem" onClick={() => setModal("start-over")} disabled={busy || agentBusy}><RotateCcw size={16}/>Start over</button>
|
|
1820
|
+
<hr />
|
|
1821
|
+
<button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>Connections</button>
|
|
1822
|
+
<button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
|
|
1823
|
+
</div>
|
|
1824
|
+
)}
|
|
1825
|
+
</header>
|
|
1826
|
+
|
|
1827
|
+
<section className="journey-wizard unified-journey" aria-label="Architecture journey">
|
|
1828
|
+
<nav className="stage-tabs" role="tablist" aria-label="Project stages">{DESIGN_SECTIONS.map((section,i)=><button key={section.id} role="tab" aria-label={section.label} aria-selected={activeSection.id===section.id} className={completedStages(project)[section.id]?'stage-complete':''} onClick={()=>visitJourney(section.id)} title={completedStages(project)[section.id]?'Working draft acknowledged':'Not yet complete'}><span>{completedStages(project)[section.id]?'✓':i+1}</span>{section.label}</button>)}</nav>
|
|
1829
|
+
{nextAction&&<div className="journey-current"><p>{guidance.reason}</p><button className="btn primary" disabled={busy||agentBusy||!!pending} onClick={()=>nextAction.kind==='navigate'?visitJourney(nextAction.section):nextAction.kind==='continue'?continueJourney():followJourney()}>{nextAction.label}</button></div>}
|
|
1830
|
+
</section>
|
|
1831
|
+
<main
|
|
1832
|
+
id="canvas"
|
|
1833
|
+
className={`canvas level-${level}`}
|
|
1834
|
+
onDrop={(e) => {
|
|
1835
|
+
e.preventDefault();
|
|
1836
|
+
if(buildView)return;
|
|
1837
|
+
const item = ALL_CATALOG.find((c) => c.id === e.dataTransfer.getData("application/atforma-block"));
|
|
1838
|
+
if (item) addComponent(item, screenToFlowPosition({ x: e.clientX, y: e.clientY }));
|
|
1839
|
+
}}
|
|
1840
|
+
onDragOver={(e) => {
|
|
1841
|
+
e.preventDefault();
|
|
1842
|
+
e.dataTransfer.dropEffect = "move";
|
|
1843
|
+
}}
|
|
1844
|
+
>
|
|
1845
|
+
<div className={`board-surface ${treeOpen&&level!=="block"?"has-component-sidebar":""}`} hidden={!onBoard}>
|
|
1846
|
+
{level!=="block"&&<aside className="board-components component-navigator" data-expanded={treeOpen} aria-label="Board components"><button className="btn" aria-label="Toggle components and layers" aria-expanded={treeOpen} onClick={()=>setTreeOpen(!treeOpen)}><LayoutGrid size={16}/> Components & layers</button>
|
|
1847
|
+
{treeOpen&&<nav className="board-component-tree" aria-label="Component tree"><strong>{project.name}</strong><div className="tree-views"><button disabled={!blockReviews.length} onClick={()=>{reviewBlock((blockReviews.find(r=>r.status!=='reviewed')||blockReviews[0]).id);}}><CheckCheck size={16}/> Review blocks · {blockReviews.filter(r=>r.status==='reviewed').length}/{blockReviews.length}</button></div>{[...new Map(project.nodes.filter(n=>n.data.kind!=='note').map(n=>[layerOf(n).id,layerOf(n)])).values()].map(layer=><details open key={layer.id}><summary>{layer.label}</summary>{project.nodes.filter(n=>n.data.kind!=='note'&&layerOf(n).id===layer.id).map(n=><button key={n.id} aria-current={selected===n.id?'true':undefined} onClick={()=>{setMode('design');setDesignView('board');setFocus(null);setLevel('flow');setSelected(n.id);setTimeout(()=>fitView({nodes:[{id:n.id}],padding:1,maxZoom:1.2,duration:350}),100);}}><Database size={14}/><span>{n.data.label}<small>{n.data.service||n.data.provider}</small><small className={`tree-review-state ${blockReviews.find(r=>r.id===n.id)?.status}`}>{blockReviews.find(r=>r.id===n.id)?.statusLabel}</small></span></button>)}</details>)}</nav>}
|
|
1848
|
+
{treeOpen&&<button className="btn" disabled={busy} onClick={()=>setPalette(true)}><Plus size={16}/> Add component</button>}</aside>}
|
|
1849
|
+
{!hasDesign&&!palette&&!dismissedStarts.includes(project.id)&&<section className="board-start" aria-label="Start your project"><button type="button" className="icon board-start-close" aria-label="Dismiss welcome card" title="Dismiss welcome card" onClick={dismissStart}><X size={16}/></button><h2>What would you like to create?</h2><p>Describe your idea in chat, upload existing requirements or a diagram, or open a saved project.</p><div className="row"><button className="btn primary" onClick={()=>{dismissStart();setChatOpen(true);document.querySelector('.composer textarea, .chat textarea')?.focus();}}>Start chatting</button><button className="btn" onClick={()=>{setChatOpen(true);requirementsInput.current.click();}}>Upload requirements or diagram</button><button className="btn" onClick={()=>setModal("project-file")}>Open saved project file</button></div><small>Word (.docx), PDF, text, Markdown, draw.io, images and Architect project JSON. Architect reads your material and asks questions in chat.</small></section>}
|
|
1850
|
+
<ReactFlow
|
|
1851
|
+
nodes={canvasNodes}
|
|
1852
|
+
edges={canvasEdges}
|
|
1853
|
+
nodeTypes={nodeTypes}
|
|
1854
|
+
colorMode={theme}
|
|
1855
|
+
onNodesChange={onNodesChange}
|
|
1856
|
+
onEdgesChange={onEdgesChange}
|
|
1857
|
+
onConnect={(c) => {
|
|
1858
|
+
if (!busy && level === "flow")
|
|
1859
|
+
commit({ ...project, edges: addEdge({ ...c, type: "smoothstep", label: "data" }, project.edges) });
|
|
1860
|
+
}}
|
|
1861
|
+
isValidConnection={(c) =>
|
|
1862
|
+
c.source !== c.target && !project.edges.some((e) => e.source === c.source && e.target === c.target)
|
|
1863
|
+
}
|
|
1864
|
+
onNodeClick={(_, n) => {
|
|
1865
|
+
if (level === "system") return;
|
|
1866
|
+
setSelected(n.id);
|
|
1867
|
+
setProbe(null);
|
|
1868
|
+
}}
|
|
1869
|
+
onNodeDoubleClick={(_, n) => (level === "system" ? n.data.blocks.length===1?reviewBlock(n.data.blocks[0]):goFlow(n.data.layer) : openBlock(n.id))}
|
|
1870
|
+
onEdgeClick={(_, e) => (level === "system" ? goFlow(null) : openBlock(e.source, e))}
|
|
1871
|
+
onPaneContextMenu={(e) => {
|
|
1872
|
+
e.preventDefault();
|
|
1873
|
+
if (previewing) return;
|
|
1874
|
+
setCtx({ x: e.clientX, y: e.clientY, at: screenToFlowPosition({ x: e.clientX, y: e.clientY }) });
|
|
1875
|
+
}}
|
|
1876
|
+
onNodeContextMenu={(e, n) => {
|
|
1877
|
+
e.preventDefault();
|
|
1878
|
+
if (previewing) return;
|
|
1879
|
+
setCtx({ x: e.clientX, y: e.clientY, at: screenToFlowPosition({ x: e.clientX, y: e.clientY }), node: level === "system" ? null : n });
|
|
1880
|
+
}}
|
|
1881
|
+
onPaneClick={() => {
|
|
1882
|
+
setCtx(null);
|
|
1883
|
+
setPicked(new Set());
|
|
1884
|
+
setSelected(null);
|
|
1885
|
+
setPalette(false);
|
|
1886
|
+
}}
|
|
1887
|
+
nodesDraggable={!busy && !previewing && level === "flow"}
|
|
1888
|
+
nodesConnectable={!busy && !previewing && level === "flow"}
|
|
1889
|
+
elementsSelectable={!busy}
|
|
1890
|
+
deleteKeyCode={busy || level !== "flow" ? null : ["Backspace", "Delete"]}
|
|
1891
|
+
fitView
|
|
1892
|
+
fitViewOptions={{ padding: 0.25, maxZoom: 1.2 }}
|
|
1893
|
+
minZoom={0.25}
|
|
1894
|
+
maxZoom={1.8}
|
|
1895
|
+
zoomOnDoubleClick={false}
|
|
1896
|
+
proOptions={{ hideAttribution: true }}
|
|
1897
|
+
>
|
|
1898
|
+
<Background gap={20} size={1} />
|
|
1899
|
+
</ReactFlow>
|
|
1900
|
+
|
|
1901
|
+
<div className="toolbar" aria-label="Tools">
|
|
1902
|
+
<button
|
|
1903
|
+
className="icon"
|
|
1904
|
+
title="Auto-arrange"
|
|
1905
|
+
aria-label="Auto-arrange"
|
|
1906
|
+
disabled={busy || previewing || level==='block' || !project.nodes.length}
|
|
1907
|
+
onClick={() => {
|
|
1908
|
+
if(level==='system')setTopologyPositions(arrangeOverview(system,measured));
|
|
1909
|
+
else commit(autoLayout(projectRef.current));
|
|
1910
|
+
refit();
|
|
1911
|
+
}}
|
|
1912
|
+
>
|
|
1913
|
+
<LayoutGrid size={17} />
|
|
1914
|
+
</button>
|
|
1915
|
+
<button className="icon" title="Clear board" aria-label="Clear board" disabled={!project.nodes.length || busy} onClick={clearBoard}>
|
|
1916
|
+
<Eraser size={17} />
|
|
1917
|
+
</button>
|
|
1918
|
+
<button className="icon" title="Undo board change" aria-label="Undo" disabled={!history.length || busy || agentBusy} onClick={undo}>
|
|
1919
|
+
<Undo2 size={17} />
|
|
1920
|
+
</button>
|
|
1921
|
+
<button className="icon" title="Redo board change" aria-label="Redo" disabled={!future.length || busy || agentBusy} onClick={redo}><Redo2 size={17} /></button>
|
|
1922
|
+
</div>
|
|
1923
|
+
{level !== "block" && (
|
|
1924
|
+
<div className="zoom">
|
|
1925
|
+
<button className="icon" aria-label="Zoom out" onClick={() => zoomOut()}><ZoomOut size={15} /></button>
|
|
1926
|
+
<button className="icon" aria-label="Fit" title="Fit" onClick={() => refit()}><Maximize2 size={14} /></button>
|
|
1927
|
+
<button className="icon" aria-label="Zoom in" onClick={() => zoomIn()}><ZoomIn size={15} /></button>
|
|
1928
|
+
</div>
|
|
1929
|
+
)}
|
|
1930
|
+
{palette && <Palette onAdd={(item) => addComponent(item, paletteAt.current || undefined)} onClose={() => ((paletteAt.current = null), setPalette(false))} disabled={busy} />}
|
|
1931
|
+
|
|
1932
|
+
{level === "system" && project.nodes.length > 0 && (
|
|
1933
|
+
<p className="hint">Double-click to open</p>
|
|
1934
|
+
)}
|
|
1935
|
+
{level === "flow" && selectedNode && (
|
|
1936
|
+
<p className="hint">Double-click for detail · Esc back</p>
|
|
1937
|
+
)}
|
|
1938
|
+
{ctx && (
|
|
1939
|
+
<div className="ctx-menu" role="menu" style={{ left: Math.min(ctx.x,window.innerWidth-210), top: Math.min(ctx.y,window.innerHeight-200) }} onMouseLeave={() => setCtx(null)}>
|
|
1940
|
+
{ctx.node ? (
|
|
1941
|
+
<>
|
|
1942
|
+
<button role="menuitem" onClick={() => (addNote({ x: ctx.at.x + 40, y: ctx.at.y - 120 }, ctx.node.id), setCtx(null))}>
|
|
1943
|
+
<StickyNote size={16}/> Add comment
|
|
1944
|
+
</button>
|
|
1945
|
+
<div className="ctx-divider" role="separator"/>
|
|
1946
|
+
<button role="menuitem" className="danger" onClick={() => (removeNodes(new Set(picked.size > 1 ? picked : [ctx.node.id])), setCtx(null))}>
|
|
1947
|
+
<Trash2 size={16}/>{picked.size > 1 ? `Delete ${picked.size} items` : "Delete"}
|
|
1948
|
+
</button>
|
|
1949
|
+
</>
|
|
1950
|
+
) : (
|
|
1951
|
+
<>
|
|
1952
|
+
<button role="menuitem" onClick={() => ((paletteAt.current = ctx.at), setPalette(true), setCtx(null))}>
|
|
1953
|
+
<Plus size={16}/> Add block here
|
|
1954
|
+
</button>
|
|
1955
|
+
<button role="menuitem" onClick={() => (addNote(ctx.at), setCtx(null))}>
|
|
1956
|
+
<StickyNote size={16}/> Add comment here
|
|
1957
|
+
</button>
|
|
1958
|
+
<button role="menuitem" onClick={() => (setLevel("flow"), setPicked(new Set(project.nodes.map((n) => n.id))), setCtx(null))}>
|
|
1959
|
+
<Scan size={16}/> Select all
|
|
1960
|
+
</button>
|
|
1961
|
+
<div className="ctx-divider" role="separator"/>
|
|
1962
|
+
<button role="menuitem" className="danger" disabled={!project.nodes.length} onClick={() => (clearBoard(), setCtx(null))}>
|
|
1963
|
+
<Eraser size={16}/> Clear board
|
|
1964
|
+
</button>
|
|
1965
|
+
</>
|
|
1966
|
+
)}
|
|
1967
|
+
</div>
|
|
1968
|
+
)}
|
|
1969
|
+
{picked.size > 1 && (
|
|
1970
|
+
<div className="preview-bar" role="status">
|
|
1971
|
+
<span>{picked.size} selected</span>
|
|
1972
|
+
<button className="btn" onClick={() => removeNodes(picked)}>
|
|
1973
|
+
Delete
|
|
1974
|
+
</button>
|
|
1975
|
+
<button className="btn ghost" onClick={() => setPicked(new Set())}>
|
|
1976
|
+
Cancel
|
|
1977
|
+
</button>
|
|
1978
|
+
</div>
|
|
1979
|
+
)}
|
|
1980
|
+
{previewing && (
|
|
1981
|
+
<div className="preview-bar" role="status">
|
|
1982
|
+
<span>
|
|
1983
|
+
Preview · <strong>{pending.title}</strong>
|
|
1984
|
+
</span>
|
|
1985
|
+
<button className="btn primary" disabled={busy} onClick={() => applyProposal()}>
|
|
1986
|
+
Apply
|
|
1987
|
+
</button>
|
|
1988
|
+
<button className="btn ghost" onClick={() => setPending(null)}>
|
|
1989
|
+
Dismiss
|
|
1990
|
+
</button>
|
|
1991
|
+
</div>
|
|
1992
|
+
)}
|
|
1993
|
+
{!shown.nodes.length && (
|
|
1994
|
+
<div className="empty-canvas">
|
|
1995
|
+
<p className="muted">Describe your idea in the chat — it takes shape here.</p>
|
|
1996
|
+
</div>
|
|
1997
|
+
)}
|
|
1998
|
+
|
|
1999
|
+
{level === "block" && selectedNode && (
|
|
2000
|
+
<BlockDetail
|
|
2001
|
+
project={project}
|
|
2002
|
+
node={selectedNode}
|
|
2003
|
+
run={run}
|
|
2004
|
+
probe={probe}
|
|
2005
|
+
busy={busy||agentBusy||!!pending}
|
|
2006
|
+
reviewItems={componentReviews(project)}
|
|
2007
|
+
onReviewConfirm={()=>{updateEngagement(markComponentReviewed(project,selectedNode.id));notify(`${selectedNode.data.label}: design reviewed.`);}}
|
|
2008
|
+
onReviewDiscuss={issue=>{if(issue?.kind!=='finding')return reviewComponent(selectedNode.id);setChatOpen(true);return send(`Help address this design issue for ${selectedNode.data.label}: ${issue.text} Suggested next step: ${issue.action||'Propose a concrete correction.'} Explain what is missing and why it matters here, then propose the design changes needed to resolve it. This is engineering work, not a question the user must solve. Ask only for a consequential business choice that cannot be inferred from the saved design. Preserve all requirements, existing systems and provider choices. Keep any change reviewable, and do not claim monitoring or other runtime behavior is already implemented.`,'workflow');}}
|
|
2009
|
+
onRequirementSave={async(id,text)=>{
|
|
2010
|
+
updateEngagement(editBlockRequirement(projectRef.current,selectedNode.id,id,text));
|
|
2011
|
+
if(connection&&session)await reviewComponent(selectedNode.id);
|
|
2012
|
+
else notify('Requirement saved. Local checks updated; connect Architect for its review.');
|
|
2013
|
+
}}
|
|
2014
|
+
onReviewQuestion={question=>updateEngagement({...project,openQuestions:[...(project.openQuestions||[]),newQuestion({question,links:[selectedNode.id]})]})}
|
|
2015
|
+
onReviewResolve={(id,answer)=>updateEngagement({...project,openQuestions:project.openQuestions.map(q=>q.id===id?{...q,status:'answered',answer}:q)})}
|
|
2016
|
+
onChange={updateNode}
|
|
2017
|
+
onWorkModelChange={changeArchitectureWork}
|
|
2018
|
+
onRemove={() => removeNode(selectedNode.id)}
|
|
2019
|
+
onOpen={(id) => openBlock(id)}
|
|
2020
|
+
onBack={goUp}
|
|
2021
|
+
/>
|
|
2022
|
+
)}
|
|
2023
|
+
|
|
2024
|
+
{testing && view === "panel" && level !== "block" && (
|
|
2025
|
+
<FrontPanel project={project} run={run} onSelect={openBlock} />
|
|
2026
|
+
)}
|
|
2027
|
+
|
|
2028
|
+
{testing && !runnable && (
|
|
2029
|
+
<div className="runbar note">
|
|
2030
|
+
<span>
|
|
2031
|
+
{project.nodes.length
|
|
2032
|
+
? "This design is a sketch, so it can't be test-run yet. Test runs need blocks with sample-data behaviour — the Predictive maintenance and Analytics templates have them."
|
|
2033
|
+
: "Nothing to test yet. Describe what you want to build in Discover first."}
|
|
2034
|
+
</span>
|
|
2035
|
+
</div>
|
|
2036
|
+
)}
|
|
2037
|
+
{testing && runnable && issues.length > 0 && (
|
|
2038
|
+
<div className="runbar note">
|
|
2039
|
+
<span>Fix before testing:</span>
|
|
2040
|
+
<div className="steps">
|
|
2041
|
+
{issues.map((issue, i) => (
|
|
2042
|
+
<button key={i} className="step failed" onClick={() => issue.nodeId && openBlock(issue.nodeId)}>
|
|
2043
|
+
{issue.text}
|
|
2044
|
+
</button>
|
|
2045
|
+
))}
|
|
2046
|
+
</div>
|
|
2047
|
+
</div>
|
|
2048
|
+
)}
|
|
2049
|
+
{testing && runnable && !issues.length && (
|
|
2050
|
+
<div className="runbar">
|
|
2051
|
+
{busy ? (
|
|
2052
|
+
<button className="btn" onClick={cancelRun}>
|
|
2053
|
+
<X size={14} /> Stop
|
|
2054
|
+
</button>
|
|
2055
|
+
) : (
|
|
2056
|
+
<button className="btn primary run-button" onClick={runFlow} title="Runs each block in order with sample data on this computer. No AI model, no cloud, no cost.">
|
|
2057
|
+
<Play size={13} /> Test with sample data
|
|
2058
|
+
</button>
|
|
2059
|
+
)}
|
|
2060
|
+
<select aria-label="Test scenario" disabled={busy} value={scenario} onChange={(e) => setScenario(e.target.value)}>
|
|
2061
|
+
{SCENARIOS.filter(scenarioReady).map(([id, label]) => (
|
|
2062
|
+
<option key={id} value={id}>
|
|
2063
|
+
{label}
|
|
2064
|
+
</option>
|
|
2065
|
+
))}
|
|
2066
|
+
</select>
|
|
2067
|
+
<div className="seg">
|
|
2068
|
+
<button className={view === "diagram" ? "on" : ""} onClick={() => setView("diagram")}>Diagram</button>
|
|
2069
|
+
<button className={view === "panel" ? "on" : ""} onClick={() => setView("panel")}>Panel</button>
|
|
2070
|
+
</div>
|
|
2071
|
+
{run && (
|
|
2072
|
+
<span className={`run-summary ${run.status}`}>
|
|
2073
|
+
{STATUS_GLYPH[run.status]} {run.status[0].toUpperCase() + run.status.slice(1)} · {run.steps.filter((s) => s.status === "passed").length}/{project.nodes.length}
|
|
2074
|
+
</span>
|
|
2075
|
+
)}
|
|
2076
|
+
<div className="steps">
|
|
2077
|
+
{run?.steps.map((s) => (
|
|
2078
|
+
<button
|
|
2079
|
+
key={s.id}
|
|
2080
|
+
className={`step ${s.status} ${selected === s.id ? "on" : ""}`}
|
|
2081
|
+
title={s.error || `${s.duration} ms`}
|
|
2082
|
+
onClick={() => openBlock(s.id)}
|
|
2083
|
+
>
|
|
2084
|
+
{s.label}
|
|
2085
|
+
</button>
|
|
2086
|
+
))}
|
|
2087
|
+
{!run && <span className="muted">Pick a scenario, then test. Failure scenarios show where problems are caught.</span>}
|
|
2088
|
+
</div>
|
|
2089
|
+
</div>
|
|
2090
|
+
)}
|
|
2091
|
+
|
|
2092
|
+
{testing && runnable && level !== "block" && <TestCard project={project} run={run} onOpen={(id) => openBlock(id)} />}
|
|
2093
|
+
|
|
2094
|
+
</div>
|
|
2095
|
+
<div className="review-workspace" hidden={mode!=="review"&&mode!=="estimate"}>
|
|
2096
|
+
<header className="review-intro"><h2>Review and verify</h2><p>Review the specification, tradeoffs and costs, then generate the project and inspect its checks. Delivery packages the resulting files and evidence.</p></header>
|
|
2097
|
+
<details className="review-group"><summary>Specification, risks and tradeoffs</summary>
|
|
2098
|
+
{mode === "review" && reviewView === "findings" && (
|
|
2099
|
+
<ReviewView
|
|
2100
|
+
project={project}
|
|
2101
|
+
findings={findings}
|
|
2102
|
+
onChange={updateEngagement}
|
|
2103
|
+
onOpen={(id) => {
|
|
2104
|
+
setMode("design");
|
|
2105
|
+
setDesignView("board");
|
|
2106
|
+
openBlock(id);
|
|
2107
|
+
}}
|
|
2108
|
+
onAddRequirement={(category) => {
|
|
2109
|
+
setMode("design");setDesignView("requirements");setChatOpen(true);
|
|
2110
|
+
send(`Help us establish the ${category} requirement for this architecture. Explain why it matters and ask for the target we actually need; do not insert a default SLA.`,"workflow",{mode:"design",view:"requirements"});
|
|
2111
|
+
}}
|
|
2112
|
+
/>
|
|
2113
|
+
)}
|
|
2114
|
+
</details>
|
|
2115
|
+
<details className="review-group"><summary>Costs and usage assumptions</summary>
|
|
2116
|
+
{["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={(sized)=>{setChatOpen(true);send(`Prepare a rough monthly estimate for the saved design using the ${sized.estimate?.projectSize||'selected'} project-size planning assumptions. Preserve explicit business usage, requirements, provider and region. Use available sourced price tools automatically; never ask the client to research unit prices. Explain the subtotal and excluded charges, and ask at most one material unresolved question. Do not invent rates or treat logical security controls as separately billable services without evidence. Planning usage: ${JSON.stringify(estimate(sized).usage)}. Keep the design unchanged.`,"workflow",null,null,{estimateOnly:true});}} />}
|
|
2117
|
+
</details>
|
|
2118
|
+
{mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} onWorkModelChange={changeArchitectureWork} onIntentContext={syncEngineeringIntent} projectId={project.id} session={session} connection={connection} contextBusy={busy||agentBusy||!!pending} saveProject={()=>saveEngineeringProject(project.id)} buildRequest={buildRequest} onBuildRequestHandled={id=>setBuildRequest(current=>current?.id===id?null:current)} onConnections={()=>setModal('connections')}/></section>}
|
|
2119
|
+
{mode==='review'&&!session?.engineeringEnabled&&<p>Generation and local checks require the engineering service. Your design is retained; Delivery can package the design and remaining tasks.</p>}
|
|
2120
|
+
{mode==='review'&&<details className="review-infrastructure"><summary>Generated infrastructure and configuration checks</summary><TerraformView project={project} environment={environment} session={session} discussionBusy={busy||agentBusy||!!pending} onDelivery={()=>visitJourney('delivery')} onBack={()=>visitJourney('board')}/></details>}
|
|
2121
|
+
</div>
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
|
|
2126
|
+
{onBoard&&emulationOpen&&<div className="emulation-dock"><EmulationPanel project={project} session={session} emulation={emulation} onDiscuss={discussEmulation} onOpenComponent={id=>{reviewBlock(id);setEmulationOpen(false);}} discussionBusy={busy||agentBusy||!!pending} onClose={()=>setEmulationOpen(false)} onBuild={session?.engineeringEnabled?()=>{setEmulationOpen(false);buildCurrentDesign('test');}:undefined}/></div>}
|
|
2127
|
+
|
|
2128
|
+
{mode === "design" && designView === "outputs" && <OutputsView onSave={expected=>saveEngineeringProject(project.id,expected)} project={project} run={run} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDownload={downloadBundle} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2129
|
+
{mode === "design" && designView === "terraform" && <TerraformView project={project} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDelivery={()=>visitJourney("delivery")} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2130
|
+
<div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
|
|
2131
|
+
{mode === "design" && ["options","decisions"].includes(designView) && (
|
|
2132
|
+
<OptionsView project={project} busy={busy||agentBusy||!!pending} onChoose={chooseCloud} onContinue={()=>visitJourney("review")} onPreview={(provider)=>{setDesignView("board");setLevel("flow");setChatOpen(true);send(`Preview a concrete ${OPTION_NAMES[provider]} implementation of our working scope, using actual named services in data.service and explicit connections. Keep existing systems and comments. Use the known country/residency constraints, or leave region undecided and ask the country question. This is an alternative preview, not a cloud choice or deployment. Return a graph proposal and explain key integration assumptions.`,"preview",{mode:"design",view:"board"});}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
|
|
2133
|
+
)}
|
|
2134
|
+
{mode === "design" && ["options","decisions","location"].includes(designView) && <section className="doc" aria-label="Country and region"><header className="doc-head"><h2>Country & region</h2><p className="muted">Where users and data are located affects residency, regional availability and cost. Proposed locations require review before they become decisions.</p></header>{['platform.cloud','data.country','cloud.region'].map(subject=>{const decision=[...(project.decisions||[])].reverse().find(d=>d.subject===subject&&d.status==='Accepted');return <p key={subject}><strong>{{'platform.cloud':'Cloud','data.country':'Country / data residency','cloud.region':'Region'}[subject]}:</strong> {decision?.decision||'Not yet agreed'}</p>;})}<button className="btn primary" disabled={busy||agentBusy||!!pending} onClick={()=>{setChatOpen(true);send('Review the accepted cloud, country and region choices for this project. Explain what is known, then ask one important unresolved location or data residency question. Keep the diagram unchanged until we agree a change. Do not silently choose a country or region.','workflow');}}>Discuss country and region</button><button className="link" onClick={()=>visitJourney('review')}>Continue to Review</button></section>}
|
|
2135
|
+
</div>
|
|
2136
|
+
{mode === "design" && designView === "requirements" && (
|
|
2137
|
+
<RequirementsView onUpload={()=>{setChatOpen(true);requirementsInput.current.click();}} onOpenProject={()=>setModal("project-file")} project={project} onChange={updateEngagement} reviewing={agentBusy} onReview={(comments)=>{setChatOpen(true);send("Propose revisions for these user comments on the requirements document. The document will remain unchanged until the user applies each suggestion. Explain the proposed change; never say you have already changed the document. Return requirementUpdates for the existing IDs where the feedback is clear, quoting the user's comment exactly in sourceText. Preserve other requirements and the diagram. Ask one question if a comment is ambiguous.\n" + comments.map(c=>`Requirement ${c.requirementId}${c.selectedText ? ` (selected text: ${c.selectedText.slice(0,300)})` : ""} — comment: ${c.text}`).join("\n"), "document");}} onDiscuss={(question) => { setChatOpen(true); send(question, "workflow"); }} />
|
|
2138
|
+
)}
|
|
2139
|
+
</main>
|
|
2140
|
+
|
|
2141
|
+
{!chatOpen&&!buildView && <button className="icon chat-reopen" title="Show chat" aria-label="Show chat" onClick={()=>setChatOpen(true)}><PanelRightOpen size={18}/></button>}
|
|
2142
|
+
{chatOpen&&!buildView && (
|
|
2143
|
+
<aside className="chat" aria-label="Chat"><ChatResizeHandle width={chatWidth} onChange={resizeChat}/>
|
|
2144
|
+
<div className="chat-head">
|
|
2145
|
+
<div className="chat-heading-row"><strong className="chat-title">
|
|
2146
|
+
<Mark size={18} /> AI Architect
|
|
2147
|
+
</strong><button className="link small chat-width-toggle" onClick={()=>resizeChat(chatWidth>360?360:Math.max(320,Math.min(720,window.innerWidth-360)))} title="You can also drag the left edge of the chat panel">{chatWidth>360?"Narrow":"Widen"}</button><button className="icon" title="Hide chat" aria-label="Hide chat" onClick={()=>setChatOpen(false)}><PanelRightClose size={16}/></button></div>
|
|
2148
|
+
<label className="live-toggle" title="On: changes are drawn as we talk, and you can undo any of them. Off: they wait as previews for you to apply.">
|
|
2149
|
+
<input
|
|
2150
|
+
type="checkbox"
|
|
2151
|
+
checked={drawLive}
|
|
2152
|
+
onChange={(e) => {
|
|
2153
|
+
setDrawLive(e.target.checked);
|
|
2154
|
+
try {
|
|
2155
|
+
localStorage.setItem("atforma-draw-live", e.target.checked ? "on" : "off");
|
|
2156
|
+
} catch {}
|
|
2157
|
+
}}
|
|
2158
|
+
/>
|
|
2159
|
+
Draw as we talk
|
|
2160
|
+
</label>
|
|
2161
|
+
<button className="model" onClick={() => setModal("connections")} title="Connections">
|
|
2162
|
+
<span className={`led ${connection && session ? "on" : ""}`} />
|
|
2163
|
+
{connection ? `${connection.model}${connection.effort ? ` · ${EFFORT_LABEL[connection.effort] || connection.effort}` : ""}` : "Connect a model"}
|
|
2164
|
+
</button>
|
|
2165
|
+
</div>
|
|
2166
|
+
<div className="skills" aria-label="Active skills">
|
|
2167
|
+
<button className="known-bar" disabled={!session} aria-label="AtMem memory" title="Open AtMem memory, confirmed decisions and sync activity" onClick={()=>setModal("memory")}><BrainCircuit size={15}/>{memoryLabel}<ChevronRight size={14}/></button>
|
|
2168
|
+
<button
|
|
2169
|
+
className={`known-bar ${peek === "known" ? "on" : ""}`}
|
|
2170
|
+
aria-label="Design context" aria-expanded={peek === "known"} title="Review what is known, assumed or still open"
|
|
2171
|
+
onClick={() => setPeek((p) => (p === "known" ? null : "known"))}
|
|
2172
|
+
>
|
|
2173
|
+
<CheckCheck size={15}/> Design context <ChevronRight size={14}/>
|
|
2174
|
+
</button>
|
|
2175
|
+
{WORKFLOWS.filter((w) => w.id === project.workflow).map((w) => (
|
|
2176
|
+
<button key={w.id} className={`skill ${peek === "skill" ? "on" : ""}`} onClick={() => setPeek((p) => (p === "skill" ? null : "skill"))}>
|
|
2177
|
+
Skill: {w.name}
|
|
2178
|
+
</button>
|
|
2179
|
+
))}
|
|
2180
|
+
{guidance.availableModes.includes("review") && (
|
|
2181
|
+
<button className="skill" title="Review rules check the design on every change" onClick={() => setMode("review")}>
|
|
2182
|
+
Review {findings.filter((f) => f.status === "open").length}
|
|
2183
|
+
</button>
|
|
2184
|
+
)}
|
|
2185
|
+
</div>
|
|
2186
|
+
{peek === "skill" &&
|
|
2187
|
+
WORKFLOWS.filter((w) => w.id === project.workflow).map((w) => (
|
|
2188
|
+
<div key={w.id} className="peek">
|
|
2189
|
+
<strong>{w.name}</strong> <span className="muted">knows</span>
|
|
2190
|
+
<div className="knows">
|
|
2191
|
+
{w.knows.map((k) => (
|
|
2192
|
+
<span key={k}>{k}</span>
|
|
2193
|
+
))}
|
|
2194
|
+
</div>
|
|
2195
|
+
</div>
|
|
2196
|
+
))}
|
|
2197
|
+
{peek === "known" && (
|
|
2198
|
+
<ul className="peek known-list" aria-label="Design context summary"><li className="context-caption">Context from our conversation · not workflow steps</li>
|
|
2199
|
+
{topics.map((t) => (
|
|
2200
|
+
<li key={t.id} className={t.state} title={project.brief?.[t.id] || t.question}>
|
|
2201
|
+
<span>{t.state === "known" ? "✓" : t.state === "assumed" ? "~" : "·"}</span>
|
|
2202
|
+
{t.label}
|
|
2203
|
+
{t.state !== "open" && <em>{String(project.brief?.[t.id] || "").split("\n").filter((l) => !/^Not sure yet/.test(l)).pop() || "assumed"}</em>}
|
|
2204
|
+
</li>
|
|
2205
|
+
))}
|
|
2206
|
+
</ul>
|
|
2207
|
+
)}
|
|
2208
|
+
<div className="messages" ref={chatScroll.viewport}>
|
|
2209
|
+
<div className="message-list" ref={chatScroll.content}>
|
|
2210
|
+
{architectureChatEntries(project,messages).map(entry=>{
|
|
2211
|
+
if(entry.kind==='activity-error')return <p key="activity-error" role="alert">{entry.error}</p>;
|
|
2212
|
+
if(entry.kind==='legacy-activity')return <EarlierArchitectureActivity key={entry.id} entries={entry.entries} projectId={project.id} session={session}/>;
|
|
2213
|
+
if(entry.kind==='activity')return <ArchitectureTurn key={project.id+':'+entry.turn.id} turn={entry.turn} number={entry.number} projectId={project.id} session={session} active={agentBusy&&entry.turn.clientStatus==='submitted'&&entry.turn.id===latestArchitectureTurn?.id} working={agentBusy&&entry.turn.clientStatus==='submitted'} onStop={()=>agentAbort.current?.abort()}/>;
|
|
2214
|
+
const m=entry.message,i=entry.index;
|
|
2215
|
+
return (
|
|
2216
|
+
<div key={i} className={`msg ${m.role}`}>
|
|
2217
|
+
<ChatMessage text={m.text}/>
|
|
2218
|
+
{(m.workState==='queued'||m.workState==='processing'&&intake.some(q=>q.id===m.requestId))&&<small className="message-work-state">Queued for the next update <button className="link" onClick={()=>{updateIntake(intakeRef.current.filter(q=>q.id!==m.requestId));setRequestState([m.requestId],'cancelled');}}>Remove from queue</button><button className="link" onClick={()=>{if(input||attachments.length){notify('Your current draft is preserved. Send or clear it before editing a queued update.');return;}const queued=intakeRef.current.find(q=>q.id===m.requestId);if(!queued)return;setInput(queued.text);setAttachments(queued.attachments);updateIntake(intakeRef.current.filter(q=>q.id!==m.requestId));setRequestState([m.requestId],'cancelled');}}>Edit update</button>{m.attachments?.some(a=>!a.data)&&<button className="link" onClick={()=>{updateIntake(intakeRef.current.map(q=>q.id===m.requestId?{...q,attachments:[]}:q));setIntakePaused(false);}}>Continue with text only</button>}</small>}
|
|
2219
|
+
{!!m.attachments?.length&&<div className="sent-attachments">{m.attachments.map(a=><figure key={a.id}>{a.data&&a.mime.startsWith('image/')&&<img src={`data:${a.mime};base64,${a.data}`} alt={a.name}/>}<figcaption><Paperclip size={12}/>{a.name}{a.originalNotSaved && <small>Original file not saved; attach it again to review its contents.</small>}</figcaption></figure>)}</div>}
|
|
2220
|
+
{m.fix && (
|
|
2221
|
+
<button className="btn fix" onClick={() => setModal("connections")}>
|
|
2222
|
+
Fix connection
|
|
2223
|
+
</button>
|
|
2224
|
+
)}
|
|
2225
|
+
{m.signals?.length > 0 && (
|
|
2226
|
+
<div className="signals" aria-label="What the harness did">
|
|
2227
|
+
{m.signals.map((sig) => (
|
|
2228
|
+
<span key={sig}>{sig}</span>
|
|
2229
|
+
))}
|
|
2230
|
+
</div>
|
|
2231
|
+
)}
|
|
2232
|
+
{m.contextUsed && <ContextUsed value={m.contextUsed} />}
|
|
2233
|
+
</div>
|
|
2234
|
+
);})}
|
|
2235
|
+
{pending && diff && (
|
|
2236
|
+
<div className="proposal">
|
|
2237
|
+
<strong>{pending.title}</strong>
|
|
2238
|
+
<ul>
|
|
2239
|
+
{diff.added.map((l) => <li key={`a${l}`} className="add">+ {l}</li>)}
|
|
2240
|
+
{diff.changed.map((l) => <li key={`c${l}`} className="chg">~ {l}</li>)}
|
|
2241
|
+
{diff.removed.map((l) => <li key={`r${l}`} className="del">− {l}</li>)}
|
|
2242
|
+
{(diff.wiresAdded > 0 || diff.wiresRemoved > 0) && (
|
|
2243
|
+
<li className="muted">
|
|
2244
|
+
wires +{diff.wiresAdded} −{diff.wiresRemoved}
|
|
2245
|
+
</li>
|
|
2246
|
+
)}
|
|
2247
|
+
</ul>
|
|
2248
|
+
<small className="muted">Based on r{pending.baseRevision}</small>
|
|
2249
|
+
<div className="row">
|
|
2250
|
+
<button className="btn primary" disabled={busy} onClick={() => applyProposal()}>Apply to canvas</button>
|
|
2251
|
+
<button className="btn ghost" onClick={() => setPending(null)}>Dismiss</button>
|
|
2252
|
+
</div>
|
|
2253
|
+
</div>
|
|
2254
|
+
)}
|
|
2255
|
+
{agentBusy && !(session?.architectureActivityEnabled&&latestArchitectureTurn?.clientStatus==='submitted') && (
|
|
2256
|
+
<div className="msg assistant working">
|
|
2257
|
+
<BrainActivity size={24} /> Architecting…
|
|
2258
|
+
<button className="link" onClick={() => agentAbort.current?.abort()}>Cancel</button>
|
|
2259
|
+
</div>
|
|
2260
|
+
)}
|
|
2261
|
+
</div>
|
|
2262
|
+
</div>
|
|
2263
|
+
{!!intake.filter(q=>q.projectId===project.id).length&&<div className="intake-status" role="status">{intake.filter(q=>q.projectId===project.id).length} update(s) {intakePaused?'saved · paused':'queued · combining next'}{intakeIssue&&<span> {intakeIssue}</span>}{intakePaused&&<button className="link" onClick={()=>setIntakePaused(false)}>Resume</button>}</div>}
|
|
2264
|
+
<div className="prompts">
|
|
2265
|
+
{mode === "design" && brief(project) && ask && (
|
|
2266
|
+
<>
|
|
2267
|
+
<button disabled={busy || agentBusy} onClick={() => send("Not sure")}>Not sure</button>
|
|
2268
|
+
<button onClick={() => setMessages((m) => [...m, { role: "assistant", text: `Why I ask: ${ask.why || "it shapes the design."}` }])}>Why?</button>
|
|
2269
|
+
<button onClick={skipQuestion}>Skip</button>
|
|
2270
|
+
</>
|
|
2271
|
+
)}
|
|
2272
|
+
{mode === "design" && project.workflow && !project.nodes.length && !pending && (
|
|
2273
|
+
<button className="primary-chip" disabled={busy || agentBusy} onClick={sketchIt}>Sketch it →</button>
|
|
2274
|
+
)}
|
|
2275
|
+
{PROMPTS[mode].filter(() => project.nodes.length > 0 && !["idea","scope"].includes(guidance.id)).map((s) => (
|
|
2276
|
+
<button key={s} disabled={busy || agentBusy} onClick={() => send(s)}>
|
|
2277
|
+
{s}
|
|
2278
|
+
</button>
|
|
2279
|
+
))}
|
|
2280
|
+
</div>
|
|
2281
|
+
{readingAttachments&&<p className="attachment-status" role="status"><BrainActivity/> Reading files…</p>}
|
|
2282
|
+
{attachmentError&&<p className="attachment-status" role="alert">{attachmentError}</p>}
|
|
2283
|
+
{!!attachments.length&&<div className="attachment-tray" aria-label="Attachments ready to send">{attachments.map(a=><div className="attachment-chip" key={a.id}>{a.data&&a.mime.startsWith('image/')?<img src={`data:${a.mime};base64,${a.data}`} alt=""/>:<Paperclip size={16}/>}<span>{a.name}<small>{!a.data?'Original missing · attach again':a.mime.startsWith('image/')?'Image':a.mime==='application/pdf'||a.mime.includes('wordprocessingml')?'Text will be extracted; embedded images omitted':'Document text'}</small></span><button className="icon" aria-label={`Remove ${a.name}`} onClick={()=>setAttachments(list=>list.filter(item=>item.id!==a.id))}><X size={14}/></button></div>)}<p>Sent to your selected architect with this message. Original files are not stored in project files.</p></div>}
|
|
2284
|
+
<form
|
|
2285
|
+
className="composer"
|
|
2286
|
+
onSubmit={(e) => {
|
|
2287
|
+
e.preventDefault();
|
|
2288
|
+
void postMessage();
|
|
2289
|
+
}}
|
|
2290
|
+
>
|
|
2291
|
+
<textarea
|
|
2292
|
+
onPaste={e=>{const files=[...e.clipboardData.files];if(files.length){e.preventDefault();void attachFiles(files);}}}
|
|
2293
|
+
ref={messageInput}
|
|
2294
|
+
aria-label="Your message"
|
|
2295
|
+
placeholder={brief(project) ? "Reply in your own words…" : "Describe what you want to build…"}
|
|
2296
|
+
rows={2}
|
|
2297
|
+
value={input}
|
|
2298
|
+
onChange={(e) => { stopVoice(); setInput(e.target.value); }}
|
|
2299
|
+
onKeyDown={(e) => {
|
|
2300
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
2301
|
+
e.preventDefault();
|
|
2302
|
+
void postMessage();
|
|
2303
|
+
}
|
|
2304
|
+
}}
|
|
2305
|
+
/>
|
|
2306
|
+
<div className="composer-actions">
|
|
2307
|
+
<button type="button" className="icon attach-button" aria-label="Attach images or documents" title="Attach images or documents" disabled={readingAttachments} onClick={()=>attachmentInput.current.click()}><Paperclip size={18}/></button>
|
|
2308
|
+
<input id="chat-attachment-input" type="file" ref={attachmentInput} accept={ATTACHMENT_ACCEPT} multiple hidden onChange={e=>{void attachFiles(e.target.files);e.target.value='';}}/>
|
|
2309
|
+
<button
|
|
2310
|
+
type="button"
|
|
2311
|
+
className={`mic ${voiceActive ? "on" : ""}`}
|
|
2312
|
+
aria-label={voiceActive ? "Stop dictation" : "Start dictation"}
|
|
2313
|
+
aria-pressed={voiceActive}
|
|
2314
|
+
title={voiceActive ? "Stop dictation and finish the transcript" : "Dictate locally with Whisper"}
|
|
2315
|
+
disabled={voiceState === "transcribing"}
|
|
2316
|
+
onClick={voiceActive ? () => { void voice.current?.finish(); } : startVoice}
|
|
2317
|
+
>
|
|
2318
|
+
{voiceActive ? <Square size={13} /> : <Mic size={15} />}
|
|
2319
|
+
</button>
|
|
2320
|
+
<button type="submit" className="send" aria-label="Send message" disabled={(!input.trim() && !voiceActive && !attachments.length) || busy || readingAttachments || voiceState === "transcribing"}>
|
|
2321
|
+
<ArrowUp size={15} />
|
|
2322
|
+
</button>
|
|
2323
|
+
</div>
|
|
2324
|
+
</form>
|
|
2325
|
+
{session&&<OpenAIVoicePanel key={project.id} session={session} connection={connection} transportRef={openaiVoice} onConnections={()=>setModal('connections')} onStart={()=>{voice.current?.cancel();setVoiceError('');}} onTranscript={(text,automatic)=>{
|
|
2326
|
+
if(automatic&&!input.trim()&&!attachments.length){setInput(text);void send(text);}
|
|
2327
|
+
else setInput(current=>current+(current.trim()?' ':'')+text);
|
|
2328
|
+
}}/>}
|
|
2329
|
+
<div className="voice-status" role="status" aria-live="polite">
|
|
2330
|
+
{!voiceError&&["starting","transcribing"].includes(voiceState)&&<BrainActivity size={22}/>}
|
|
2331
|
+
{voiceError || ({ starting: "Opening microphone…", listening: "Dictating locally — review your words, then press Send.", transcribing: "Finishing your transcript…" }[voiceState] || "")}
|
|
2332
|
+
</div>
|
|
2333
|
+
</aside>
|
|
2334
|
+
)}
|
|
2335
|
+
|
|
2336
|
+
<input type="file" id="requirements-file-input" ref={requirementsInput} accept={ATTACHMENT_ACCEPT} multiple hidden onChange={async e=>{const files=[...e.target.files];e.target.value='';if(!files.length||readingAttachments)return;const uploadOwner=projectRef.current.id;setReadingAttachments(true);setAttachmentError('');try{const added=await readAttachments(files,attachments);if(projectRef.current.id!==uploadOwner)return;const all=[...attachments,...added];setAttachments(all);setChatOpen(true);setInput('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.');if(connection&&session){setReadingAttachments(false);void send('Read these requirements and diagrams. Summarise what you understand, propose requirements and a connected design, and ask one focused question about anything important that is unclear. Preserve existing systems and distinguish source facts from assumptions.','chat',null,null,{attachments:all});}else{setModal('connections');}}catch(error){setAttachmentError(error.message);}finally{setReadingAttachments(false);}}}/>
|
|
2337
|
+
<input id="project-file-input" type="file" ref={fileRef} accept="application/json,.json" hidden onChange={importProject} />
|
|
2338
|
+
{toast && (
|
|
2339
|
+
<div className="toast" role="status">
|
|
2340
|
+
{toast === "Preparing documents and diagram exports…"&&<BrainActivity/>}{toast}
|
|
2341
|
+
</div>
|
|
2342
|
+
)}
|
|
2343
|
+
{modal && (
|
|
2344
|
+
<div className="backdrop" onMouseDown={(e) => e.target === e.currentTarget && !(modal === "welcome" && !connection) && setModal(null)}>
|
|
2345
|
+
<section className="modal" role="dialog" aria-modal="true" aria-label={modal}>
|
|
2346
|
+
{!(modal === "welcome" && !connection) && <button className="icon modal-close" onClick={() => setModal(null)} aria-label="Close dialog">
|
|
2347
|
+
<X size={16} />
|
|
2348
|
+
</button>}
|
|
2349
|
+
{modal === "project-file" ? (<><h2>Use a saved project</h2><p>Open the original project, or create a separate project from its requirements and design.</p><button className="btn" onClick={()=>{fileImportAsCopy.current=false;fileRef.current.click();}}>Open saved project</button><button className="btn primary" onClick={()=>{fileImportAsCopy.current=true;fileRef.current.click();}}>Start a new project from a saved file</button></>) : modal === "memory" ? (
|
|
2350
|
+
<MemoryPanel key={project.id} session={session} project={project} onError={notify} onStatus={status=>setMemorySync({projectId:project.id,...status})} />
|
|
2351
|
+
) : modal === "templates" ? (
|
|
2352
|
+
<TemplateDialog onLoad={loadDesign} />
|
|
2353
|
+
) : modal === "import" ? (
|
|
2354
|
+
<ImportDialog onLoad={loadDesign} onNotify={notify} />
|
|
2355
|
+
) : modal === "pulumi" ? (
|
|
2356
|
+
<PulumiDialog
|
|
2357
|
+
snapshot={snapshot}
|
|
2358
|
+
onSnapshot={setSnapshot}
|
|
2359
|
+
onLoad={loadDesign}
|
|
2360
|
+
onExport={exportPulumi}
|
|
2361
|
+
onNotify={notify}
|
|
2362
|
+
environment={environment}
|
|
2363
|
+
/>
|
|
2364
|
+
) : modal === "projects" ? (
|
|
2365
|
+
<div><div className="row"><button className="btn" onClick={()=>setModal("project-file")}>Open or copy a project file</button><button className="btn" onClick={saveProjectFile}>Save project backup (JSON)</button></div><ProjectsDialog
|
|
2366
|
+
list={saved}
|
|
2367
|
+
folder={saved.folder}
|
|
2368
|
+
currentId={project.id}
|
|
2369
|
+
onOpen={openSaved}
|
|
2370
|
+
onNew={newProject}
|
|
2371
|
+
onBin={binProject}
|
|
2372
|
+
onRestore={restoreProject}
|
|
2373
|
+
onPurge={purgeProject}
|
|
2374
|
+
/></div>
|
|
2375
|
+
) : modal === "start-over" ? (
|
|
2376
|
+
<>
|
|
2377
|
+
<h2>Start over?</h2>
|
|
2378
|
+
<p>Start a new idea with an empty board and chat. Requirements, decisions and estimates start fresh too. Your model connection stays.</p>
|
|
2379
|
+
<div className="reset-actions">
|
|
2380
|
+
<button className="btn primary" title="Download your project, then start a new one" aria-label="Save project and start over" onClick={() => {
|
|
2381
|
+
if (!saveProjectFile()) return;
|
|
2382
|
+
switchTo(validateImport(emptyProject()));
|
|
2383
|
+
setModal(null);
|
|
2384
|
+
}}><Package size={18}/> Save & start new</button>
|
|
2385
|
+
<button
|
|
2386
|
+
className="btn" title="Start a new project without downloading this one" aria-label="Start over without download"
|
|
2387
|
+
onClick={() => {
|
|
2388
|
+
switchTo(validateImport(emptyProject()));
|
|
2389
|
+
setModal(null);
|
|
2390
|
+
}}
|
|
2391
|
+
>
|
|
2392
|
+
<Plus size={18}/> Start without saving
|
|
2393
|
+
</button>
|
|
2394
|
+
<button className="btn ghost" onClick={() => setModal(null)}>
|
|
2395
|
+
<ArrowLeft size={18}/> Keep working
|
|
2396
|
+
</button>
|
|
2397
|
+
</div>
|
|
2398
|
+
<p className="modal-fine">Save a project file to reopen this engagement later. Undo and redo apply within the current project.</p>
|
|
2399
|
+
</>
|
|
2400
|
+
) : modal === "connections" || modal === "welcome" ? (
|
|
2401
|
+
<Connections
|
|
2402
|
+
session={session}
|
|
2403
|
+
connection={connection}
|
|
2404
|
+
onRetry={refreshSession}
|
|
2405
|
+
welcome={!connection}
|
|
2406
|
+
onCheck={verifyConnection}
|
|
2407
|
+
onSave={(choice) => {
|
|
2408
|
+
setConnection(choice);
|
|
2409
|
+
saveConnection(choice);
|
|
2410
|
+
setModal(null);
|
|
2411
|
+
}}
|
|
2412
|
+
/>
|
|
2413
|
+
) : modal === "licenses" ? (
|
|
2414
|
+
<>
|
|
2415
|
+
<h2>Licences</h2>
|
|
2416
|
+
<dl className="licences">
|
|
2417
|
+
<dt>OpenArchFlow</dt>
|
|
2418
|
+
<dd>
|
|
2419
|
+
MIT · © 2026 Rafael Sales. Catalogues, templates, import parsers, layout, payload formatting.{" "}
|
|
2420
|
+
<a href="/licenses/openarchflow-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
|
|
2421
|
+
</dd>
|
|
2422
|
+
<dt>AWS GenAI Solutions Architect Agent</dt>
|
|
2423
|
+
<dd>
|
|
2424
|
+
MIT-0. Agent composition, retry and rate limiting in the sidecar; prompt replaced with a cloud-neutral one.{" "}
|
|
2425
|
+
<a href="/licenses/aws-architect-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
|
|
2426
|
+
</dd>
|
|
2427
|
+
<dt>AWS Labs MCP servers</dt>
|
|
2428
|
+
<dd>
|
|
2429
|
+
Apache-2.0. Manifest only; not bundled or executed.{" "}
|
|
2430
|
+
<a href="/licenses/aws-mcp-LICENSE.txt" target="_blank" rel="noreferrer">Licence</a>
|
|
2431
|
+
</dd>
|
|
2432
|
+
</dl>
|
|
2433
|
+
<a href="/licenses/dependency-notices.txt" target="_blank" rel="noreferrer">Dependency notices</a>
|
|
2434
|
+
<p className="modal-fine">Cloud names identify design concepts. No vendor endorsement or icon packs.</p>
|
|
2435
|
+
</>
|
|
2436
|
+
) : (
|
|
2437
|
+
<>
|
|
2438
|
+
<h2>AtMem AI Architect</h2>
|
|
2439
|
+
<p className="muted">Version {appVersion} · local candidate</p>
|
|
2440
|
+
<p>
|
|
2441
|
+
Start in Design to describe your project and develop its architecture. Follow Architecture activity while it works, then choose Build this design to create and check its implementation package.
|
|
2442
|
+
Part of atmem.ai. Runs on your computer; nothing is deployed.
|
|
2443
|
+
</p>
|
|
2444
|
+
<p className="muted">Keys: / add block · Esc up a level · ⌘↵ run</p>
|
|
2445
|
+
</>
|
|
2446
|
+
)}
|
|
2447
|
+
</section>
|
|
2448
|
+
</div>
|
|
2449
|
+
)}
|
|
2450
|
+
</div>
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
createRoot(document.getElementById("root")).render(
|
|
2455
|
+
<ReactFlowProvider>
|
|
2456
|
+
<App />
|
|
2457
|
+
</ReactFlowProvider>,
|
|
2458
|
+
);
|