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
|
@@ -17,25 +17,32 @@
|
|
|
17
17
|
import * as React from 'react';
|
|
18
18
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
19
19
|
import { cn } from '@/lib/utils';
|
|
20
|
+
import { Sounds } from '@/lib/sound';
|
|
20
21
|
|
|
21
22
|
export const actionButtonVariants = cva(
|
|
22
23
|
// Base: 32px tall pill with icon-text gap, semibold, focus ring, smooth hover.
|
|
23
|
-
|
|
24
|
+
// `action-btn` + `btn` co-classes are the design-handoff structural
|
|
25
|
+
// hooks for per-theme decorations (gold-foil on Renaissance, neon
|
|
26
|
+
// outline on Cyber, hard 4px shadow on Atomic) and the global hover
|
|
27
|
+
// sound delegate. Disabled state uses shadcn-idiomatic
|
|
28
|
+
// `disabled:opacity-50` so we don't do per-token opacity arithmetic
|
|
29
|
+
// at the call site.
|
|
30
|
+
'action-btn btn inline-flex h-8 items-center gap-1.5 rounded-md border px-3.5 text-[13px] font-semibold transition-all outline-none select-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
24
31
|
{
|
|
25
32
|
variants: {
|
|
26
33
|
intent: {
|
|
27
34
|
run:
|
|
28
|
-
'border-action-run-border bg-action-run-soft text-action-run hover:bg-action-run
|
|
35
|
+
'border-action-run-border bg-action-run-soft text-action-run hover:bg-action-run-hover',
|
|
29
36
|
stop:
|
|
30
|
-
'border-action-stop-border bg-action-stop-soft text-action-stop hover:bg-action-stop
|
|
37
|
+
'border-action-stop-border bg-action-stop-soft text-action-stop hover:bg-action-stop-hover',
|
|
31
38
|
save:
|
|
32
|
-
'border-action-save-border bg-action-save-soft text-action-save hover:bg-action-save
|
|
39
|
+
'border-action-save-border bg-action-save-soft text-action-save hover:bg-action-save-hover',
|
|
33
40
|
config:
|
|
34
|
-
'border-action-config-border bg-action-config-soft text-action-config hover:bg-action-config
|
|
41
|
+
'border-action-config-border bg-action-config-soft text-action-config hover:bg-action-config-hover',
|
|
35
42
|
secret:
|
|
36
|
-
'border-action-secret-border bg-action-secret-soft text-action-secret hover:bg-action-secret
|
|
43
|
+
'border-action-secret-border bg-action-secret-soft text-action-secret hover:bg-action-secret-hover',
|
|
37
44
|
tools:
|
|
38
|
-
'border-action-tools-border bg-action-tools-soft text-action-tools hover:bg-action-tools
|
|
45
|
+
'border-action-tools-border bg-action-tools-soft text-action-tools hover:bg-action-tools-hover',
|
|
39
46
|
},
|
|
40
47
|
},
|
|
41
48
|
defaultVariants: { intent: 'save' },
|
|
@@ -49,13 +56,31 @@ export interface ActionButtonProps
|
|
|
49
56
|
VariantProps<typeof actionButtonVariants> {}
|
|
50
57
|
|
|
51
58
|
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
|
52
|
-
({ className, intent, ...props }, ref) =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
({ className, intent, onClick, ...props }, ref) => {
|
|
60
|
+
// Fire the per-theme `click` sound BEFORE the user-supplied
|
|
61
|
+
// handler so the audio cue doesn't depend on the action
|
|
62
|
+
// succeeding (e.g., even a disabled-late workflow run still
|
|
63
|
+
// gives feedback). Sounds.play() is a no-op when the engine is
|
|
64
|
+
// disabled or the active pack is `none`, so this costs nothing
|
|
65
|
+
// in the default state.
|
|
66
|
+
const handleClick = onClick
|
|
67
|
+
? (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
68
|
+
Sounds.play('click');
|
|
69
|
+
onClick(event);
|
|
70
|
+
}
|
|
71
|
+
: (_event: React.MouseEvent<HTMLButtonElement>) => {
|
|
72
|
+
Sounds.play('click');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
ref={ref}
|
|
78
|
+
type={props.type ?? 'button'}
|
|
79
|
+
className={cn(actionButtonVariants({ intent }), className)}
|
|
80
|
+
onClick={handleClick}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
},
|
|
60
85
|
);
|
|
61
86
|
ActionButton.displayName = 'ActionButton';
|
|
@@ -3,9 +3,16 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
3
3
|
import { Slot } from "radix-ui"
|
|
4
4
|
|
|
5
5
|
import { cn } from "@/lib/utils"
|
|
6
|
+
import { Sounds } from "@/lib/sound"
|
|
6
7
|
|
|
8
|
+
// `btn` co-class is the design-handoff structural hook — every shadcn
|
|
9
|
+
// Button gets it so per-theme CSS rules (`.btn`, `.btn-primary`, etc.)
|
|
10
|
+
// activate (gold-foil on Renaissance, neon outline on Cyber, riveted
|
|
11
|
+
// brass on Steampunk, boomerang shadow on Atomic, etc.). It also
|
|
12
|
+
// powers the global hover-sound delegate which selects on `.btn, .row,
|
|
13
|
+
// .action-btn, .menu-pop-item, .wf-card, .comp, .cmdk-item`.
|
|
7
14
|
const buttonVariants = cva(
|
|
8
|
-
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
15
|
+
"btn group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
16
|
{
|
|
10
17
|
variants: {
|
|
11
18
|
variant: {
|
|
@@ -46,6 +53,7 @@ function Button({
|
|
|
46
53
|
variant = "default",
|
|
47
54
|
size = "default",
|
|
48
55
|
asChild = false,
|
|
56
|
+
onClick,
|
|
49
57
|
...props
|
|
50
58
|
}: React.ComponentProps<"button"> &
|
|
51
59
|
VariantProps<typeof buttonVariants> & {
|
|
@@ -53,12 +61,27 @@ function Button({
|
|
|
53
61
|
}) {
|
|
54
62
|
const Comp = asChild ? Slot.Root : "button"
|
|
55
63
|
|
|
64
|
+
// Fire the per-theme `click` sound BEFORE the user-supplied handler
|
|
65
|
+
// so audio feedback is instant — mirrors the ActionButton pattern.
|
|
66
|
+
// No-op when the engine is disabled or the active pack is `none`.
|
|
67
|
+
// Disabled buttons never reach here (the `disabled` prop filters
|
|
68
|
+
// pointer events at the DOM level).
|
|
69
|
+
const handleClick = onClick
|
|
70
|
+
? (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
71
|
+
Sounds.play('click');
|
|
72
|
+
onClick(event);
|
|
73
|
+
}
|
|
74
|
+
: (_event: React.MouseEvent<HTMLButtonElement>) => {
|
|
75
|
+
Sounds.play('click');
|
|
76
|
+
};
|
|
77
|
+
|
|
56
78
|
return (
|
|
57
79
|
<Comp
|
|
58
80
|
data-slot="button"
|
|
59
81
|
data-variant={variant}
|
|
60
82
|
data-size={size}
|
|
61
83
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
84
|
+
onClick={handleClick}
|
|
62
85
|
{...props}
|
|
63
86
|
/>
|
|
64
87
|
)
|
|
@@ -4,6 +4,7 @@ import * as React from "react"
|
|
|
4
4
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
|
5
5
|
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
|
+
import { Sounds } from "@/lib/sound"
|
|
7
8
|
import { CheckIcon, ChevronRightIcon } from "lucide-react"
|
|
8
9
|
|
|
9
10
|
function DropdownMenu({
|
|
@@ -43,7 +44,11 @@ function DropdownMenuContent({
|
|
|
43
44
|
data-slot="dropdown-menu-content"
|
|
44
45
|
sideOffset={sideOffset}
|
|
45
46
|
align={align}
|
|
46
|
-
|
|
47
|
+
// `menu-pop` co-class is the design-handoff structural hook
|
|
48
|
+
// for per-theme dropdown decorations (parchment + gilded
|
|
49
|
+
// border on Renaissance, neon outline + > prefix on Cyber,
|
|
50
|
+
// etc.).
|
|
51
|
+
className={cn("menu-pop z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className )}
|
|
47
52
|
{...props}
|
|
48
53
|
/>
|
|
49
54
|
</DropdownMenuPrimitive.Portal>
|
|
@@ -62,18 +67,35 @@ function DropdownMenuItem({
|
|
|
62
67
|
className,
|
|
63
68
|
inset,
|
|
64
69
|
variant = "default",
|
|
70
|
+
onSelect,
|
|
65
71
|
...props
|
|
66
72
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
67
73
|
inset?: boolean
|
|
68
74
|
variant?: "default" | "destructive"
|
|
69
75
|
}) {
|
|
76
|
+
// Fire the per-theme `click` sound when the item is selected (Radix
|
|
77
|
+
// calls onSelect for both pointer + keyboard activation, which
|
|
78
|
+
// matches the upstream's `.menu-pop-item` click trigger).
|
|
79
|
+
const handleSelect = onSelect
|
|
80
|
+
? (event: Event) => {
|
|
81
|
+
Sounds.play('click');
|
|
82
|
+
onSelect(event);
|
|
83
|
+
}
|
|
84
|
+
: (_event: Event) => {
|
|
85
|
+
Sounds.play('click');
|
|
86
|
+
};
|
|
87
|
+
|
|
70
88
|
return (
|
|
71
89
|
<DropdownMenuPrimitive.Item
|
|
72
90
|
data-slot="dropdown-menu-item"
|
|
73
91
|
data-inset={inset}
|
|
74
92
|
data-variant={variant}
|
|
93
|
+
onSelect={handleSelect}
|
|
94
|
+
// `menu-pop-item` co-class powers per-theme item decorations
|
|
95
|
+
// (Renaissance ink-spread hover + ✦ marker; Cyber `> ` prefix +
|
|
96
|
+
// neon glow; etc.) and the W18 hover-sound delegate.
|
|
75
97
|
className={cn(
|
|
76
|
-
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
|
98
|
+
"menu-pop-item group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
|
77
99
|
className
|
|
78
100
|
)}
|
|
79
101
|
{...props}
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
|
|
3
3
|
import { cn } from "@/lib/utils"
|
|
4
|
+
import { Sounds } from "@/lib/sound"
|
|
5
|
+
|
|
6
|
+
function Input({ className, type, onChange, ...props }: React.ComponentProps<"input">) {
|
|
7
|
+
// Fire the per-theme `type` sound on every keystroke. The engine
|
|
8
|
+
// throttles `type` internally (W19 30 ms last-fire window) so rapid
|
|
9
|
+
// typing into a long input doesn't queue dozens of OscillatorNodes.
|
|
10
|
+
// No-op when sound is disabled.
|
|
11
|
+
const handleChange = onChange
|
|
12
|
+
? (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
13
|
+
Sounds.play('type');
|
|
14
|
+
onChange(event);
|
|
15
|
+
}
|
|
16
|
+
: undefined;
|
|
4
17
|
|
|
5
|
-
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
18
|
return (
|
|
7
19
|
<input
|
|
8
20
|
type={type}
|
|
9
21
|
data-slot="input"
|
|
22
|
+
onChange={handleChange}
|
|
23
|
+
// `input` co-class is the design-handoff structural hook —
|
|
24
|
+
// per-theme CSS attaches ruled-line backgrounds (Renaissance),
|
|
25
|
+
// terminal prompt prefix (Cyber), washi underline (Edo), etc.
|
|
26
|
+
// Also targeted by the W18 `type` sound delegate.
|
|
10
27
|
className={cn(
|
|
11
|
-
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
28
|
+
"input h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
12
29
|
className
|
|
13
30
|
)}
|
|
14
31
|
{...props}
|
|
@@ -4,6 +4,7 @@ import * as React from "react"
|
|
|
4
4
|
import { Select as SelectPrimitive } from "radix-ui"
|
|
5
5
|
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
|
+
import { Sounds } from "@/lib/sound"
|
|
7
8
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
|
8
9
|
|
|
9
10
|
function Select({
|
|
@@ -106,8 +107,21 @@ function SelectLabel({
|
|
|
106
107
|
function SelectItem({
|
|
107
108
|
className,
|
|
108
109
|
children,
|
|
110
|
+
onClick,
|
|
109
111
|
...props
|
|
110
112
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
113
|
+
// Radix Select doesn't surface an `onSelect` per-item; clicks +
|
|
114
|
+
// keyboard activation both surface as DOM clicks on the Item, so we
|
|
115
|
+
// wrap onClick. Falls through to the original handler if any.
|
|
116
|
+
const handleClick = onClick
|
|
117
|
+
? (event: React.MouseEvent<HTMLDivElement>) => {
|
|
118
|
+
Sounds.play('click');
|
|
119
|
+
onClick(event);
|
|
120
|
+
}
|
|
121
|
+
: (_event: React.MouseEvent<HTMLDivElement>) => {
|
|
122
|
+
Sounds.play('click');
|
|
123
|
+
};
|
|
124
|
+
|
|
111
125
|
return (
|
|
112
126
|
<SelectPrimitive.Item
|
|
113
127
|
data-slot="select-item"
|
|
@@ -115,6 +129,7 @@ function SelectItem({
|
|
|
115
129
|
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
116
130
|
className
|
|
117
131
|
)}
|
|
132
|
+
onClick={handleClick}
|
|
118
133
|
{...props}
|
|
119
134
|
>
|
|
120
135
|
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
|
|
3
3
|
import { cn } from "@/lib/utils"
|
|
4
|
+
import { Sounds } from "@/lib/sound"
|
|
5
|
+
|
|
6
|
+
function Textarea({ className, onChange, ...props }: React.ComponentProps<"textarea">) {
|
|
7
|
+
// Fire the per-theme `type` sound on every keystroke (throttled by
|
|
8
|
+
// the W19 last-fire window inside the engine).
|
|
9
|
+
const handleChange = onChange
|
|
10
|
+
? (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
11
|
+
Sounds.play('type');
|
|
12
|
+
onChange(event);
|
|
13
|
+
}
|
|
14
|
+
: undefined;
|
|
4
15
|
|
|
5
|
-
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
16
|
return (
|
|
7
17
|
<textarea
|
|
8
18
|
data-slot="textarea"
|
|
19
|
+
onChange={handleChange}
|
|
20
|
+
// `input` co-class shares per-theme decorations + W18 type delegate
|
|
21
|
+
// with the Input primitive.
|
|
9
22
|
className={cn(
|
|
10
|
-
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
23
|
+
"input flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
11
24
|
className
|
|
12
25
|
)}
|
|
13
26
|
{...props}
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication Context for user session management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The auth-status check runs through TanStack Query (`useQuery`) so
|
|
5
|
+
* exponential backoff with full jitter, AbortController-based unmount
|
|
6
|
+
* cleanup, Strict-Mode safety, and 401/403 fast-fail are all delegated
|
|
7
|
+
* to the library — see https://tanstack.com/query/v5/docs/framework/react/guides/query-retries.
|
|
8
|
+
*
|
|
9
|
+
* The context's public surface (user, isAuthenticated, isLoading,
|
|
10
|
+
* authMode, canRegister, error, login, register, logout, checkAuth) is
|
|
11
|
+
* unchanged so consumer code does not move.
|
|
12
|
+
*
|
|
13
|
+
* Login / register / logout mutate the auth state by invalidating the
|
|
14
|
+
* `['auth', 'status']` query rather than calling a private setter — the
|
|
15
|
+
* single source of truth stays the query cache, which TanStack Query
|
|
16
|
+
* dedupes by reference equality, eliminating the spurious
|
|
17
|
+
* `isAuthenticated` flips that closed the WS prematurely under React
|
|
18
|
+
* Strict Mode.
|
|
9
19
|
*/
|
|
10
20
|
|
|
11
|
-
import React, { createContext, useContext,
|
|
21
|
+
import React, { createContext, useContext, useCallback, useMemo } from 'react';
|
|
22
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
12
23
|
import { API_CONFIG } from '../config/api';
|
|
24
|
+
import { AUTH_RETRY } from '../lib/connectionConfig';
|
|
13
25
|
|
|
14
26
|
export interface User {
|
|
15
27
|
id: number;
|
|
@@ -43,159 +55,185 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
43
55
|
|
|
44
56
|
const getApiBase = () => `${API_CONFIG.PYTHON_BASE_URL}/api/auth`;
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
const ANONYMOUS_USER: User = {
|
|
59
|
+
id: 0,
|
|
60
|
+
email: 'anonymous',
|
|
61
|
+
display_name: 'Anonymous',
|
|
62
|
+
is_owner: true,
|
|
63
|
+
};
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
// `['auth', 'status']` is the canonical key for the bootstrap query.
|
|
66
|
+
// Login / register / logout invalidate it via `queryClient.invalidateQueries`.
|
|
67
|
+
export const AUTH_STATUS_QUERY_KEY = ['auth', 'status'] as const;
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
setUser({ id: 0, email: 'anonymous', display_name: 'Anonymous', is_owner: true });
|
|
66
|
-
setIsLoading(false);
|
|
67
|
-
setError(null);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
69
|
+
/**
|
|
70
|
+
* Full-jitter exponential backoff. Constants live in
|
|
71
|
+
* `lib/connectionConfig.ts` (`AUTH_RETRY`) so a future tuning pass is a
|
|
72
|
+
* single-file edit. See that module for the rationale and the reference
|
|
73
|
+
* link to the AWS Architecture Blog.
|
|
74
|
+
*/
|
|
75
|
+
const authRetryDelay = (attemptIndex: number): number =>
|
|
76
|
+
Math.random() * Math.min(AUTH_RETRY.CAP_MS, AUTH_RETRY.BASE_MS * 2 ** attemptIndex);
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Retry on network failures + 5xx; never retry on auth errors (401/403)
|
|
80
|
+
* because those are valid responses meaning "auth disabled / not logged
|
|
81
|
+
* in", not "backend unavailable". Cap at `AUTH_RETRY.MAX_ATTEMPTS`.
|
|
82
|
+
*/
|
|
83
|
+
const authShouldRetry = (failureCount: number, error: unknown): boolean => {
|
|
84
|
+
if (failureCount >= AUTH_RETRY.MAX_ATTEMPTS) return false;
|
|
85
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
86
|
+
if (msg.includes('HTTP 401') || msg.includes('HTTP 403')) return false;
|
|
87
|
+
return true;
|
|
88
|
+
};
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
} else {
|
|
90
|
-
setUser(null);
|
|
91
|
-
setError('Failed to connect to server');
|
|
92
|
-
setIsLoading(false);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, []);
|
|
90
|
+
const fetchAuthStatus = async ({ signal }: { signal: AbortSignal }): Promise<AuthStatus> => {
|
|
91
|
+
const response = await fetch(`${getApiBase()}/status`, {
|
|
92
|
+
credentials: 'include',
|
|
93
|
+
signal,
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
// Wrap status in the error message so `authShouldRetry` can detect
|
|
97
|
+
// 401/403 without parsing the original Response.
|
|
98
|
+
throw new Error(`auth.status: HTTP ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
return response.json() as Promise<AuthStatus>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
104
|
+
const queryClient = useQueryClient();
|
|
96
105
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
// Bootstrap auth-status query. The `signal` plumbed through `queryFn`
|
|
107
|
+
// is automatically aborted when the component unmounts (Strict Mode
|
|
108
|
+
// double-mount lifecycle handled by TanStack Query, see
|
|
109
|
+
// https://tanstack.com/query/v5/docs/react/guides/cancellation).
|
|
110
|
+
const authQuery = useQuery({
|
|
111
|
+
queryKey: AUTH_STATUS_QUERY_KEY,
|
|
112
|
+
queryFn: fetchAuthStatus,
|
|
113
|
+
retry: authShouldRetry,
|
|
114
|
+
retryDelay: authRetryDelay,
|
|
115
|
+
// Boot-once: never refetch on focus / mount / network reconnect.
|
|
116
|
+
// Logout / login explicitly invalidate.
|
|
117
|
+
staleTime: Infinity,
|
|
118
|
+
refetchOnMount: false,
|
|
119
|
+
refetchOnWindowFocus: false,
|
|
120
|
+
refetchOnReconnect: false,
|
|
121
|
+
});
|
|
101
122
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
const data = authQuery.data;
|
|
124
|
+
const user: User | null = useMemo(() => {
|
|
125
|
+
if (!data) return null;
|
|
126
|
+
if (data.auth_enabled === false) return ANONYMOUS_USER;
|
|
127
|
+
return data.authenticated ? data.user : null;
|
|
128
|
+
}, [data]);
|
|
129
|
+
|
|
130
|
+
const authMode: 'single' | 'multi' = data?.auth_mode ?? 'single';
|
|
131
|
+
const canRegister = data?.can_register ?? false;
|
|
132
|
+
const isAuthenticated = user !== null;
|
|
133
|
+
const isLoading = authQuery.isPending;
|
|
134
|
+
const error = authQuery.isError ? 'Failed to connect to server' : null;
|
|
135
|
+
|
|
136
|
+
const invalidateAuth = useCallback(
|
|
137
|
+
() => queryClient.invalidateQueries({ queryKey: AUTH_STATUS_QUERY_KEY }),
|
|
138
|
+
[queryClient],
|
|
139
|
+
);
|
|
105
140
|
|
|
141
|
+
const login = useCallback(async (email: string, password: string): Promise<boolean> => {
|
|
106
142
|
try {
|
|
107
143
|
const response = await fetch(`${getApiBase()}/login`, {
|
|
108
144
|
method: 'POST',
|
|
109
145
|
headers: { 'Content-Type': 'application/json' },
|
|
110
146
|
credentials: 'include',
|
|
111
|
-
body: JSON.stringify({ email, password })
|
|
147
|
+
body: JSON.stringify({ email, password }),
|
|
112
148
|
});
|
|
149
|
+
const body = await response.json();
|
|
113
150
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
setIsLoading(false);
|
|
151
|
+
if (!response.ok || !body.success || !body.user) {
|
|
152
|
+
// Surface the server's error via the query cache so `error` flips
|
|
153
|
+
// through the same path as a normal `isError` would.
|
|
154
|
+
queryClient.setQueryData<AuthStatus | null>(AUTH_STATUS_QUERY_KEY, null);
|
|
119
155
|
return false;
|
|
120
156
|
}
|
|
121
157
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
158
|
+
// Optimistically write the new user into the cache so the UI
|
|
159
|
+
// updates this render; then invalidate so the next refetch
|
|
160
|
+
// picks up server-derived fields (auth_mode, can_register).
|
|
161
|
+
queryClient.setQueryData<AuthStatus>(AUTH_STATUS_QUERY_KEY, {
|
|
162
|
+
auth_enabled: true,
|
|
163
|
+
auth_mode: authMode,
|
|
164
|
+
authenticated: true,
|
|
165
|
+
user: body.user,
|
|
166
|
+
can_register: false,
|
|
167
|
+
});
|
|
168
|
+
await invalidateAuth();
|
|
169
|
+
return true;
|
|
131
170
|
} catch (err) {
|
|
132
171
|
console.error('Login error:', err);
|
|
133
|
-
setError('Failed to connect to server');
|
|
134
|
-
setIsLoading(false);
|
|
135
172
|
return false;
|
|
136
173
|
}
|
|
137
|
-
}, []);
|
|
174
|
+
}, [queryClient, invalidateAuth, authMode]);
|
|
138
175
|
|
|
139
176
|
const register = useCallback(async (
|
|
140
177
|
email: string,
|
|
141
178
|
password: string,
|
|
142
|
-
displayName: string
|
|
179
|
+
displayName: string,
|
|
143
180
|
): Promise<boolean> => {
|
|
144
|
-
setError(null);
|
|
145
|
-
setIsLoading(true);
|
|
146
|
-
|
|
147
181
|
try {
|
|
148
182
|
const response = await fetch(`${getApiBase()}/register`, {
|
|
149
183
|
method: 'POST',
|
|
150
184
|
headers: { 'Content-Type': 'application/json' },
|
|
151
185
|
credentials: 'include',
|
|
152
|
-
body: JSON.stringify({ email, password, display_name: displayName })
|
|
186
|
+
body: JSON.stringify({ email, password, display_name: displayName }),
|
|
153
187
|
});
|
|
188
|
+
const body = await response.json();
|
|
154
189
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (!response.ok) {
|
|
158
|
-
setError(data.detail || 'Registration failed');
|
|
159
|
-
setIsLoading(false);
|
|
190
|
+
if (!response.ok || !body.success || !body.user) {
|
|
160
191
|
return false;
|
|
161
192
|
}
|
|
162
193
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return false;
|
|
194
|
+
queryClient.setQueryData<AuthStatus>(AUTH_STATUS_QUERY_KEY, {
|
|
195
|
+
auth_enabled: true,
|
|
196
|
+
auth_mode: authMode,
|
|
197
|
+
authenticated: true,
|
|
198
|
+
user: body.user,
|
|
199
|
+
can_register: false,
|
|
200
|
+
});
|
|
201
|
+
await invalidateAuth();
|
|
202
|
+
return true;
|
|
173
203
|
} catch (err) {
|
|
174
204
|
console.error('Register error:', err);
|
|
175
|
-
setError('Failed to connect to server');
|
|
176
|
-
setIsLoading(false);
|
|
177
205
|
return false;
|
|
178
206
|
}
|
|
179
|
-
}, []);
|
|
207
|
+
}, [queryClient, invalidateAuth, authMode]);
|
|
180
208
|
|
|
181
209
|
const logout = useCallback(async () => {
|
|
182
210
|
try {
|
|
183
211
|
await fetch(`${getApiBase()}/logout`, {
|
|
184
212
|
method: 'POST',
|
|
185
|
-
credentials: 'include'
|
|
213
|
+
credentials: 'include',
|
|
186
214
|
});
|
|
187
215
|
} catch (err) {
|
|
188
216
|
console.error('Logout error:', err);
|
|
189
217
|
} finally {
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
218
|
+
// Force `authenticated: false` immediately so consumers (esp. the
|
|
219
|
+
// WebSocket logout effect) react this render; then refetch so
|
|
220
|
+
// `can_register` and `auth_mode` come back fresh.
|
|
221
|
+
queryClient.setQueryData<AuthStatus>(AUTH_STATUS_QUERY_KEY, (prev) => ({
|
|
222
|
+
...(prev ?? { auth_enabled: true, auth_mode: 'single' as const, can_register: false }),
|
|
223
|
+
authenticated: false,
|
|
224
|
+
user: null,
|
|
225
|
+
}));
|
|
226
|
+
await invalidateAuth();
|
|
193
227
|
}
|
|
194
|
-
}, [
|
|
228
|
+
}, [queryClient, invalidateAuth]);
|
|
229
|
+
|
|
230
|
+
const checkAuth = useCallback(async () => {
|
|
231
|
+
await authQuery.refetch();
|
|
232
|
+
}, [authQuery]);
|
|
195
233
|
|
|
196
|
-
const value: AuthContextType = {
|
|
234
|
+
const value: AuthContextType = useMemo(() => ({
|
|
197
235
|
user,
|
|
198
|
-
isAuthenticated
|
|
236
|
+
isAuthenticated,
|
|
199
237
|
isLoading,
|
|
200
238
|
authMode,
|
|
201
239
|
canRegister,
|
|
@@ -203,8 +241,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|
|
203
241
|
login,
|
|
204
242
|
register,
|
|
205
243
|
logout,
|
|
206
|
-
checkAuth
|
|
207
|
-
}
|
|
244
|
+
checkAuth,
|
|
245
|
+
}), [user, isAuthenticated, isLoading, authMode, canRegister, error,
|
|
246
|
+
login, register, logout, checkAuth]);
|
|
208
247
|
|
|
209
248
|
return (
|
|
210
249
|
<AuthContext.Provider value={value}>
|