machinaos 0.0.76 → 0.0.78
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/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sound.ts — per-theme WebAudio sound packs.
|
|
3
|
+
*
|
|
4
|
+
* Synthesizes click / hover / type / success / error / run / save /
|
|
5
|
+
* modalOpen / modalClose events via a single OscillatorNode + GainNode
|
|
6
|
+
* per call. No external samples; no module-level audio context until
|
|
7
|
+
* the first play(). Off by default — toggle with `Sounds.setEnabled`.
|
|
8
|
+
*
|
|
9
|
+
* The active pack is selected by name (matches the per-theme
|
|
10
|
+
* `--sound-pack` CSS token). React glue lives in
|
|
11
|
+
* client/src/hooks/useSound.ts which:
|
|
12
|
+
* 1. Reads `--sound-pack` from `:root` whenever the active theme
|
|
13
|
+
* changes and calls `Sounds.setPack(...)`.
|
|
14
|
+
* 2. Wires `Sounds.setEnabled(...)` to the `soundEnabled` Zustand
|
|
15
|
+
* slice (persisted to localStorage as `machinaos-sound`).
|
|
16
|
+
*
|
|
17
|
+
* Ported from design_handoff_machinaos_themes/app/sound.js. The DOM
|
|
18
|
+
* autocapture (`document.addEventListener('click', ...)`) at the
|
|
19
|
+
* bottom of the upstream module is intentionally dropped — in React
|
|
20
|
+
* we fire `play()` from explicit handlers (ActionButton onClick,
|
|
21
|
+
* Modal open/close effect, etc.) so the side-effect surface is
|
|
22
|
+
* traceable.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type SoundEvent =
|
|
26
|
+
| 'click'
|
|
27
|
+
| 'hover'
|
|
28
|
+
| 'type'
|
|
29
|
+
| 'success'
|
|
30
|
+
| 'error'
|
|
31
|
+
| 'run'
|
|
32
|
+
| 'save'
|
|
33
|
+
| 'modalOpen'
|
|
34
|
+
| 'modalClose';
|
|
35
|
+
|
|
36
|
+
export type SoundPackName =
|
|
37
|
+
| 'none'
|
|
38
|
+
| 'parchment'
|
|
39
|
+
| 'marble'
|
|
40
|
+
| 'ink'
|
|
41
|
+
| 'clockwork'
|
|
42
|
+
| 'vibraphone'
|
|
43
|
+
| 'terminal'
|
|
44
|
+
| 'scrap'
|
|
45
|
+
| 'crypt'
|
|
46
|
+
| 'bell'
|
|
47
|
+
| 'telex';
|
|
48
|
+
|
|
49
|
+
interface OscConfig {
|
|
50
|
+
type: OscillatorType;
|
|
51
|
+
freq: number;
|
|
52
|
+
dur: number;
|
|
53
|
+
vol: number;
|
|
54
|
+
attack: number;
|
|
55
|
+
decay: number;
|
|
56
|
+
/** Optional low-pass filter cutoff (Hz). */
|
|
57
|
+
lp?: number;
|
|
58
|
+
/** Optional frequency sweep multiplier; final = freq * sweep. */
|
|
59
|
+
sweep?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type EventTable = Partial<Record<SoundEvent, OscConfig>>;
|
|
63
|
+
|
|
64
|
+
// Singleton AudioContext lazily constructed on first play.
|
|
65
|
+
let audioCtx: AudioContext | null = null;
|
|
66
|
+
function ensureCtx(): AudioContext | null {
|
|
67
|
+
if (audioCtx) return audioCtx;
|
|
68
|
+
try {
|
|
69
|
+
const Ctx = window.AudioContext || (window as any).webkitAudioContext;
|
|
70
|
+
if (!Ctx) return null;
|
|
71
|
+
audioCtx = new Ctx();
|
|
72
|
+
return audioCtx;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let enabled = false;
|
|
79
|
+
let activePackName: SoundPackName = 'none';
|
|
80
|
+
let activePack: EventTable = {};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Per-event last-fire timestamps for throttling. Currently only `type`
|
|
84
|
+
* is throttled — rapid typing into a long input shouldn't queue dozens
|
|
85
|
+
* of OscillatorNodes per second. Other events (click, hover, success,
|
|
86
|
+
* etc.) are user-paced and don't need throttling.
|
|
87
|
+
*
|
|
88
|
+
* Uses `performance.now()` for monotonic timing. The 30 ms window
|
|
89
|
+
* matches the upstream `app/sound.js` reference engine's documented
|
|
90
|
+
* hover debounce; we apply the same to `type` since each oscillator
|
|
91
|
+
* costs ~25 ms decay on the parchment / marble packs.
|
|
92
|
+
*/
|
|
93
|
+
const lastFireMs: Partial<Record<SoundEvent, number>> = {};
|
|
94
|
+
const THROTTLE_MS: Partial<Record<SoundEvent, number>> = {
|
|
95
|
+
type: 30,
|
|
96
|
+
hover: 30,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function play(cfg: OscConfig | undefined): void {
|
|
100
|
+
if (!cfg || !enabled) return;
|
|
101
|
+
const ac = ensureCtx();
|
|
102
|
+
if (!ac) return;
|
|
103
|
+
// If suspended, defer oscillator scheduling until resume() settles.
|
|
104
|
+
// Synchronously scheduled oscillators on a suspended context are
|
|
105
|
+
// silently dropped per the WebAudio autoplay policy
|
|
106
|
+
// (https://developer.chrome.com/blog/autoplay/#webaudio).
|
|
107
|
+
const schedule = () => {
|
|
108
|
+
try {
|
|
109
|
+
const osc = ac.createOscillator();
|
|
110
|
+
const gain = ac.createGain();
|
|
111
|
+
osc.type = cfg.type;
|
|
112
|
+
osc.frequency.value = cfg.freq;
|
|
113
|
+
let target: AudioNode = osc;
|
|
114
|
+
if (cfg.lp) {
|
|
115
|
+
const lp = ac.createBiquadFilter();
|
|
116
|
+
lp.type = 'lowpass';
|
|
117
|
+
lp.frequency.value = cfg.lp;
|
|
118
|
+
target.connect(lp);
|
|
119
|
+
target = lp;
|
|
120
|
+
}
|
|
121
|
+
target.connect(gain);
|
|
122
|
+
gain.connect(ac.destination);
|
|
123
|
+
const now = ac.currentTime;
|
|
124
|
+
gain.gain.setValueAtTime(0, now);
|
|
125
|
+
gain.gain.linearRampToValueAtTime(cfg.vol, now + cfg.attack);
|
|
126
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, now + cfg.attack + cfg.decay);
|
|
127
|
+
if (cfg.sweep) {
|
|
128
|
+
osc.frequency.exponentialRampToValueAtTime(cfg.freq * cfg.sweep, now + cfg.dur);
|
|
129
|
+
}
|
|
130
|
+
osc.start(now);
|
|
131
|
+
osc.stop(now + cfg.dur + 0.02);
|
|
132
|
+
} catch {
|
|
133
|
+
// WebAudio errors are non-fatal — silently drop.
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
if (ac.state === 'running') {
|
|
137
|
+
schedule();
|
|
138
|
+
} else {
|
|
139
|
+
void Promise.resolve(ac.resume()).then(schedule).catch(() => {/* noop */});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Sound packs ──────────────────────────────────────────────────────────
|
|
144
|
+
//
|
|
145
|
+
// Each pack maps each `SoundEvent` to an oscillator config. Volume is
|
|
146
|
+
// kept ≤ 0.10 across the board so no pack clips. Configs are ported
|
|
147
|
+
// verbatim from app/sound.js (handoff bundle).
|
|
148
|
+
|
|
149
|
+
const PARCHMENT: EventTable = {
|
|
150
|
+
click: { type: 'sine', freq: 220, dur: 0.04, vol: 0.06, attack: 0.005, decay: 0.04 },
|
|
151
|
+
hover: { type: 'triangle', freq: 380, dur: 0.02, vol: 0.025, attack: 0.002, decay: 0.02 },
|
|
152
|
+
type: { type: 'square', freq: 180, dur: 0.025, vol: 0.04, attack: 0.001, decay: 0.025, lp: 800 },
|
|
153
|
+
success: { type: 'sine', freq: 523, dur: 0.4, vol: 0.08, attack: 0.01, decay: 0.4, sweep: 1.5 },
|
|
154
|
+
error: { type: 'sawtooth', freq: 110, dur: 0.25, vol: 0.10, attack: 0.005, decay: 0.25, lp: 600 },
|
|
155
|
+
run: { type: 'sine', freq: 392, dur: 0.18, vol: 0.10, attack: 0.01, decay: 0.18, sweep: 1.3 },
|
|
156
|
+
save: { type: 'triangle', freq: 294, dur: 0.3, vol: 0.10, attack: 0.02, decay: 0.3 },
|
|
157
|
+
modalOpen: { type: 'sine', freq: 392, dur: 0.18, vol: 0.07, attack: 0.02, decay: 0.18, sweep: 1.2 },
|
|
158
|
+
modalClose: { type: 'sine', freq: 392, dur: 0.14, vol: 0.06, attack: 0.005, decay: 0.14, sweep: 0.7 },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const MARBLE: EventTable = {
|
|
162
|
+
click: { type: 'sine', freq: 330, dur: 0.05, vol: 0.07, attack: 0.001, decay: 0.05 },
|
|
163
|
+
hover: { type: 'sine', freq: 520, dur: 0.02, vol: 0.025, attack: 0.001, decay: 0.02 },
|
|
164
|
+
type: { type: 'triangle', freq: 260, dur: 0.03, vol: 0.04, attack: 0.001, decay: 0.03, lp: 1200 },
|
|
165
|
+
success: { type: 'sine', freq: 392, dur: 0.5, vol: 0.09, attack: 0.005, decay: 0.5, sweep: 1.6 },
|
|
166
|
+
error: { type: 'sawtooth', freq: 130, dur: 0.30, vol: 0.10, attack: 0.005, decay: 0.30, lp: 500 },
|
|
167
|
+
run: { type: 'sine', freq: 440, dur: 0.22, vol: 0.10, attack: 0.005, decay: 0.22, sweep: 1.4 },
|
|
168
|
+
save: { type: 'sine', freq: 330, dur: 0.35, vol: 0.10, attack: 0.01, decay: 0.35 },
|
|
169
|
+
modalOpen: { type: 'sine', freq: 392, dur: 0.20, vol: 0.07, attack: 0.02, decay: 0.20, sweep: 1.3 },
|
|
170
|
+
modalClose: { type: 'sine', freq: 392, dur: 0.16, vol: 0.06, attack: 0.005, decay: 0.16, sweep: 0.7 },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const INK: EventTable = {
|
|
174
|
+
click: { type: 'triangle', freq: 280, dur: 0.05, vol: 0.05, attack: 0.005, decay: 0.05, lp: 1500 },
|
|
175
|
+
hover: { type: 'sine', freq: 440, dur: 0.018, vol: 0.02, attack: 0.001, decay: 0.018 },
|
|
176
|
+
type: { type: 'sine', freq: 220, dur: 0.04, vol: 0.04, attack: 0.005, decay: 0.04, lp: 600 },
|
|
177
|
+
success: { type: 'sine', freq: 660, dur: 1.2, vol: 0.08, attack: 0.01, decay: 1.2 },
|
|
178
|
+
error: { type: 'triangle', freq: 174, dur: 0.4, vol: 0.08, attack: 0.005, decay: 0.4, lp: 800 },
|
|
179
|
+
run: { type: 'sine', freq: 523, dur: 0.6, vol: 0.08, attack: 0.02, decay: 0.6 },
|
|
180
|
+
save: { type: 'sine', freq: 440, dur: 0.8, vol: 0.10, attack: 0.01, decay: 0.8 },
|
|
181
|
+
modalOpen: { type: 'sine', freq: 523, dur: 0.4, vol: 0.06, attack: 0.04, decay: 0.4 },
|
|
182
|
+
modalClose: { type: 'sine', freq: 392, dur: 0.4, vol: 0.05, attack: 0.005, decay: 0.4 },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const CLOCKWORK: EventTable = {
|
|
186
|
+
click: { type: 'square', freq: 800, dur: 0.04, vol: 0.06, attack: 0.001, decay: 0.04, lp: 2200 },
|
|
187
|
+
hover: { type: 'square', freq: 1100, dur: 0.012, vol: 0.02, attack: 0.001, decay: 0.012 },
|
|
188
|
+
type: { type: 'square', freq: 600, dur: 0.025, vol: 0.05, attack: 0.001, decay: 0.025, lp: 1800 },
|
|
189
|
+
success: { type: 'sawtooth', freq: 523, dur: 0.5, vol: 0.08, attack: 0.005, decay: 0.5, sweep: 1.5, lp: 2500 },
|
|
190
|
+
error: { type: 'sawtooth', freq: 196, dur: 0.35, vol: 0.10, attack: 0.005, decay: 0.35, lp: 700 },
|
|
191
|
+
run: { type: 'square', freq: 440, dur: 0.30, vol: 0.10, attack: 0.005, decay: 0.30, sweep: 1.4, lp: 2000 },
|
|
192
|
+
save: { type: 'triangle', freq: 660, dur: 0.45, vol: 0.10, attack: 0.01, decay: 0.45, lp: 2400 },
|
|
193
|
+
modalOpen: { type: 'square', freq: 392, dur: 0.18, vol: 0.07, attack: 0.005, decay: 0.18, sweep: 1.3, lp: 1800 },
|
|
194
|
+
modalClose: { type: 'square', freq: 392, dur: 0.14, vol: 0.06, attack: 0.005, decay: 0.14, sweep: 0.65, lp: 1400 },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const VIBRAPHONE: EventTable = {
|
|
198
|
+
click: { type: 'triangle', freq: 880, dur: 0.10, vol: 0.06, attack: 0.001, decay: 0.10 },
|
|
199
|
+
hover: { type: 'triangle', freq: 1320, dur: 0.05, vol: 0.025, attack: 0.001, decay: 0.05 },
|
|
200
|
+
type: { type: 'sine', freq: 660, dur: 0.06, vol: 0.04, attack: 0.001, decay: 0.06 },
|
|
201
|
+
success: { type: 'triangle', freq: 988, dur: 0.5, vol: 0.09, attack: 0.001, decay: 0.5, sweep: 1.5 },
|
|
202
|
+
error: { type: 'sawtooth', freq: 233, dur: 0.25, vol: 0.10, attack: 0.005, decay: 0.25, sweep: 0.6 },
|
|
203
|
+
run: { type: 'triangle', freq: 1175, dur: 0.30, vol: 0.10, attack: 0.001, decay: 0.30, sweep: 1.4 },
|
|
204
|
+
save: { type: 'triangle', freq: 880, dur: 0.40, vol: 0.10, attack: 0.001, decay: 0.40 },
|
|
205
|
+
modalOpen: { type: 'triangle', freq: 988, dur: 0.18, vol: 0.06, attack: 0.001, decay: 0.18, sweep: 1.4 },
|
|
206
|
+
modalClose: { type: 'triangle', freq: 988, dur: 0.18, vol: 0.06, attack: 0.001, decay: 0.18, sweep: 0.65 },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const TERMINAL: EventTable = {
|
|
210
|
+
click: { type: 'square', freq: 1200, dur: 0.025, vol: 0.06, attack: 0.001, decay: 0.025 },
|
|
211
|
+
hover: { type: 'square', freq: 1800, dur: 0.012, vol: 0.025, attack: 0.001, decay: 0.012 },
|
|
212
|
+
type: { type: 'square', freq: 880, dur: 0.018, vol: 0.05, attack: 0.0005, decay: 0.018 },
|
|
213
|
+
success: { type: 'square', freq: 1318, dur: 0.18, vol: 0.08, attack: 0.001, decay: 0.18, sweep: 1.5 },
|
|
214
|
+
error: { type: 'sawtooth', freq: 220, dur: 0.18, vol: 0.10, attack: 0.001, decay: 0.18, sweep: 0.5 },
|
|
215
|
+
run: { type: 'square', freq: 1568, dur: 0.10, vol: 0.10, attack: 0.001, decay: 0.10 },
|
|
216
|
+
save: { type: 'square', freq: 988, dur: 0.12, vol: 0.08, attack: 0.001, decay: 0.12, sweep: 1.2 },
|
|
217
|
+
modalOpen: { type: 'square', freq: 1175, dur: 0.06, vol: 0.06, attack: 0.001, decay: 0.06, sweep: 1.4 },
|
|
218
|
+
modalClose: { type: 'square', freq: 1175, dur: 0.06, vol: 0.06, attack: 0.001, decay: 0.06, sweep: 0.6 },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const SCRAP: EventTable = {
|
|
222
|
+
click: { type: 'sawtooth', freq: 180, dur: 0.05, vol: 0.07, attack: 0.0005, decay: 0.05, lp: 1400 },
|
|
223
|
+
hover: { type: 'sawtooth', freq: 320, dur: 0.012, vol: 0.025, attack: 0.0005, decay: 0.012, lp: 2000 },
|
|
224
|
+
type: { type: 'square', freq: 140, dur: 0.03, vol: 0.05, attack: 0.0005, decay: 0.03, lp: 800 },
|
|
225
|
+
success: { type: 'sawtooth', freq: 440, dur: 0.18, vol: 0.10, attack: 0.001, decay: 0.18, sweep: 1.3, lp: 1600 },
|
|
226
|
+
error: { type: 'sawtooth', freq: 90, dur: 0.5, vol: 0.12, attack: 0.001, decay: 0.5, lp: 400 },
|
|
227
|
+
run: { type: 'square', freq: 220, dur: 0.20, vol: 0.10, attack: 0.001, decay: 0.20, sweep: 1.2, lp: 1200 },
|
|
228
|
+
save: { type: 'sawtooth', freq: 330, dur: 0.18, vol: 0.10, attack: 0.001, decay: 0.18, lp: 1400 },
|
|
229
|
+
modalOpen: { type: 'sawtooth', freq: 196, dur: 0.10, vol: 0.07, attack: 0.001, decay: 0.10, lp: 1000 },
|
|
230
|
+
modalClose: { type: 'sawtooth', freq: 196, dur: 0.08, vol: 0.06, attack: 0.001, decay: 0.08, sweep: 0.5, lp: 700 },
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const CRYPT: EventTable = {
|
|
234
|
+
click: { type: 'sine', freq: 220, dur: 0.08, vol: 0.06, attack: 0.005, decay: 0.08, lp: 600 },
|
|
235
|
+
hover: { type: 'sine', freq: 330, dur: 0.025, vol: 0.02, attack: 0.005, decay: 0.025, lp: 800 },
|
|
236
|
+
type: { type: 'sine', freq: 165, dur: 0.04, vol: 0.04, attack: 0.005, decay: 0.04, lp: 400 },
|
|
237
|
+
success: { type: 'triangle', freq: 261, dur: 0.8, vol: 0.08, attack: 0.02, decay: 0.8, sweep: 1.5, lp: 1200 },
|
|
238
|
+
error: { type: 'sawtooth', freq: 73, dur: 0.6, vol: 0.10, attack: 0.01, decay: 0.6, lp: 300 },
|
|
239
|
+
run: { type: 'sine', freq: 196, dur: 0.45, vol: 0.10, attack: 0.02, decay: 0.45, sweep: 1.3, lp: 800 },
|
|
240
|
+
save: { type: 'sine', freq: 174, dur: 0.7, vol: 0.10, attack: 0.05, decay: 0.7, lp: 700 },
|
|
241
|
+
modalOpen: { type: 'sine', freq: 220, dur: 0.5, vol: 0.07, attack: 0.05, decay: 0.5, lp: 600 },
|
|
242
|
+
modalClose: { type: 'sine', freq: 220, dur: 0.4, vol: 0.06, attack: 0.005, decay: 0.4, sweep: 0.6, lp: 500 },
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const BELL: EventTable = {
|
|
246
|
+
click: { type: 'triangle', freq: 392, dur: 0.10, vol: 0.06, attack: 0.001, decay: 0.10, lp: 1500 },
|
|
247
|
+
hover: { type: 'sine', freq: 660, dur: 0.025, vol: 0.025, attack: 0.001, decay: 0.025 },
|
|
248
|
+
type: { type: 'square', freq: 220, dur: 0.025, vol: 0.04, attack: 0.001, decay: 0.025, lp: 800 },
|
|
249
|
+
success: { type: 'triangle', freq: 523, dur: 1.0, vol: 0.09, attack: 0.001, decay: 1.0 },
|
|
250
|
+
error: { type: 'sawtooth', freq: 110, dur: 0.6, vol: 0.10, attack: 0.005, decay: 0.6, lp: 500 },
|
|
251
|
+
run: { type: 'triangle', freq: 440, dur: 0.6, vol: 0.10, attack: 0.001, decay: 0.6 },
|
|
252
|
+
save: { type: 'triangle', freq: 392, dur: 0.9, vol: 0.10, attack: 0.001, decay: 0.9 },
|
|
253
|
+
modalOpen: { type: 'triangle', freq: 523, dur: 0.4, vol: 0.07, attack: 0.001, decay: 0.4 },
|
|
254
|
+
modalClose: { type: 'triangle', freq: 392, dur: 0.4, vol: 0.06, attack: 0.001, decay: 0.4 },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const TELEX: EventTable = {
|
|
258
|
+
click: { type: 'square', freq: 1500, dur: 0.012, vol: 0.07, attack: 0.0005, decay: 0.012 },
|
|
259
|
+
hover: { type: 'square', freq: 2200, dur: 0.006, vol: 0.02, attack: 0.0005, decay: 0.006 },
|
|
260
|
+
type: { type: 'square', freq: 1100, dur: 0.010, vol: 0.05, attack: 0.0005, decay: 0.010 },
|
|
261
|
+
success: { type: 'square', freq: 1318, dur: 0.10, vol: 0.08, attack: 0.0005, decay: 0.10 },
|
|
262
|
+
error: { type: 'sawtooth', freq: 220, dur: 0.4, vol: 0.12, attack: 0.0005, decay: 0.4 },
|
|
263
|
+
run: { type: 'square', freq: 1760, dur: 0.06, vol: 0.10, attack: 0.0005, decay: 0.06 },
|
|
264
|
+
save: { type: 'square', freq: 1175, dur: 0.08, vol: 0.08, attack: 0.0005, decay: 0.08 },
|
|
265
|
+
modalOpen: { type: 'square', freq: 988, dur: 0.05, vol: 0.07, attack: 0.0005, decay: 0.05 },
|
|
266
|
+
modalClose: { type: 'square', freq: 988, dur: 0.04, vol: 0.06, attack: 0.0005, decay: 0.04, sweep: 0.6 },
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const PACKS: Record<SoundPackName, EventTable> = {
|
|
270
|
+
none: {},
|
|
271
|
+
parchment: PARCHMENT,
|
|
272
|
+
marble: MARBLE,
|
|
273
|
+
ink: INK,
|
|
274
|
+
clockwork: CLOCKWORK,
|
|
275
|
+
vibraphone: VIBRAPHONE,
|
|
276
|
+
terminal: TERMINAL,
|
|
277
|
+
scrap: SCRAP,
|
|
278
|
+
crypt: CRYPT,
|
|
279
|
+
bell: BELL,
|
|
280
|
+
telex: TELEX,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
export const Sounds = {
|
|
286
|
+
setEnabled(value: boolean): void {
|
|
287
|
+
enabled = value;
|
|
288
|
+
},
|
|
289
|
+
isEnabled(): boolean {
|
|
290
|
+
return enabled;
|
|
291
|
+
},
|
|
292
|
+
setPack(name: SoundPackName): void {
|
|
293
|
+
activePackName = name;
|
|
294
|
+
activePack = PACKS[name] ?? PACKS.none;
|
|
295
|
+
},
|
|
296
|
+
pack(): SoundPackName {
|
|
297
|
+
return activePackName;
|
|
298
|
+
},
|
|
299
|
+
/**
|
|
300
|
+
* Idempotent gesture-time unlock. Constructs the AudioContext (if not
|
|
301
|
+
* already constructed) and resumes it if the browser autoplay policy
|
|
302
|
+
* left it `suspended`. Safe to call repeatedly — `resume()` on a
|
|
303
|
+
* running context is a no-op.
|
|
304
|
+
*
|
|
305
|
+
* Modern Chrome / Safari require the resume() call to occur inside
|
|
306
|
+
* the same task as a user gesture (pointerdown / keydown / touchstart).
|
|
307
|
+
* `Sounds.play()` defensively calls resume() too, but state-change-
|
|
308
|
+
* driven plays (e.g. modal open from an effect) can land microseconds
|
|
309
|
+
* after the gesture frame and lose the very first sound. Calling
|
|
310
|
+
* `unlock()` from a once-listener on the gesture path closes that gap.
|
|
311
|
+
*/
|
|
312
|
+
unlock(): void {
|
|
313
|
+
const ac = ensureCtx();
|
|
314
|
+
if (!ac) return;
|
|
315
|
+
if (ac.state === 'suspended') void ac.resume();
|
|
316
|
+
},
|
|
317
|
+
play(event: SoundEvent): void {
|
|
318
|
+
const throttle = THROTTLE_MS[event];
|
|
319
|
+
if (throttle !== undefined) {
|
|
320
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
321
|
+
const last = lastFireMs[event] ?? 0;
|
|
322
|
+
if (now - last < throttle) return;
|
|
323
|
+
lastFireMs[event] = now;
|
|
324
|
+
}
|
|
325
|
+
play(activePack[event]);
|
|
326
|
+
},
|
|
327
|
+
/**
|
|
328
|
+
* Snapshot the engine state for DevTools self-diagnosis. Returns an
|
|
329
|
+
* object the user can inspect from the browser console:
|
|
330
|
+
* `> Sounds.diagnose()` →
|
|
331
|
+
* { enabled, pack, eventsForPack, audioContextState, browserSupport }
|
|
332
|
+
* Common silent-audio modes:
|
|
333
|
+
* - `enabled: false` → user hasn't toggled Settings → Audio.
|
|
334
|
+
* - `pack: 'none'` → :root[data-theme="..."] declares chime/typo
|
|
335
|
+
* and the validator falls through to none.
|
|
336
|
+
* - `audioContextState: 'suspended'` after a click → a browser
|
|
337
|
+
* extension or autoplay-block policy is denying resume().
|
|
338
|
+
*/
|
|
339
|
+
diagnose(): {
|
|
340
|
+
enabled: boolean;
|
|
341
|
+
pack: SoundPackName;
|
|
342
|
+
eventsForPack: string[];
|
|
343
|
+
cssSoundPack: string;
|
|
344
|
+
cssDataTheme: string | null;
|
|
345
|
+
audioContextState: string | 'no-ac' | 'no-audio-api';
|
|
346
|
+
browserSupport: boolean;
|
|
347
|
+
} {
|
|
348
|
+
const Ctx = typeof window !== 'undefined'
|
|
349
|
+
? window.AudioContext || (window as any).webkitAudioContext
|
|
350
|
+
: undefined;
|
|
351
|
+
const cssSoundPack = typeof document !== 'undefined'
|
|
352
|
+
? getComputedStyle(document.documentElement).getPropertyValue('--sound-pack').trim().replace(/['"]/g, '')
|
|
353
|
+
: '';
|
|
354
|
+
const cssDataTheme = typeof document !== 'undefined'
|
|
355
|
+
? document.documentElement.getAttribute('data-theme')
|
|
356
|
+
: null;
|
|
357
|
+
return {
|
|
358
|
+
enabled,
|
|
359
|
+
pack: activePackName,
|
|
360
|
+
eventsForPack: Object.keys(activePack),
|
|
361
|
+
cssSoundPack,
|
|
362
|
+
cssDataTheme,
|
|
363
|
+
audioContextState: !Ctx ? 'no-audio-api' : audioCtx ? audioCtx.state : 'no-ac',
|
|
364
|
+
browserSupport: !!Ctx,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
/**
|
|
368
|
+
* Force a sound to play for testing — bypasses the `enabled` gate.
|
|
369
|
+
* Call from DevTools: `Sounds.test('click')` after the AC has unlocked.
|
|
370
|
+
* Returns whether the play scheduled successfully.
|
|
371
|
+
*/
|
|
372
|
+
test(event: SoundEvent = 'click'): boolean {
|
|
373
|
+
const cfg = activePack[event];
|
|
374
|
+
if (!cfg) return false;
|
|
375
|
+
const ac = ensureCtx();
|
|
376
|
+
if (!ac) return false;
|
|
377
|
+
const wasEnabled = enabled;
|
|
378
|
+
enabled = true;
|
|
379
|
+
try {
|
|
380
|
+
play(cfg);
|
|
381
|
+
return true;
|
|
382
|
+
} finally {
|
|
383
|
+
enabled = wasEnabled;
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Expose on `window` in dev mode so users can self-diagnose silent audio
|
|
389
|
+
// from DevTools (`> Sounds.diagnose()`, `> Sounds.test()`). No-op in
|
|
390
|
+
// production builds — Vite tree-shakes the `import.meta.env.DEV` branch.
|
|
391
|
+
if (typeof window !== 'undefined' && import.meta.env.DEV) {
|
|
392
|
+
(window as any).Sounds = Sounds;
|
|
393
|
+
}
|
package/client/src/main.tsx
CHANGED
|
@@ -2,6 +2,26 @@ import { StrictMode } from 'react'
|
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
|
4
4
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
5
|
+
// Theme CSS — load BEFORE index.css so the per-theme [data-theme="..."]
|
|
6
|
+
// blocks register their tokens before Tailwind compiles `@theme inline`
|
|
7
|
+
// against the cascade. Order within the theme list matches specificity:
|
|
8
|
+
// base.css (neutral defaults) → light.css (default :root + [data-theme="light"])
|
|
9
|
+
// → dark.css (.dark + [data-theme="dark"]) → renaissance + cyber overrides.
|
|
10
|
+
import './themes/base.css'
|
|
11
|
+
import './themes/light.css'
|
|
12
|
+
import './themes/dark.css'
|
|
13
|
+
// Utopian set
|
|
14
|
+
import './themes/renaissance.css'
|
|
15
|
+
import './themes/greek.css'
|
|
16
|
+
import './themes/edo.css'
|
|
17
|
+
import './themes/steampunk.css'
|
|
18
|
+
import './themes/atomic.css'
|
|
19
|
+
// Dystopian set
|
|
20
|
+
import './themes/cyber.css'
|
|
21
|
+
import './themes/wasteland.css'
|
|
22
|
+
import './themes/rot.css'
|
|
23
|
+
import './themes/plague.css'
|
|
24
|
+
import './themes/surveillance.css'
|
|
5
25
|
import './index.css'
|
|
6
26
|
import App from './App'
|
|
7
27
|
import { ThemeProvider } from './contexts/ThemeContext'
|
|
@@ -49,6 +49,13 @@ interface AppStore {
|
|
|
49
49
|
componentPaletteVisible: boolean;
|
|
50
50
|
consolePanelVisible: boolean;
|
|
51
51
|
proMode: boolean; // false = noob mode (only AI categories), true = pro mode (all categories)
|
|
52
|
+
/** WebAudio sound effects toggle (per-theme pack picked from
|
|
53
|
+
* --sound-pack CSS token by `useSoundSync()`). Persisted to
|
|
54
|
+
* localStorage as `machinaos-sound`; default ON (user disables in
|
|
55
|
+
* Settings -> Audio). The AudioContext starts suspended per
|
|
56
|
+
* browser autoplay policy — `Sounds.unlock()` resumes it on the
|
|
57
|
+
* user's first interaction (no separate audio permission needed). */
|
|
58
|
+
soundEnabled: boolean;
|
|
52
59
|
renamingNodeId: string | null;
|
|
53
60
|
|
|
54
61
|
// Workflow actions
|
|
@@ -65,6 +72,8 @@ interface AppStore {
|
|
|
65
72
|
toggleSidebar: () => void;
|
|
66
73
|
toggleComponentPalette: () => void;
|
|
67
74
|
toggleProMode: () => void;
|
|
75
|
+
setSoundEnabled: (enabled: boolean) => void;
|
|
76
|
+
toggleSoundEnabled: () => void;
|
|
68
77
|
setRenamingNodeId: (nodeId: string | null) => void;
|
|
69
78
|
|
|
70
79
|
// UI defaults from database
|
|
@@ -130,6 +139,10 @@ const STORAGE_KEYS = {
|
|
|
130
139
|
componentPaletteVisible: 'ui_component_palette_visible',
|
|
131
140
|
consolePanelVisible: 'ui_console_panel_visible',
|
|
132
141
|
proMode: 'ui_pro_mode',
|
|
142
|
+
/** Sound enabled key — matches the design handoff's
|
|
143
|
+
* `localStorage['machinaos-sound']` convention so a returning user's
|
|
144
|
+
* prior choice rehydrates regardless of which session set it. */
|
|
145
|
+
soundEnabled: 'machinaos-sound',
|
|
133
146
|
};
|
|
134
147
|
|
|
135
148
|
// Helper to load boolean from localStorage
|
|
@@ -163,6 +176,7 @@ export const useAppStore = create<AppStore>((set, get) => ({
|
|
|
163
176
|
componentPaletteVisible: loadBooleanFromStorage(STORAGE_KEYS.componentPaletteVisible, true),
|
|
164
177
|
consolePanelVisible: loadBooleanFromStorage(STORAGE_KEYS.consolePanelVisible, false),
|
|
165
178
|
proMode: loadBooleanFromStorage(STORAGE_KEYS.proMode, false), // Default to noob mode
|
|
179
|
+
soundEnabled: loadBooleanFromStorage(STORAGE_KEYS.soundEnabled, true), // On by default; user can disable in Settings -> Audio. Browsers gesture-gate WebAudio (no separate permission), so the AC unlocks on first interaction via Sounds.unlock().
|
|
166
180
|
renamingNodeId: null,
|
|
167
181
|
|
|
168
182
|
// Workflow management
|
|
@@ -331,6 +345,18 @@ export const useAppStore = create<AppStore>((set, get) => ({
|
|
|
331
345
|
});
|
|
332
346
|
},
|
|
333
347
|
|
|
348
|
+
setSoundEnabled: (enabled) => {
|
|
349
|
+
saveBooleanToStorage(STORAGE_KEYS.soundEnabled, enabled);
|
|
350
|
+
set({ soundEnabled: enabled });
|
|
351
|
+
},
|
|
352
|
+
toggleSoundEnabled: () => {
|
|
353
|
+
set((state) => {
|
|
354
|
+
const newValue = !state.soundEnabled;
|
|
355
|
+
saveBooleanToStorage(STORAGE_KEYS.soundEnabled, newValue);
|
|
356
|
+
return { soundEnabled: newValue };
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
|
|
334
360
|
setRenamingNodeId: (nodeId) => {
|
|
335
361
|
set({ renamingNodeId: nodeId });
|
|
336
362
|
},
|
|
@@ -3,14 +3,26 @@
|
|
|
3
3
|
* Dashboard. Split into named groups so a new status visual or keyframe
|
|
4
4
|
* can be added without touching Dashboard.tsx.
|
|
5
5
|
*
|
|
6
|
-
* KEYFRAMES -- @keyframes definitions
|
|
6
|
+
* KEYFRAMES -- @keyframes definitions for edges
|
|
7
7
|
* edgeStatusStyles(...) -- .react-flow__edge.{selected,executing,...}
|
|
8
|
-
* nodeStatusStyles(...) -- .react-flow__node.{
|
|
8
|
+
* nodeStatusStyles(...) -- .react-flow__node.{...} (status-class colors only)
|
|
9
9
|
* buildCanvasStyles(...) -- composes the three for Dashboard
|
|
10
10
|
*
|
|
11
11
|
* Per-node inline animations (border pulse, etc.) live in their own
|
|
12
12
|
* components and read theme tokens directly; this module is for
|
|
13
13
|
* canvas-wide rules that need to match React Flow's wrapper classes.
|
|
14
|
+
*
|
|
15
|
+
* Node execution glow is owned by `client/src/themes/base.css` — see
|
|
16
|
+
* the `node-pulse` keyframe + `.react-flow__node.executing .node` /
|
|
17
|
+
* `.sq-node[data-executing] .sq-node-box` rules there. This file used
|
|
18
|
+
* to inject a competing `nodeGlow` keyframe targeting the React Flow
|
|
19
|
+
* wrapper; that was dead code (only the inner `.node` child animated)
|
|
20
|
+
* and has been removed in favour of base.css as the single source of
|
|
21
|
+
* truth.
|
|
22
|
+
*
|
|
23
|
+
* The light vs dark distinction is encoded entirely in `colors` (the
|
|
24
|
+
* theme object provides different values per mode), so this file knows
|
|
25
|
+
* nothing about which theme is active.
|
|
14
26
|
*/
|
|
15
27
|
|
|
16
28
|
export interface CanvasStatusColors {
|
|
@@ -19,6 +31,9 @@ export interface CanvasStatusColors {
|
|
|
19
31
|
edgeExecuting: string;
|
|
20
32
|
edgeCompleted: string;
|
|
21
33
|
edgeError: string;
|
|
34
|
+
edgePending: string;
|
|
35
|
+
edgeMemoryActive: string;
|
|
36
|
+
edgeToolActive: string;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
const KEYFRAMES = `
|
|
@@ -26,19 +41,9 @@ const KEYFRAMES = `
|
|
|
26
41
|
0% { stroke-dashoffset: 24; }
|
|
27
42
|
100% { stroke-dashoffset: 0; }
|
|
28
43
|
}
|
|
29
|
-
|
|
30
|
-
@keyframes nodeGlowDark {
|
|
31
|
-
0%, 100% { filter: drop-shadow(0 0 8px var(--node-glow)) drop-shadow(0 0 16px var(--node-glow-soft)); }
|
|
32
|
-
50% { filter: drop-shadow(0 0 14px var(--node-glow)) drop-shadow(0 0 24px var(--node-glow)); }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@keyframes nodeGlowLight {
|
|
36
|
-
0%, 100% { filter: drop-shadow(0 0 10px rgba(37, 99, 235, 0.8)) drop-shadow(0 0 20px rgba(37, 99, 235, 0.6)); }
|
|
37
|
-
50% { filter: drop-shadow(0 0 16px rgba(37, 99, 235, 1)) drop-shadow(0 0 30px rgba(37, 99, 235, 0.8)); }
|
|
38
|
-
}
|
|
39
44
|
`;
|
|
40
45
|
|
|
41
|
-
function edgeStatusStyles(colors: CanvasStatusColors
|
|
46
|
+
function edgeStatusStyles(colors: CanvasStatusColors): string {
|
|
42
47
|
return `
|
|
43
48
|
.react-flow__edge path {
|
|
44
49
|
stroke: ${colors.edgeDefault} !important;
|
|
@@ -51,14 +56,14 @@ function edgeStatusStyles(colors: CanvasStatusColors, isDark: boolean): string {
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
.react-flow__edge.executing path {
|
|
54
|
-
stroke: ${
|
|
59
|
+
stroke: ${colors.edgeExecuting} !important;
|
|
55
60
|
stroke-width: 3px !important;
|
|
56
61
|
stroke-dasharray: 8 4;
|
|
57
62
|
animation: dashFlow 0.5s linear infinite;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
.react-flow__edge.completed path {
|
|
61
|
-
stroke: ${
|
|
66
|
+
stroke: ${colors.edgeCompleted} !important;
|
|
62
67
|
stroke-width: 2px !important;
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -68,45 +73,41 @@ function edgeStatusStyles(colors: CanvasStatusColors, isDark: boolean): string {
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
.react-flow__edge.pending path {
|
|
71
|
-
stroke: ${
|
|
76
|
+
stroke: ${colors.edgePending} !important;
|
|
72
77
|
stroke-width: 2px !important;
|
|
73
78
|
stroke-dasharray: 8 4;
|
|
74
79
|
animation: dashFlow 0.5s linear infinite;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
.react-flow__edge.memory-active path {
|
|
78
|
-
stroke: ${
|
|
83
|
+
stroke: ${colors.edgeMemoryActive} !important;
|
|
79
84
|
stroke-width: 3px !important;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
.react-flow__edge.tool-active path {
|
|
83
|
-
stroke: ${
|
|
88
|
+
stroke: ${colors.edgeToolActive} !important;
|
|
84
89
|
stroke-width: 3px !important;
|
|
85
90
|
}
|
|
86
91
|
`;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
function nodeStatusStyles(
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`;
|
|
94
|
+
function nodeStatusStyles(_colors: CanvasStatusColors): string {
|
|
95
|
+
// Node execution animation is owned by base.css (`node-pulse`
|
|
96
|
+
// keyframe + `.react-flow__node.executing .node` /
|
|
97
|
+
// `.sq-node[data-executing] .sq-node-box` rules). This function is
|
|
98
|
+
// retained as a hook for future canvas-wide status-class rules on
|
|
99
|
+
// the React Flow wrapper that don't fit per-component CSS.
|
|
100
|
+
//
|
|
101
|
+
// `_colors` is intentionally unused at the moment; the parameter is
|
|
102
|
+
// kept on the signature so callers (buildCanvasStyles) and the
|
|
103
|
+
// CanvasStatusColors contract stay stable for downstream consumers.
|
|
104
|
+
return '';
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
export function buildCanvasStyles(
|
|
104
|
-
colors: CanvasStatusColors,
|
|
105
|
-
isDark: boolean,
|
|
106
|
-
): string {
|
|
107
|
+
export function buildCanvasStyles(colors: CanvasStatusColors): string {
|
|
107
108
|
return [
|
|
108
|
-
edgeStatusStyles(colors
|
|
109
|
-
nodeStatusStyles(colors
|
|
109
|
+
edgeStatusStyles(colors),
|
|
110
|
+
nodeStatusStyles(colors),
|
|
110
111
|
KEYFRAMES,
|
|
111
112
|
].join('\n');
|
|
112
113
|
}
|