@elizaos/app-core 2.0.0-alpha.10
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/LICENSE +21 -0
- package/package.json +90 -0
- package/src/App.tsx +472 -0
- package/src/actions/character.test.ts +139 -0
- package/src/actions/character.ts +152 -0
- package/src/actions/chat-helpers.ts +100 -0
- package/src/actions/cloud.ts +59 -0
- package/src/actions/index.ts +12 -0
- package/src/actions/lifecycle.ts +175 -0
- package/src/actions/onboarding.ts +46 -0
- package/src/actions/triggers.ts +190 -0
- package/src/ambient.d.ts +16 -0
- package/src/api/client.ts +5516 -0
- package/src/api/index.ts +1 -0
- package/src/autonomy/index.ts +477 -0
- package/src/bridge/capacitor-bridge.ts +295 -0
- package/src/bridge/electrobun-rpc.ts +58 -0
- package/src/bridge/electrobun-runtime.ts +28 -0
- package/src/bridge/index.ts +5 -0
- package/src/bridge/native-plugins.ts +134 -0
- package/src/bridge/plugin-bridge.ts +352 -0
- package/src/bridge/storage-bridge.ts +162 -0
- package/src/chat/index.ts +250 -0
- package/src/coding/index.ts +43 -0
- package/src/components/AdvancedPageView.tsx +362 -0
- package/src/components/AgentActivityBox.tsx +49 -0
- package/src/components/ApiKeyConfig.tsx +224 -0
- package/src/components/AppsPageView.tsx +52 -0
- package/src/components/AppsView.tsx +293 -0
- package/src/components/AvatarLoader.tsx +86 -0
- package/src/components/AvatarSelector.tsx +223 -0
- package/src/components/BscTradePanel.tsx +549 -0
- package/src/components/BugReportModal.tsx +499 -0
- package/src/components/CharacterView.tsx +1645 -0
- package/src/components/ChatAvatar.test.ts +96 -0
- package/src/components/ChatAvatar.tsx +147 -0
- package/src/components/ChatComposer.tsx +330 -0
- package/src/components/ChatMessage.tsx +448 -0
- package/src/components/ChatModalView.test.tsx +118 -0
- package/src/components/ChatModalView.tsx +125 -0
- package/src/components/ChatView.tsx +992 -0
- package/src/components/CloudSourceControls.tsx +80 -0
- package/src/components/CodingAgentSettingsSection.tsx +536 -0
- package/src/components/CommandPalette.tsx +284 -0
- package/src/components/CompanionSceneHost.tsx +497 -0
- package/src/components/CompanionShell.tsx +31 -0
- package/src/components/CompanionView.tsx +109 -0
- package/src/components/ConfigPageView.tsx +758 -0
- package/src/components/ConfigSaveFooter.tsx +41 -0
- package/src/components/ConfirmModal.tsx +379 -0
- package/src/components/ConnectionFailedBanner.tsx +91 -0
- package/src/components/ConnectorsPageView.tsx +13 -0
- package/src/components/ConversationsSidebar.tsx +279 -0
- package/src/components/CustomActionEditor.tsx +1125 -0
- package/src/components/CustomActionsPanel.tsx +288 -0
- package/src/components/CustomActionsView.tsx +322 -0
- package/src/components/DatabasePageView.tsx +55 -0
- package/src/components/DatabaseView.tsx +814 -0
- package/src/components/ElizaCloudDashboard.tsx +1696 -0
- package/src/components/EmotePicker.tsx +529 -0
- package/src/components/ErrorBoundary.tsx +76 -0
- package/src/components/FineTuningView.tsx +1077 -0
- package/src/components/GameView.tsx +552 -0
- package/src/components/GameViewOverlay.tsx +133 -0
- package/src/components/GlobalEmoteOverlay.tsx +155 -0
- package/src/components/Header.test.tsx +413 -0
- package/src/components/Header.tsx +403 -0
- package/src/components/HeartbeatsView.tsx +1003 -0
- package/src/components/InventoryView.tsx +385 -0
- package/src/components/KnowledgeView.tsx +1128 -0
- package/src/components/LanguageDropdown.tsx +188 -0
- package/src/components/LifoMonitorPanel.tsx +196 -0
- package/src/components/LifoSandboxView.tsx +499 -0
- package/src/components/LoadingScreen.tsx +77 -0
- package/src/components/LogsPageView.tsx +17 -0
- package/src/components/LogsView.tsx +239 -0
- package/src/components/MediaGalleryView.tsx +433 -0
- package/src/components/MediaSettingsSection.tsx +893 -0
- package/src/components/MessageContent.tsx +815 -0
- package/src/components/OnboardingWizard.test.tsx +107 -0
- package/src/components/OnboardingWizard.tsx +189 -0
- package/src/components/PairingView.tsx +110 -0
- package/src/components/PermissionsSection.tsx +1186 -0
- package/src/components/PluginsPageView.tsx +9 -0
- package/src/components/PluginsView.tsx +3157 -0
- package/src/components/ProviderSwitcher.tsx +908 -0
- package/src/components/RestartBanner.tsx +76 -0
- package/src/components/RuntimeView.tsx +460 -0
- package/src/components/SaveCommandModal.tsx +211 -0
- package/src/components/SecretsView.tsx +569 -0
- package/src/components/SettingsView.tsx +825 -0
- package/src/components/ShellOverlays.tsx +41 -0
- package/src/components/ShortcutsOverlay.tsx +155 -0
- package/src/components/SkillsView.tsx +1435 -0
- package/src/components/StartupFailureView.tsx +63 -0
- package/src/components/StreamView.tsx +483 -0
- package/src/components/StripeEmbeddedCheckout.tsx +155 -0
- package/src/components/SubscriptionStatus.tsx +640 -0
- package/src/components/SystemWarningBanner.tsx +71 -0
- package/src/components/ThemeToggle.tsx +100 -0
- package/src/components/TrajectoriesView.tsx +526 -0
- package/src/components/TrajectoryDetailView.tsx +426 -0
- package/src/components/TriggersView.tsx +1 -0
- package/src/components/VectorBrowserView.tsx +1633 -0
- package/src/components/VoiceConfigView.tsx +675 -0
- package/src/components/VrmStage.test.ts +219 -0
- package/src/components/VrmStage.tsx +432 -0
- package/src/components/WhatsAppQrOverlay.tsx +230 -0
- package/src/components/__tests__/chainConfig.test.ts +220 -0
- package/src/components/apps/AppDetailPane.tsx +242 -0
- package/src/components/apps/AppsCatalogGrid.tsx +137 -0
- package/src/components/apps/extensions/HyperscapeAppDetailPanel.tsx +577 -0
- package/src/components/apps/extensions/registry.ts +16 -0
- package/src/components/apps/extensions/types.ts +9 -0
- package/src/components/apps/helpers.ts +44 -0
- package/src/components/avatar/VrmAnimationLoader.test.ts +164 -0
- package/src/components/avatar/VrmAnimationLoader.ts +151 -0
- package/src/components/avatar/VrmBlinkController.ts +118 -0
- package/src/components/avatar/VrmCameraManager.ts +407 -0
- package/src/components/avatar/VrmEngine.ts +2678 -0
- package/src/components/avatar/VrmFootShadow.ts +96 -0
- package/src/components/avatar/VrmViewer.tsx +421 -0
- package/src/components/avatar/__tests__/VrmCameraManager.test.ts +168 -0
- package/src/components/avatar/__tests__/VrmEngine.test.ts +1574 -0
- package/src/components/avatar/mixamoVRMRigMap.ts +62 -0
- package/src/components/avatar/retargetMixamoFbxToVrm.ts +144 -0
- package/src/components/avatar/retargetMixamoGltfToVrm.ts +119 -0
- package/src/components/chainConfig.ts +380 -0
- package/src/components/companion/CompanionHeader.tsx +47 -0
- package/src/components/companion/CompanionSceneHost.tsx +1 -0
- package/src/components/companion/VrmStage.tsx +2 -0
- package/src/components/companion/__tests__/walletUtils.test.ts +742 -0
- package/src/components/companion/walletUtils.ts +290 -0
- package/src/components/companion-shell-styles.test.ts +142 -0
- package/src/components/companion-shell-styles.ts +270 -0
- package/src/components/confirm-delete-control.tsx +69 -0
- package/src/components/conversations/ConversationListItem.tsx +185 -0
- package/src/components/conversations/conversation-utils.ts +151 -0
- package/src/components/format.ts +131 -0
- package/src/components/index.ts +94 -0
- package/src/components/inventory/CopyableAddress.tsx +41 -0
- package/src/components/inventory/InventoryToolbar.tsx +142 -0
- package/src/components/inventory/NftGrid.tsx +99 -0
- package/src/components/inventory/TokenLogo.tsx +71 -0
- package/src/components/inventory/TokensTable.tsx +216 -0
- package/src/components/inventory/constants.ts +170 -0
- package/src/components/inventory/index.ts +29 -0
- package/src/components/inventory/media-url.test.ts +38 -0
- package/src/components/inventory/media-url.ts +36 -0
- package/src/components/inventory/useInventoryData.ts +460 -0
- package/src/components/knowledge-upload-image.ts +215 -0
- package/src/components/labels.ts +46 -0
- package/src/components/onboarding/ActivateStep.tsx +30 -0
- package/src/components/onboarding/ConnectionStep.tsx +1530 -0
- package/src/components/onboarding/IdentityStep.tsx +147 -0
- package/src/components/onboarding/OnboardingPanel.tsx +39 -0
- package/src/components/onboarding/OnboardingStepNav.tsx +31 -0
- package/src/components/onboarding/PermissionsStep.tsx +20 -0
- package/src/components/onboarding/RpcStep.tsx +402 -0
- package/src/components/onboarding/WakeUpStep.tsx +184 -0
- package/src/components/permissions/PermissionIcon.tsx +25 -0
- package/src/components/permissions/StreamingPermissions.tsx +413 -0
- package/src/components/plugins/showcase-data.ts +481 -0
- package/src/components/shared/ShellHeaderControls.tsx +193 -0
- package/src/components/shared-companion-scene-context.ts +15 -0
- package/src/components/skeletons.tsx +88 -0
- package/src/components/stream/ActivityFeed.tsx +113 -0
- package/src/components/stream/AvatarPip.tsx +10 -0
- package/src/components/stream/ChatContent.tsx +126 -0
- package/src/components/stream/ChatTicker.tsx +55 -0
- package/src/components/stream/IdleContent.tsx +73 -0
- package/src/components/stream/StatusBar.tsx +469 -0
- package/src/components/stream/StreamSettings.tsx +506 -0
- package/src/components/stream/StreamTerminal.tsx +94 -0
- package/src/components/stream/StreamVoiceConfig.tsx +160 -0
- package/src/components/stream/helpers.ts +134 -0
- package/src/components/stream/overlays/OverlayLayer.tsx +75 -0
- package/src/components/stream/overlays/built-in/ActionTickerWidget.tsx +64 -0
- package/src/components/stream/overlays/built-in/AlertPopupWidget.tsx +87 -0
- package/src/components/stream/overlays/built-in/BrandingWidget.tsx +51 -0
- package/src/components/stream/overlays/built-in/CustomHtmlWidget.tsx +105 -0
- package/src/components/stream/overlays/built-in/PeonGlassWidget.tsx +265 -0
- package/src/components/stream/overlays/built-in/PeonHudWidget.tsx +247 -0
- package/src/components/stream/overlays/built-in/PeonSakuraWidget.tsx +278 -0
- package/src/components/stream/overlays/built-in/ThoughtBubbleWidget.tsx +77 -0
- package/src/components/stream/overlays/built-in/ViewerCountWidget.tsx +46 -0
- package/src/components/stream/overlays/built-in/index.ts +13 -0
- package/src/components/stream/overlays/registry.ts +22 -0
- package/src/components/stream/overlays/types.ts +90 -0
- package/src/components/stream/overlays/useOverlayLayout.ts +218 -0
- package/src/components/trajectory-format.ts +50 -0
- package/src/components/ui-badges.tsx +109 -0
- package/src/components/ui-switch.tsx +57 -0
- package/src/components/vector-browser-three.ts +27 -0
- package/src/config/config-catalog.ts +1092 -0
- package/src/config/config-field.tsx +1901 -0
- package/src/config/config-renderer.tsx +730 -0
- package/src/config/index.ts +11 -0
- package/src/config/ui-renderer.tsx +1751 -0
- package/src/config/ui-spec.ts +256 -0
- package/src/events/index.ts +89 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/useBugReport.tsx +43 -0
- package/src/hooks/useCanvasWindow.ts +372 -0
- package/src/hooks/useChatAvatarVoice.ts +111 -0
- package/src/hooks/useContextMenu.ts +127 -0
- package/src/hooks/useKeyboardShortcuts.ts +86 -0
- package/src/hooks/useLifoSync.ts +143 -0
- package/src/hooks/useMemoryMonitor.ts +334 -0
- package/src/hooks/useRenderGuard.ts +43 -0
- package/src/hooks/useRetakeCapture.ts +67 -0
- package/src/hooks/useStreamPopoutNavigation.ts +27 -0
- package/src/hooks/useTimeout.ts +37 -0
- package/src/hooks/useVoiceChat.ts +1441 -0
- package/src/hooks/useWhatsAppPairing.ts +123 -0
- package/src/i18n/index.ts +76 -0
- package/src/i18n/locales/en.json +1194 -0
- package/src/i18n/locales/es.json +1194 -0
- package/src/i18n/locales/ko.json +1194 -0
- package/src/i18n/locales/pt.json +1194 -0
- package/src/i18n/locales/zh-CN.json +1194 -0
- package/src/i18n/messages.ts +21 -0
- package/src/index.ts +6 -0
- package/src/navigation/index.ts +282 -0
- package/src/navigation.test.ts +189 -0
- package/src/onboarding-config.test.ts +104 -0
- package/src/onboarding-config.ts +114 -0
- package/src/platform/browser-launch.test.ts +94 -0
- package/src/platform/browser-launch.ts +149 -0
- package/src/platform/index.ts +58 -0
- package/src/platform/init.ts +236 -0
- package/src/platform/lifo.ts +215 -0
- package/src/providers/index.ts +99 -0
- package/src/state/AppContext.tsx +5846 -0
- package/src/state/index.ts +6 -0
- package/src/state/internal.ts +86 -0
- package/src/state/onboarding-resume.test.ts +135 -0
- package/src/state/onboarding-resume.ts +263 -0
- package/src/state/parsers.test.ts +124 -0
- package/src/state/parsers.ts +308 -0
- package/src/state/persistence.ts +321 -0
- package/src/state/shell-routing.ts +32 -0
- package/src/state/types.ts +701 -0
- package/src/state/ui-preferences.ts +3 -0
- package/src/state/useApp.ts +23 -0
- package/src/state/vrm.ts +76 -0
- package/src/stories/AppMockProvider.tsx +32 -0
- package/src/stories/ChatEmptyState.stories.tsx +27 -0
- package/src/stories/ChatMessage.stories.tsx +115 -0
- package/src/stories/CompanionHeader.stories.tsx +74 -0
- package/src/stories/CompanionView.stories.tsx +33 -0
- package/src/stories/ConversationListItem.stories.tsx +102 -0
- package/src/stories/TypingIndicator.stories.tsx +28 -0
- package/src/styles/anime.css +6324 -0
- package/src/styles/base.css +196 -0
- package/src/styles/onboarding-game.css +738 -0
- package/src/styles/styles.css +2087 -0
- package/src/styles/xterm.css +241 -0
- package/src/types/index.ts +715 -0
- package/src/types/react-test-renderer.d.ts +45 -0
- package/src/utils/asset-url.ts +110 -0
- package/src/utils/assistant-text.ts +172 -0
- package/src/utils/clipboard.ts +41 -0
- package/src/utils/desktop-dialogs.ts +80 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/number-parsing.ts +125 -0
- package/src/utils/openExternalUrl.ts +20 -0
- package/src/utils/spoken-text.ts +65 -0
- package/src/utils/streaming-text.ts +120 -0
- package/src/voice/index.ts +1 -0
- package/src/voice/types.ts +197 -0
- package/src/wallet-rpc.ts +176 -0
- package/test/app/AppContext.pty-sessions.test.tsx +143 -0
- package/test/app/MessageContent.test.tsx +326 -0
- package/test/app/PermissionsOnboarding.test.tsx +356 -0
- package/test/app/PermissionsSection.test.tsx +573 -0
- package/test/app/advanced-trajectory-fine-tuning.e2e.test.ts +393 -0
- package/test/app/agent-activity-box.test.tsx +132 -0
- package/test/app/agent-transfer-lock.test.ts +274 -0
- package/test/app/api-client-electron-fallback.test.ts +139 -0
- package/test/app/api-client-timeout.test.ts +75 -0
- package/test/app/api-client-ws.test.ts +98 -0
- package/test/app/api-client.ws-max-reconnect.test.ts +139 -0
- package/test/app/api-client.ws-reconnect.test.ts +157 -0
- package/test/app/app-context-autonomy-events.test.ts +478 -0
- package/test/app/apps-page-view.test.ts +114 -0
- package/test/app/apps-view.test.ts +769 -0
- package/test/app/autonomous-workflows.e2e.test.ts +765 -0
- package/test/app/autonomy-events.test.ts +150 -0
- package/test/app/avatar-selector.test.tsx +52 -0
- package/test/app/bsc-trade-panel.test.tsx +134 -0
- package/test/app/bug-report-modal.test.tsx +353 -0
- package/test/app/character-customization.e2e.test.ts +1199 -0
- package/test/app/chat-advanced-features.e2e.test.ts +706 -0
- package/test/app/chat-composer.test.tsx +181 -0
- package/test/app/chat-language-header.test.ts +64 -0
- package/test/app/chat-message.test.tsx +222 -0
- package/test/app/chat-modal-view.test.tsx +191 -0
- package/test/app/chat-routine-filter.test.ts +96 -0
- package/test/app/chat-send-lock.test.ts +1465 -0
- package/test/app/chat-stream-api-client.test.tsx +390 -0
- package/test/app/chat-view-game-modal.test.tsx +661 -0
- package/test/app/chat-view.test.tsx +877 -0
- package/test/app/cloud-api.e2e.test.ts +258 -0
- package/test/app/cloud-login-flow.e2e.test.ts +494 -0
- package/test/app/cloud-login-lock.test.ts +411 -0
- package/test/app/command-palette.test.tsx +184 -0
- package/test/app/command-registry.test.ts +75 -0
- package/test/app/companion-greeting-wave.test.tsx +425 -0
- package/test/app/companion-stale-conversation.test.tsx +447 -0
- package/test/app/companion-view.test.tsx +686 -0
- package/test/app/confirm-delete-control.test.ts +79 -0
- package/test/app/confirm-modal.test.tsx +219 -0
- package/test/app/connectors-ui.e2e.test.ts +508 -0
- package/test/app/conversations-sidebar-game-modal.test.tsx +260 -0
- package/test/app/conversations-sidebar.test.tsx +160 -0
- package/test/app/custom-actions-smoke.test.ts +387 -0
- package/test/app/custom-avatar-api-client.test.ts +207 -0
- package/test/app/desktop-utils.test.ts +145 -0
- package/test/app/electrobun-rpc-bridge.test.ts +83 -0
- package/test/app/events.test.ts +88 -0
- package/test/app/export-import-flows.e2e.test.ts +700 -0
- package/test/app/fine-tuning-view.test.ts +471 -0
- package/test/app/game-view-auth-session.test.tsx +186 -0
- package/test/app/game-view.test.ts +444 -0
- package/test/app/global-emote-overlay.test.tsx +106 -0
- package/test/app/header-status.test.tsx +149 -0
- package/test/app/i18n.test.ts +152 -0
- package/test/app/inventory-bsc-view.test.ts +940 -0
- package/test/app/knowledge-ui.e2e.test.ts +762 -0
- package/test/app/knowledge-upload-helpers.test.ts +124 -0
- package/test/app/lifecycle-lock.test.ts +267 -0
- package/test/app/lifo-popout-utils.test.ts +208 -0
- package/test/app/lifo-safe-endpoint.test.ts +34 -0
- package/test/app/loading-screen.test.tsx +45 -0
- package/test/app/memory-monitor.test.ts +332 -0
- package/test/app/navigation.test.tsx +22 -0
- package/test/app/onboarding-finish-lock.test.ts +663 -0
- package/test/app/onboarding-language.test.tsx +160 -0
- package/test/app/onboarding-steps.test.tsx +375 -0
- package/test/app/open-external-url.test.ts +65 -0
- package/test/app/pages-navigation-smoke.e2e.test.ts +633 -0
- package/test/app/pairing-lock.test.ts +260 -0
- package/test/app/pairing-view.test.tsx +74 -0
- package/test/app/permissions-section.test.ts +432 -0
- package/test/app/plugin-bridge.test.ts +109 -0
- package/test/app/plugins-ui.e2e.test.ts +605 -0
- package/test/app/plugins-view-game-modal.test.tsx +650 -0
- package/test/app/plugins-view-toggle-restart.test.ts +129 -0
- package/test/app/provider-dropdown-default.test.tsx +302 -0
- package/test/app/restart-banner.test.tsx +197 -0
- package/test/app/retake-capture.test.ts +84 -0
- package/test/app/sandbox-api-client.test.ts +108 -0
- package/test/app/save-command-modal.test.tsx +109 -0
- package/test/app/secrets-view.test.tsx +92 -0
- package/test/app/settings-control-styles.test.tsx +142 -0
- package/test/app/settings-reset.e2e.test.ts +726 -0
- package/test/app/settings-sections.e2e.test.ts +614 -0
- package/test/app/shared-format.test.ts +44 -0
- package/test/app/shared-switch.test.ts +69 -0
- package/test/app/shell-mode-switching.e2e.test.ts +829 -0
- package/test/app/shell-mode-tab-memory.test.tsx +58 -0
- package/test/app/shell-overlays.test.tsx +50 -0
- package/test/app/shortcuts-overlay.test.tsx +111 -0
- package/test/app/sse-interruption.test.ts +122 -0
- package/test/app/startup-asset-missing.e2e.test.ts +126 -0
- package/test/app/startup-backend-missing.e2e.test.ts +118 -0
- package/test/app/startup-chat.e2e.test.ts +305 -0
- package/test/app/startup-conversation-restore.test.tsx +344 -0
- package/test/app/startup-failure-view.test.tsx +103 -0
- package/test/app/startup-onboarding.e2e.test.ts +618 -0
- package/test/app/startup-timeout.test.tsx +80 -0
- package/test/app/startup-token-401.e2e.test.ts +103 -0
- package/test/app/stream-helpers.test.ts +46 -0
- package/test/app/stream-popout-navigation.test.tsx +41 -0
- package/test/app/stream-status-bar.test.tsx +89 -0
- package/test/app/theme-toggle.test.tsx +33 -0
- package/test/app/training-api-client.test.ts +128 -0
- package/test/app/trajectories-view.test.tsx +220 -0
- package/test/app/triggers-api-client.test.ts +77 -0
- package/test/app/triggers-navigation.test.ts +113 -0
- package/test/app/triggers-view.e2e.test.ts +674 -0
- package/test/app/update-channel-lock.test.ts +259 -0
- package/test/app/vector-browser.async-cleanup.test.tsx +367 -0
- package/test/app/vector-browser.e2e.test.ts +653 -0
- package/test/app/vrm-stage.test.tsx +351 -0
- package/test/app/vrm-viewer.test.tsx +298 -0
- package/test/app/wallet-api-save-lock.test.ts +298 -0
- package/test/app/wallet-hooks.test.ts +405 -0
- package/test/app/wallet-ui-flows.e2e.test.ts +556 -0
- package/test/avatar/asset-url.test.ts +90 -0
- package/test/avatar/avatar-selector.test.ts +173 -0
- package/test/avatar/mixamo-vrm-rig-map.test.ts +111 -0
- package/test/avatar/voice-chat-streaming-text.test.ts +96 -0
- package/test/avatar/voice-chat.test.ts +391 -0
- package/test/ui/command-palette-commands.test.ts +57 -0
- package/test/ui/ui-renderer.test.ts +39 -0
- package/tsconfig.build.json +19 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigRenderer — Schema-driven plugin config form (React port).
|
|
3
|
+
*
|
|
4
|
+
* Takes a JSON Schema + ConfigUiHints, resolves each property to a field type
|
|
5
|
+
* via the catalog, and renders via the registry.
|
|
6
|
+
*
|
|
7
|
+
* Phase 2 features (json-render parity):
|
|
8
|
+
* - Rich visibility: evaluateVisibility() with LogicExpression support
|
|
9
|
+
* - Validation checks: declarative checks alongside Zod validation
|
|
10
|
+
* - Actions: onAction() callback for executing catalog actions
|
|
11
|
+
* - Prompt generation: registry.catalog.prompt() for AI system prompts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, {
|
|
15
|
+
forwardRef,
|
|
16
|
+
useCallback,
|
|
17
|
+
useImperativeHandle,
|
|
18
|
+
useMemo,
|
|
19
|
+
useState,
|
|
20
|
+
} from "react";
|
|
21
|
+
import { useApp } from "../state";
|
|
22
|
+
import type { ConfigUiHint, PluginUiTheme } from "../types";
|
|
23
|
+
import type {
|
|
24
|
+
FieldRegistry,
|
|
25
|
+
FieldRenderer,
|
|
26
|
+
FieldRenderProps,
|
|
27
|
+
JsonSchemaObject,
|
|
28
|
+
ResolvedField,
|
|
29
|
+
} from "./config-catalog";
|
|
30
|
+
import {
|
|
31
|
+
defaultCatalog,
|
|
32
|
+
defineRegistry,
|
|
33
|
+
evaluateShowIf,
|
|
34
|
+
evaluateVisibility,
|
|
35
|
+
resolveFields,
|
|
36
|
+
runValidation,
|
|
37
|
+
} from "./config-catalog";
|
|
38
|
+
import { ConfigField } from "./config-field";
|
|
39
|
+
|
|
40
|
+
// ── Props ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface ConfigRendererProps {
|
|
43
|
+
/** JSON Schema describing the config structure (type: "object"). */
|
|
44
|
+
schema: JsonSchemaObject | null;
|
|
45
|
+
/** UI rendering hints keyed by property name. */
|
|
46
|
+
hints?: Record<string, ConfigUiHint>;
|
|
47
|
+
/** Current config values keyed by property name. */
|
|
48
|
+
values?: Record<string, unknown>;
|
|
49
|
+
/** Which keys currently have values set (for status dots). */
|
|
50
|
+
setKeys?: Set<string>;
|
|
51
|
+
/** Field registry (catalog + renderers + action handlers). */
|
|
52
|
+
registry: FieldRegistry;
|
|
53
|
+
/** Plugin ID (used for revealing sensitive values via API). */
|
|
54
|
+
pluginId?: string;
|
|
55
|
+
/** Callback to reveal a sensitive field's real value. */
|
|
56
|
+
revealSecret?: (pluginId: string, key: string) => Promise<string | null>;
|
|
57
|
+
/** Callback when a field value changes. */
|
|
58
|
+
onChange?: (key: string, value: unknown) => void;
|
|
59
|
+
/** Render function for each field — receives renderProps and the resolved renderer. */
|
|
60
|
+
renderField?: (
|
|
61
|
+
renderProps: FieldRenderProps,
|
|
62
|
+
renderer: FieldRenderer,
|
|
63
|
+
) => React.ReactNode;
|
|
64
|
+
/** Show a validation error summary above the form fields when errors exist. Defaults to true. */
|
|
65
|
+
showValidationSummary?: boolean;
|
|
66
|
+
/** Partial theme overrides for plugin UI tokens. */
|
|
67
|
+
theme?: Partial<PluginUiTheme>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Handle exposed by ConfigRenderer via ref for parent-driven validation. */
|
|
71
|
+
export interface ConfigRendererHandle {
|
|
72
|
+
/** Run validation on all visible fields. Returns true if the form is valid (no errors). */
|
|
73
|
+
validateAll: () => boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Group icons ────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const GROUP_ICONS: Record<string, string> = {
|
|
79
|
+
// Auth & Security
|
|
80
|
+
auth: "\u{1F511}",
|
|
81
|
+
authentication: "\u{1F511}",
|
|
82
|
+
security: "\u{1F6E1}\uFE0F",
|
|
83
|
+
permissions: "\u{1F512}",
|
|
84
|
+
"api keys": "\u{1F511}",
|
|
85
|
+
// Connection & Network
|
|
86
|
+
connection: "\u{1F517}",
|
|
87
|
+
network: "\u{1F310}",
|
|
88
|
+
api: "\u{1F50C}",
|
|
89
|
+
webhook: "\u{1F4E1}",
|
|
90
|
+
// Models & AI
|
|
91
|
+
models: "\u{1F916}",
|
|
92
|
+
model: "\u{1F916}",
|
|
93
|
+
"ai models": "\u{1F916}",
|
|
94
|
+
"text generation": "\u{1F916}",
|
|
95
|
+
embeddings: "\u{1F9E0}",
|
|
96
|
+
// Behavior & Config
|
|
97
|
+
behavior: "\u2699\uFE0F",
|
|
98
|
+
configuration: "\u2699\uFE0F",
|
|
99
|
+
general: "\u2699\uFE0F",
|
|
100
|
+
defaults: "\u2699\uFE0F",
|
|
101
|
+
advanced: "\u{1F527}",
|
|
102
|
+
features: "\u2728",
|
|
103
|
+
// Time & Scheduling
|
|
104
|
+
timing: "\u23F1\uFE0F",
|
|
105
|
+
scheduling: "\u{1F4C5}",
|
|
106
|
+
// Storage & Data
|
|
107
|
+
storage: "\u{1F4BE}",
|
|
108
|
+
bucket: "\u{1F4E6}",
|
|
109
|
+
paths: "\u{1F4C2}",
|
|
110
|
+
output: "\u{1F4E4}",
|
|
111
|
+
repository: "\u{1F4DA}",
|
|
112
|
+
// Communication
|
|
113
|
+
messaging: "\u{1F4AC}",
|
|
114
|
+
channels: "\u{1F4E2}",
|
|
115
|
+
chatrooms: "\u{1F4AC}",
|
|
116
|
+
voice: "\u{1F3A4}",
|
|
117
|
+
speech: "\u{1F3A4}",
|
|
118
|
+
"speech-to-text": "\u{1F3A4}",
|
|
119
|
+
// Identity
|
|
120
|
+
identity: "\u{1F464}",
|
|
121
|
+
"client identity": "\u{1F464}",
|
|
122
|
+
session: "\u{1F464}",
|
|
123
|
+
// Display & Media
|
|
124
|
+
display: "\u{1F3A8}",
|
|
125
|
+
media: "\u{1F3AC}",
|
|
126
|
+
// Notifications
|
|
127
|
+
notifications: "\u{1F514}",
|
|
128
|
+
logging: "\u{1F4DD}",
|
|
129
|
+
// Finance & Trading
|
|
130
|
+
trading: "\u{1F4C8}",
|
|
131
|
+
"risk management": "\u{1F6E1}\uFE0F",
|
|
132
|
+
wallet: "\u{1F4B0}",
|
|
133
|
+
payment: "\u{1F4B3}",
|
|
134
|
+
pricing: "\u{1F4B2}",
|
|
135
|
+
// Blockchain
|
|
136
|
+
blockchain: "\u26D3\uFE0F",
|
|
137
|
+
ethereum: "\u26D3\uFE0F",
|
|
138
|
+
solana: "\u26D3\uFE0F",
|
|
139
|
+
base: "\u26D3\uFE0F",
|
|
140
|
+
arbitrum: "\u26D3\uFE0F",
|
|
141
|
+
bsc: "\u26D3\uFE0F",
|
|
142
|
+
testnets: "\u{1F9EA}",
|
|
143
|
+
"dex config": "\u{1F4CA}",
|
|
144
|
+
// Social
|
|
145
|
+
posting: "\u{1F4DD}",
|
|
146
|
+
"x/twitter authentication": "\u{1F511}",
|
|
147
|
+
"x/twitter behavior": "\u{1F426}",
|
|
148
|
+
// System
|
|
149
|
+
limits: "\u{1F4CF}",
|
|
150
|
+
providers: "\u{1F50C}",
|
|
151
|
+
commands: "\u2318",
|
|
152
|
+
actions: "\u26A1",
|
|
153
|
+
policies: "\u{1F4DC}",
|
|
154
|
+
autonomy: "\u{1F916}",
|
|
155
|
+
"background jobs": "\u{1F504}",
|
|
156
|
+
"n8n connection": "\u{1F517}",
|
|
157
|
+
app: "\u{1F4F1}",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function groupIcon(group: string): string {
|
|
161
|
+
return GROUP_ICONS[group.toLowerCase()] ?? "\u25A0";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Width → Tailwind column span ───────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function widthClass(width: "full" | "half" | "third"): string {
|
|
167
|
+
switch (width) {
|
|
168
|
+
case "half":
|
|
169
|
+
return "col-span-6 sm:col-span-3";
|
|
170
|
+
case "third":
|
|
171
|
+
return "col-span-6 sm:col-span-2";
|
|
172
|
+
default:
|
|
173
|
+
return "col-span-6";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Validation Summary ─────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
interface ValidationSummaryProps {
|
|
180
|
+
/** Map of field key to its error messages. */
|
|
181
|
+
fieldErrors: Map<string, string[]>;
|
|
182
|
+
/** Map of field key to its display label. */
|
|
183
|
+
fieldLabels: Map<string, string>;
|
|
184
|
+
/** Plugin ID for scoping field IDs. */
|
|
185
|
+
pluginId?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function ValidationSummary({
|
|
189
|
+
fieldErrors,
|
|
190
|
+
fieldLabels,
|
|
191
|
+
pluginId,
|
|
192
|
+
}: ValidationSummaryProps) {
|
|
193
|
+
const { t } = useApp();
|
|
194
|
+
const errorEntries = [...fieldErrors.entries()].filter(
|
|
195
|
+
([, errors]) => errors.length > 0,
|
|
196
|
+
);
|
|
197
|
+
const totalErrors = errorEntries.length;
|
|
198
|
+
|
|
199
|
+
if (totalErrors === 0) return null;
|
|
200
|
+
|
|
201
|
+
const handleFieldClick = (key: string) => {
|
|
202
|
+
const el = document.getElementById(
|
|
203
|
+
pluginId ? `field-${pluginId}-${key}` : `field-${key}`,
|
|
204
|
+
);
|
|
205
|
+
if (el) {
|
|
206
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div
|
|
212
|
+
className="mb-4 border border-[var(--destructive)] bg-[color-mix(in_srgb,var(--destructive)_6%,transparent)] px-4 py-3 rounded-sm"
|
|
213
|
+
role="alert"
|
|
214
|
+
>
|
|
215
|
+
<div className="text-[13px] font-semibold text-[var(--destructive)] mb-2">
|
|
216
|
+
{totalErrors} {totalErrors === 1 ? "field needs" : "fields need"}{" "}
|
|
217
|
+
{t("config-renderer.attention")}
|
|
218
|
+
</div>
|
|
219
|
+
<ul className="list-none m-0 p-0 flex flex-col gap-1">
|
|
220
|
+
{errorEntries.map(([key]) => (
|
|
221
|
+
<li key={key}>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
className="text-[12px] text-[var(--destructive)] cursor-pointer bg-transparent border-none p-0 hover:underline transition-all text-left flex items-center gap-1.5"
|
|
225
|
+
onClick={() => handleFieldClick(key)}
|
|
226
|
+
>
|
|
227
|
+
<span className="opacity-60">{t("config-renderer.Rarr")}</span>
|
|
228
|
+
<span>{fieldLabels.get(key) ?? key}</span>
|
|
229
|
+
</button>
|
|
230
|
+
</li>
|
|
231
|
+
))}
|
|
232
|
+
</ul>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Theme mapping ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/** Maps PluginUiTheme keys to CSS variable names. */
|
|
240
|
+
const THEME_TO_CSS: Record<keyof PluginUiTheme, string> = {
|
|
241
|
+
fieldGap: "--plugin-field-gap",
|
|
242
|
+
groupGap: "--plugin-group-gap",
|
|
243
|
+
sectionPadding: "--plugin-section-padding",
|
|
244
|
+
labelSize: "--plugin-label-size",
|
|
245
|
+
helpSize: "--plugin-help-size",
|
|
246
|
+
errorSize: "--plugin-error-size",
|
|
247
|
+
labelColor: "--plugin-label",
|
|
248
|
+
helpColor: "--plugin-help",
|
|
249
|
+
errorColor: "--plugin-error",
|
|
250
|
+
borderColor: "--plugin-border",
|
|
251
|
+
focusRing: "--plugin-focus-ring",
|
|
252
|
+
inputHeight: "--plugin-input-height",
|
|
253
|
+
maxFieldWidth: "--plugin-max-field-width",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// ── Component ──────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export const ConfigRenderer = forwardRef<
|
|
259
|
+
ConfigRendererHandle,
|
|
260
|
+
ConfigRendererProps
|
|
261
|
+
>(function ConfigRenderer(
|
|
262
|
+
{
|
|
263
|
+
schema,
|
|
264
|
+
hints = {},
|
|
265
|
+
values = {},
|
|
266
|
+
setKeys = new Set(),
|
|
267
|
+
registry,
|
|
268
|
+
pluginId = "",
|
|
269
|
+
revealSecret,
|
|
270
|
+
onChange,
|
|
271
|
+
renderField: renderFieldOverride,
|
|
272
|
+
showValidationSummary = true,
|
|
273
|
+
theme,
|
|
274
|
+
}: ConfigRendererProps,
|
|
275
|
+
ref,
|
|
276
|
+
) {
|
|
277
|
+
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
278
|
+
const [fieldErrors, setFieldErrors] = useState<Map<string, string[]>>(
|
|
279
|
+
new Map(),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// ── Validation pipeline (4 stages) ──────────────────────────────────
|
|
283
|
+
|
|
284
|
+
const validateField = useCallback(
|
|
285
|
+
(field: ResolvedField, value: unknown): string[] => {
|
|
286
|
+
const errors: string[] = [];
|
|
287
|
+
|
|
288
|
+
// 1. Required check
|
|
289
|
+
if (field.required && (value == null || value === "")) {
|
|
290
|
+
errors.push("This field is required.");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 2. Zod validation
|
|
294
|
+
if (value != null && value !== "") {
|
|
295
|
+
const result = registry.catalog.validate(field.fieldType, value);
|
|
296
|
+
if (!result.success) {
|
|
297
|
+
errors.push(...result.error.issues.map((i) => i.message));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 3. Pattern validation from hints
|
|
302
|
+
if (field.hint.pattern && typeof value === "string" && value) {
|
|
303
|
+
try {
|
|
304
|
+
// Guard against ReDoS: reject overly long or nested-quantifier patterns
|
|
305
|
+
const pat = field.hint.pattern;
|
|
306
|
+
if (pat.length <= 200 && !/([+*])\)?[+*]/.test(pat)) {
|
|
307
|
+
if (!new RegExp(pat).test(value)) {
|
|
308
|
+
errors.push(field.hint.patternError ?? "Invalid format.");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// invalid regex in hint — skip
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 4. Declarative validation checks (json-render style)
|
|
317
|
+
if (field.validation) {
|
|
318
|
+
const checkResult = runValidation(
|
|
319
|
+
field.validation,
|
|
320
|
+
value,
|
|
321
|
+
values,
|
|
322
|
+
registry.catalog.functions,
|
|
323
|
+
);
|
|
324
|
+
if (!checkResult.valid) {
|
|
325
|
+
errors.push(...checkResult.errors);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return errors;
|
|
330
|
+
},
|
|
331
|
+
[registry, values],
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// ── Visibility evaluation ────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
const isFieldVisible = useCallback(
|
|
337
|
+
(field: ResolvedField): boolean => {
|
|
338
|
+
// Hidden fields are never visible
|
|
339
|
+
if (field.hidden) return false;
|
|
340
|
+
|
|
341
|
+
// Rich visibility condition (json-render style) takes priority
|
|
342
|
+
if (field.visible !== undefined) {
|
|
343
|
+
return evaluateVisibility(field.visible, values);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Legacy showIf fallback
|
|
347
|
+
return evaluateShowIf(field.showIf, values);
|
|
348
|
+
},
|
|
349
|
+
[values],
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// ── Field change handler ─────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
const handleFieldChange = useCallback(
|
|
355
|
+
(field: ResolvedField, value: unknown): void => {
|
|
356
|
+
// Validate and store errors
|
|
357
|
+
const errors = validateField(field, value);
|
|
358
|
+
setFieldErrors((prev) => {
|
|
359
|
+
const next = new Map(prev);
|
|
360
|
+
if (errors.length > 0) {
|
|
361
|
+
next.set(field.key, errors);
|
|
362
|
+
} else {
|
|
363
|
+
next.delete(field.key);
|
|
364
|
+
}
|
|
365
|
+
return next;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
onChange?.(field.key, value);
|
|
369
|
+
},
|
|
370
|
+
[validateField, onChange],
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// ── Action execution ─────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
const executeAction = useCallback(
|
|
376
|
+
async (
|
|
377
|
+
action: string,
|
|
378
|
+
params?: Record<string, unknown>,
|
|
379
|
+
): Promise<unknown> => {
|
|
380
|
+
const handler = registry.resolveAction(action);
|
|
381
|
+
if (!handler) {
|
|
382
|
+
console.warn(`[config-renderer] No handler for action: ${action}`);
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
return handler(params ?? {}, values);
|
|
386
|
+
},
|
|
387
|
+
[registry, values],
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// ── Build render props for a field ───────────────────────────────────
|
|
391
|
+
|
|
392
|
+
const buildRenderProps = useCallback(
|
|
393
|
+
(field: ResolvedField): FieldRenderProps => {
|
|
394
|
+
const isSensitive = field.hint.sensitive === true;
|
|
395
|
+
return {
|
|
396
|
+
key: field.key,
|
|
397
|
+
value: values[field.key],
|
|
398
|
+
schema: field.schema,
|
|
399
|
+
hint: field.hint,
|
|
400
|
+
fieldType: field.fieldType,
|
|
401
|
+
onChange: (value: unknown) => handleFieldChange(field, value),
|
|
402
|
+
isSet: setKeys.has(field.key),
|
|
403
|
+
required: field.required,
|
|
404
|
+
errors: fieldErrors.get(field.key),
|
|
405
|
+
readonly: field.readonly,
|
|
406
|
+
onReveal:
|
|
407
|
+
isSensitive && revealSecret && pluginId
|
|
408
|
+
? () => revealSecret(pluginId, field.key)
|
|
409
|
+
: undefined,
|
|
410
|
+
onAction: (action: string, params?: Record<string, unknown>) =>
|
|
411
|
+
executeAction(action, params),
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
[
|
|
415
|
+
values,
|
|
416
|
+
setKeys,
|
|
417
|
+
fieldErrors,
|
|
418
|
+
handleFieldChange,
|
|
419
|
+
revealSecret,
|
|
420
|
+
pluginId,
|
|
421
|
+
executeAction,
|
|
422
|
+
],
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// ── Render a single field ────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
const renderField = useCallback(
|
|
428
|
+
(field: ResolvedField) => {
|
|
429
|
+
const rp = buildRenderProps(field);
|
|
430
|
+
const renderer = registry.resolveOrFallback(field.fieldType);
|
|
431
|
+
|
|
432
|
+
if (renderFieldOverride) {
|
|
433
|
+
return (
|
|
434
|
+
<div key={field.key} className={widthClass(field.width)}>
|
|
435
|
+
{renderFieldOverride(rp, renderer)}
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<div key={field.key} className={widthClass(field.width)}>
|
|
442
|
+
<ConfigField
|
|
443
|
+
renderProps={rp}
|
|
444
|
+
renderer={renderer}
|
|
445
|
+
pluginId={pluginId}
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
[buildRenderProps, registry, renderFieldOverride, pluginId],
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// ── Resolve and partition fields ─────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
const { groups, advanced, showHeaders, allVisibleFields } = useMemo(() => {
|
|
456
|
+
if (!schema)
|
|
457
|
+
return {
|
|
458
|
+
groups: new Map<string, ResolvedField[]>(),
|
|
459
|
+
advanced: [] as ResolvedField[],
|
|
460
|
+
showHeaders: false,
|
|
461
|
+
allVisibleFields: [] as ResolvedField[],
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const catalog = registry.catalog;
|
|
465
|
+
const allFields = resolveFields(schema, hints, catalog);
|
|
466
|
+
|
|
467
|
+
// Filter: hidden fields, showIf + rich visibility conditions
|
|
468
|
+
const visibleFields = allFields.filter(isFieldVisible);
|
|
469
|
+
|
|
470
|
+
const generalFields = visibleFields.filter((f) => !f.advanced);
|
|
471
|
+
const advancedFields = visibleFields.filter((f) => f.advanced);
|
|
472
|
+
|
|
473
|
+
// Group general fields, sort required-unconfigured to the top within each group
|
|
474
|
+
const fieldGroups = new Map<string, ResolvedField[]>();
|
|
475
|
+
for (const f of generalFields) {
|
|
476
|
+
const g = fieldGroups.get(f.group) ?? [];
|
|
477
|
+
g.push(f);
|
|
478
|
+
fieldGroups.set(f.group, g);
|
|
479
|
+
}
|
|
480
|
+
for (const [, fields] of fieldGroups) {
|
|
481
|
+
fields.sort((a, b) => {
|
|
482
|
+
const aEmpty =
|
|
483
|
+
a.required && (values[a.key] == null || values[a.key] === "");
|
|
484
|
+
const bEmpty =
|
|
485
|
+
b.required && (values[b.key] == null || values[b.key] === "");
|
|
486
|
+
if (aEmpty && !bEmpty) return -1;
|
|
487
|
+
if (!aEmpty && bEmpty) return 1;
|
|
488
|
+
return (a.hint.order ?? 999) - (b.hint.order ?? 999);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
groups: fieldGroups,
|
|
494
|
+
advanced: advancedFields,
|
|
495
|
+
showHeaders: fieldGroups.size > 1,
|
|
496
|
+
allVisibleFields: visibleFields,
|
|
497
|
+
};
|
|
498
|
+
}, [schema, hints, registry, isFieldVisible, values]);
|
|
499
|
+
|
|
500
|
+
// ── Field labels for validation summary ────────────────────────────
|
|
501
|
+
|
|
502
|
+
const fieldLabels = useMemo(() => {
|
|
503
|
+
const labels = new Map<string, string>();
|
|
504
|
+
for (const field of allVisibleFields) {
|
|
505
|
+
labels.set(field.key, field.hint.label ?? field.key);
|
|
506
|
+
}
|
|
507
|
+
return labels;
|
|
508
|
+
}, [allVisibleFields]);
|
|
509
|
+
|
|
510
|
+
// ── Validate all visible fields ────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
const validateAll = useCallback((): boolean => {
|
|
513
|
+
const nextErrors = new Map<string, string[]>();
|
|
514
|
+
for (const field of allVisibleFields) {
|
|
515
|
+
const errors = validateField(field, values[field.key]);
|
|
516
|
+
if (errors.length > 0) {
|
|
517
|
+
nextErrors.set(field.key, errors);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
setFieldErrors(nextErrors);
|
|
521
|
+
return nextErrors.size === 0;
|
|
522
|
+
}, [allVisibleFields, validateField, values]);
|
|
523
|
+
|
|
524
|
+
// ── Expose validateAll to parent via ref ───────────────────────────
|
|
525
|
+
|
|
526
|
+
useImperativeHandle(ref, () => ({ validateAll }), [validateAll]);
|
|
527
|
+
|
|
528
|
+
// ── Configuration progress ─────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
const configProgress = useMemo(() => {
|
|
531
|
+
const total = allVisibleFields.length;
|
|
532
|
+
if (total === 0) return null;
|
|
533
|
+
const isConfigured = (f: ResolvedField) => {
|
|
534
|
+
if (setKeys.has(f.key)) return true;
|
|
535
|
+
const v = values[f.key];
|
|
536
|
+
return v != null && v !== "";
|
|
537
|
+
};
|
|
538
|
+
const configured = allVisibleFields.filter(isConfigured).length;
|
|
539
|
+
const requiredTotal = allVisibleFields.filter((f) => f.required).length;
|
|
540
|
+
const requiredSet = allVisibleFields.filter(
|
|
541
|
+
(f) => f.required && isConfigured(f),
|
|
542
|
+
).length;
|
|
543
|
+
return { total, configured, requiredTotal, requiredSet };
|
|
544
|
+
}, [allVisibleFields, values, setKeys]);
|
|
545
|
+
|
|
546
|
+
// ── Theme style ────────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
const themeStyle = useMemo(() => {
|
|
549
|
+
if (!theme) return undefined;
|
|
550
|
+
const style: Record<string, string> = {};
|
|
551
|
+
for (const [key, value] of Object.entries(theme)) {
|
|
552
|
+
const cssVar = THEME_TO_CSS[key as keyof typeof THEME_TO_CSS];
|
|
553
|
+
if (cssVar && value) {
|
|
554
|
+
style[cssVar] = value as string;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return Object.keys(style).length > 0 ? style : undefined;
|
|
558
|
+
}, [theme]);
|
|
559
|
+
|
|
560
|
+
// ── useApp for i18n ─────────────────────────────────────────────────
|
|
561
|
+
const { t: tFn } = useApp();
|
|
562
|
+
|
|
563
|
+
// ── Empty state ──────────────────────────────────────────────────────
|
|
564
|
+
|
|
565
|
+
if (!schema) {
|
|
566
|
+
return (
|
|
567
|
+
<div className="text-xs text-[var(--muted)] italic py-3">
|
|
568
|
+
{tFn("config-renderer.NoSchemaProvided")}
|
|
569
|
+
</div>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ── Render ───────────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<div style={themeStyle}>
|
|
577
|
+
{/* Progress indicator */}
|
|
578
|
+
{configProgress &&
|
|
579
|
+
configProgress.requiredTotal > 0 &&
|
|
580
|
+
configProgress.requiredSet < configProgress.requiredTotal && (
|
|
581
|
+
<div className="mb-4 px-3.5 py-2.5 border border-[var(--warning,#f39c12)] bg-[color-mix(in_srgb,var(--warning,#f39c12)_6%,transparent)] rounded-sm">
|
|
582
|
+
<ConfigProgressText configProgress={configProgress} />
|
|
583
|
+
<div className="w-full h-1.5 bg-[var(--border)] rounded-full overflow-hidden">
|
|
584
|
+
<div
|
|
585
|
+
className="h-full bg-[var(--warning,#f39c12)] rounded-full transition-all duration-300"
|
|
586
|
+
style={{
|
|
587
|
+
width: `${(configProgress.requiredSet / configProgress.requiredTotal) * 100}%`,
|
|
588
|
+
}}
|
|
589
|
+
/>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{showValidationSummary && fieldErrors.size > 0 ? (
|
|
595
|
+
<ValidationSummary
|
|
596
|
+
fieldErrors={fieldErrors}
|
|
597
|
+
fieldLabels={fieldLabels}
|
|
598
|
+
pluginId={pluginId}
|
|
599
|
+
/>
|
|
600
|
+
) : null}
|
|
601
|
+
|
|
602
|
+
{[...groups.entries()].map(([group, fields], groupIndex) => (
|
|
603
|
+
<div key={group} className={groupIndex > 0 ? "mt-5" : ""}>
|
|
604
|
+
{showHeaders && (
|
|
605
|
+
<div className="flex items-center gap-2 mb-3">
|
|
606
|
+
<span className="text-base leading-none">{groupIcon(group)}</span>
|
|
607
|
+
<span className="text-[12px] font-bold uppercase tracking-wider text-[var(--text)] opacity-70">
|
|
608
|
+
{group}
|
|
609
|
+
</span>
|
|
610
|
+
<span className="flex-1 h-px bg-[var(--border)] ml-1" />
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
<div className="grid grid-cols-6 gap-x-5 gap-y-0">
|
|
614
|
+
{fields.map((f) => renderField(f))}
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
))}
|
|
618
|
+
|
|
619
|
+
{advanced.length > 0 && (
|
|
620
|
+
<div className="mt-5 pt-4 border-t border-[var(--border)]">
|
|
621
|
+
<AdvancedSectionToggle
|
|
622
|
+
advanced={advanced}
|
|
623
|
+
advancedOpen={advancedOpen}
|
|
624
|
+
setAdvancedOpen={setAdvancedOpen}
|
|
625
|
+
/>
|
|
626
|
+
{advancedOpen && (
|
|
627
|
+
<div className="grid grid-cols-6 gap-x-5 gap-y-0 pt-1 animate-[cr-slide_var(--duration-normal,200ms)_ease]">
|
|
628
|
+
{advanced.map((f) => renderField(f))}
|
|
629
|
+
</div>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
)}
|
|
633
|
+
</div>
|
|
634
|
+
);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
function ConfigProgressText({
|
|
638
|
+
configProgress,
|
|
639
|
+
}: {
|
|
640
|
+
configProgress: {
|
|
641
|
+
requiredSet: number;
|
|
642
|
+
requiredTotal: number;
|
|
643
|
+
configured: number;
|
|
644
|
+
total: number;
|
|
645
|
+
};
|
|
646
|
+
}) {
|
|
647
|
+
const { t } = useApp();
|
|
648
|
+
return (
|
|
649
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
650
|
+
<span className="text-[12px] font-semibold text-[var(--warning,#f39c12)]">
|
|
651
|
+
{configProgress.requiredSet}/{configProgress.requiredTotal}{" "}
|
|
652
|
+
{t("config-renderer.requiredFieldsConf")}
|
|
653
|
+
</span>
|
|
654
|
+
<span className="text-[11px] text-[var(--muted)]">
|
|
655
|
+
{configProgress.configured}/{configProgress.total}{" "}
|
|
656
|
+
{t("config-renderer.total")}
|
|
657
|
+
</span>
|
|
658
|
+
</div>
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function AdvancedSectionToggle({
|
|
663
|
+
advanced,
|
|
664
|
+
advancedOpen,
|
|
665
|
+
setAdvancedOpen,
|
|
666
|
+
}: {
|
|
667
|
+
advanced: ResolvedField[];
|
|
668
|
+
advancedOpen: boolean;
|
|
669
|
+
setAdvancedOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
670
|
+
}) {
|
|
671
|
+
const { t } = useApp();
|
|
672
|
+
return (
|
|
673
|
+
<button
|
|
674
|
+
type="button"
|
|
675
|
+
className="flex items-center gap-2 cursor-pointer select-none group mb-3"
|
|
676
|
+
onClick={() => setAdvancedOpen((prev) => !prev)}
|
|
677
|
+
>
|
|
678
|
+
<span
|
|
679
|
+
className="inline-block text-[10px] text-[var(--muted)] transition-transform duration-200 group-hover:text-[var(--text)]"
|
|
680
|
+
style={{ transform: advancedOpen ? "rotate(90deg)" : "none" }}
|
|
681
|
+
>
|
|
682
|
+
▶
|
|
683
|
+
</span>
|
|
684
|
+
<span className="text-[12px] font-bold uppercase tracking-wider text-[var(--muted)] group-hover:text-[var(--text)] transition-colors">
|
|
685
|
+
{t("config-renderer.Advanced")}
|
|
686
|
+
</span>
|
|
687
|
+
<span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1.5 text-[10px] font-bold bg-[var(--accent-subtle,rgba(255,255,255,0.05))] text-[var(--accent)] border border-[var(--border)] rounded-sm">
|
|
688
|
+
{advanced.length}
|
|
689
|
+
</span>
|
|
690
|
+
<span className="flex-1 h-px bg-[var(--border)] opacity-50 ml-1" />
|
|
691
|
+
</button>
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── Default registry ───────────────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
// Import actual field renderers
|
|
698
|
+
import { defaultRenderers } from "./config-field";
|
|
699
|
+
|
|
700
|
+
/** The default registry wiring defaultCatalog → defaultRenderers. */
|
|
701
|
+
export const defaultRegistry = defineRegistry(defaultCatalog, defaultRenderers);
|
|
702
|
+
|
|
703
|
+
// ── useConfigValidation hook ────────────────────────────────────────────
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Convenience hook that creates a ref for ConfigRenderer and exposes
|
|
707
|
+
* a `validateAll()` function the parent can call before submitting.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```tsx
|
|
711
|
+
* const { configRef, validateAll } = useConfigValidation();
|
|
712
|
+
*
|
|
713
|
+
* const handleSave = () => {
|
|
714
|
+
* if (!validateAll()) return; // form has errors
|
|
715
|
+
* // proceed with save
|
|
716
|
+
* };
|
|
717
|
+
*
|
|
718
|
+
* return <ConfigRenderer ref={configRef} ... />;
|
|
719
|
+
* ```
|
|
720
|
+
*/
|
|
721
|
+
export function useConfigValidation() {
|
|
722
|
+
const configRef = React.useRef<ConfigRendererHandle>(null);
|
|
723
|
+
|
|
724
|
+
const validateAll = useCallback((): boolean => {
|
|
725
|
+
if (!configRef.current) return true;
|
|
726
|
+
return configRef.current.validateAll();
|
|
727
|
+
}, []);
|
|
728
|
+
|
|
729
|
+
return { configRef, validateAll };
|
|
730
|
+
}
|