machinaos 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +71 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/cli.js +159 -0
- package/client/.dockerignore +45 -0
- package/client/Dockerfile +68 -0
- package/client/eslint.config.js +29 -0
- package/client/index.html +13 -0
- package/client/nginx.conf +66 -0
- package/client/package.json +48 -0
- package/client/src/App.tsx +27 -0
- package/client/src/Dashboard.tsx +1173 -0
- package/client/src/ParameterPanel.tsx +301 -0
- package/client/src/components/AIAgentNode.tsx +321 -0
- package/client/src/components/APIKeyValidator.tsx +118 -0
- package/client/src/components/ClaudeChatModelNode.tsx +18 -0
- package/client/src/components/ConditionalEdge.tsx +189 -0
- package/client/src/components/CredentialsModal.tsx +306 -0
- package/client/src/components/EdgeConditionEditor.tsx +443 -0
- package/client/src/components/GeminiChatModelNode.tsx +18 -0
- package/client/src/components/GenericNode.tsx +357 -0
- package/client/src/components/LocationParameterPanel.tsx +154 -0
- package/client/src/components/ModelNode.tsx +286 -0
- package/client/src/components/OpenAIChatModelNode.tsx +18 -0
- package/client/src/components/OutputPanel.tsx +471 -0
- package/client/src/components/ParameterRenderer.tsx +1874 -0
- package/client/src/components/SkillEditorModal.tsx +417 -0
- package/client/src/components/SquareNode.tsx +797 -0
- package/client/src/components/StartNode.tsx +250 -0
- package/client/src/components/ToolkitNode.tsx +365 -0
- package/client/src/components/TriggerNode.tsx +463 -0
- package/client/src/components/auth/LoginPage.tsx +247 -0
- package/client/src/components/auth/ProtectedRoute.tsx +59 -0
- package/client/src/components/base/BaseChatModelNode.tsx +271 -0
- package/client/src/components/icons/AIProviderIcons.tsx +50 -0
- package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
- package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
- package/client/src/components/maps/index.ts +26 -0
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
- package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
- package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
- package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
- package/client/src/components/parameterPanel/index.ts +42 -0
- package/client/src/components/shared/DataPanel.tsx +142 -0
- package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
- package/client/src/components/ui/AIResultModal.tsx +204 -0
- package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -0
- package/client/src/components/ui/CollapsibleSection.tsx +88 -0
- package/client/src/components/ui/ComponentItem.tsx +154 -0
- package/client/src/components/ui/ComponentPalette.tsx +321 -0
- package/client/src/components/ui/ConsolePanel.tsx +1074 -0
- package/client/src/components/ui/ErrorBoundary.tsx +196 -0
- package/client/src/components/ui/InputNodesPanel.tsx +204 -0
- package/client/src/components/ui/MapSelector.tsx +314 -0
- package/client/src/components/ui/Modal.tsx +149 -0
- package/client/src/components/ui/NodeContextMenu.tsx +192 -0
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
- package/client/src/components/ui/SettingsPanel.tsx +243 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -0
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
- package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
- package/client/src/config/antdTheme.ts +186 -0
- package/client/src/config/api.ts +54 -0
- package/client/src/contexts/AuthContext.tsx +221 -0
- package/client/src/contexts/ThemeContext.tsx +42 -0
- package/client/src/contexts/WebSocketContext.tsx +1971 -0
- package/client/src/factories/baseChatModelFactory.ts +256 -0
- package/client/src/hooks/useAndroidOperations.ts +164 -0
- package/client/src/hooks/useApiKeyValidation.ts +107 -0
- package/client/src/hooks/useApiKeys.ts +238 -0
- package/client/src/hooks/useAppTheme.ts +17 -0
- package/client/src/hooks/useComponentPalette.ts +51 -0
- package/client/src/hooks/useCopyPaste.ts +155 -0
- package/client/src/hooks/useDragAndDrop.ts +124 -0
- package/client/src/hooks/useDragVariable.ts +88 -0
- package/client/src/hooks/useExecution.ts +313 -0
- package/client/src/hooks/useParameterPanel.ts +176 -0
- package/client/src/hooks/useReactFlowNodes.ts +189 -0
- package/client/src/hooks/useToolSchema.ts +209 -0
- package/client/src/hooks/useWhatsApp.ts +196 -0
- package/client/src/hooks/useWorkflowManagement.ts +46 -0
- package/client/src/index.css +315 -0
- package/client/src/main.tsx +19 -0
- package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
- package/client/src/nodeDefinitions/chatNodes.ts +135 -0
- package/client/src/nodeDefinitions/codeNodes.ts +54 -0
- package/client/src/nodeDefinitions/documentNodes.ts +379 -0
- package/client/src/nodeDefinitions/index.ts +15 -0
- package/client/src/nodeDefinitions/locationNodes.ts +463 -0
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
- package/client/src/nodeDefinitions/skillNodes.ts +211 -0
- package/client/src/nodeDefinitions/toolNodes.ts +198 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
- package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
- package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
- package/client/src/nodeDefinitions.ts +104 -0
- package/client/src/schemas/workflowSchema.ts +264 -0
- package/client/src/services/dynamicParameterService.ts +96 -0
- package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
- package/client/src/services/executionService.ts +232 -0
- package/client/src/services/workflowApi.ts +91 -0
- package/client/src/store/useAppStore.ts +582 -0
- package/client/src/styles/theme.ts +508 -0
- package/client/src/styles/zIndex.ts +17 -0
- package/client/src/types/ComponentTypes.ts +39 -0
- package/client/src/types/EdgeCondition.ts +231 -0
- package/client/src/types/INodeProperties.ts +288 -0
- package/client/src/types/NodeTypes.ts +28 -0
- package/client/src/utils/formatters.ts +33 -0
- package/client/src/utils/googleMapsLoader.ts +140 -0
- package/client/src/utils/locationUtils.ts +85 -0
- package/client/src/utils/nodeUtils.ts +31 -0
- package/client/src/utils/workflow.ts +30 -0
- package/client/src/utils/workflowExport.ts +120 -0
- package/client/src/vite-env.d.ts +12 -0
- package/client/tailwind.config.js +60 -0
- package/client/tsconfig.json +25 -0
- package/client/tsconfig.node.json +11 -0
- package/client/vite.config.js +35 -0
- package/docker-compose.prod.yml +107 -0
- package/docker-compose.yml +104 -0
- package/docs-MachinaOs/README.md +85 -0
- package/docs-MachinaOs/deployment/docker.mdx +228 -0
- package/docs-MachinaOs/deployment/production.mdx +345 -0
- package/docs-MachinaOs/docs.json +75 -0
- package/docs-MachinaOs/faq.mdx +309 -0
- package/docs-MachinaOs/favicon.svg +5 -0
- package/docs-MachinaOs/installation.mdx +160 -0
- package/docs-MachinaOs/introduction.mdx +114 -0
- package/docs-MachinaOs/logo/dark.svg +6 -0
- package/docs-MachinaOs/logo/light.svg +6 -0
- package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
- package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
- package/docs-MachinaOs/nodes/android.mdx +411 -0
- package/docs-MachinaOs/nodes/overview.mdx +181 -0
- package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
- package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
- package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
- package/docs-MachinaOs/quickstart.mdx +119 -0
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
- package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
- package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
- package/nul +0 -0
- package/package.json +70 -0
- package/scripts/build.js +158 -0
- package/scripts/check-ports.ps1 +33 -0
- package/scripts/clean.js +40 -0
- package/scripts/docker.js +93 -0
- package/scripts/kill-port.ps1 +154 -0
- package/scripts/start.js +210 -0
- package/scripts/stop.js +325 -0
- package/server/.dockerignore +44 -0
- package/server/Dockerfile +45 -0
- package/server/constants.py +249 -0
- package/server/core/__init__.py +1 -0
- package/server/core/cache.py +461 -0
- package/server/core/config.py +128 -0
- package/server/core/container.py +99 -0
- package/server/core/database.py +1211 -0
- package/server/core/logging.py +314 -0
- package/server/main.py +289 -0
- package/server/middleware/__init__.py +5 -0
- package/server/middleware/auth.py +89 -0
- package/server/models/__init__.py +1 -0
- package/server/models/auth.py +52 -0
- package/server/models/cache.py +24 -0
- package/server/models/database.py +211 -0
- package/server/models/nodes.py +455 -0
- package/server/package.json +9 -0
- package/server/pyproject.toml +72 -0
- package/server/requirements.txt +83 -0
- package/server/routers/__init__.py +1 -0
- package/server/routers/android.py +294 -0
- package/server/routers/auth.py +203 -0
- package/server/routers/database.py +151 -0
- package/server/routers/maps.py +142 -0
- package/server/routers/nodejs_compat.py +289 -0
- package/server/routers/webhook.py +90 -0
- package/server/routers/websocket.py +2127 -0
- package/server/routers/whatsapp.py +761 -0
- package/server/routers/workflow.py +200 -0
- package/server/services/__init__.py +1 -0
- package/server/services/ai.py +2415 -0
- package/server/services/android/__init__.py +27 -0
- package/server/services/android/broadcaster.py +114 -0
- package/server/services/android/client.py +608 -0
- package/server/services/android/manager.py +78 -0
- package/server/services/android/protocol.py +165 -0
- package/server/services/android_service.py +588 -0
- package/server/services/auth.py +131 -0
- package/server/services/chat_client.py +160 -0
- package/server/services/deployment/__init__.py +12 -0
- package/server/services/deployment/manager.py +706 -0
- package/server/services/deployment/state.py +47 -0
- package/server/services/deployment/triggers.py +275 -0
- package/server/services/event_waiter.py +785 -0
- package/server/services/execution/__init__.py +77 -0
- package/server/services/execution/cache.py +769 -0
- package/server/services/execution/conditions.py +373 -0
- package/server/services/execution/dlq.py +132 -0
- package/server/services/execution/executor.py +1351 -0
- package/server/services/execution/models.py +531 -0
- package/server/services/execution/recovery.py +235 -0
- package/server/services/handlers/__init__.py +126 -0
- package/server/services/handlers/ai.py +355 -0
- package/server/services/handlers/android.py +260 -0
- package/server/services/handlers/code.py +278 -0
- package/server/services/handlers/document.py +598 -0
- package/server/services/handlers/http.py +193 -0
- package/server/services/handlers/polyglot.py +105 -0
- package/server/services/handlers/tools.py +845 -0
- package/server/services/handlers/triggers.py +107 -0
- package/server/services/handlers/utility.py +822 -0
- package/server/services/handlers/whatsapp.py +476 -0
- package/server/services/maps.py +289 -0
- package/server/services/memory_store.py +103 -0
- package/server/services/node_executor.py +375 -0
- package/server/services/parameter_resolver.py +218 -0
- package/server/services/polyglot_client.py +169 -0
- package/server/services/scheduler.py +155 -0
- package/server/services/skill_loader.py +417 -0
- package/server/services/status_broadcaster.py +826 -0
- package/server/services/temporal/__init__.py +23 -0
- package/server/services/temporal/activities.py +344 -0
- package/server/services/temporal/client.py +76 -0
- package/server/services/temporal/executor.py +147 -0
- package/server/services/temporal/worker.py +251 -0
- package/server/services/temporal/workflow.py +355 -0
- package/server/services/temporal/ws_client.py +236 -0
- package/server/services/text.py +111 -0
- package/server/services/user_auth.py +172 -0
- package/server/services/websocket_client.py +29 -0
- package/server/services/workflow.py +597 -0
- package/server/skills/android-skill/SKILL.md +82 -0
- package/server/skills/assistant-personality/SKILL.md +45 -0
- package/server/skills/code-skill/SKILL.md +140 -0
- package/server/skills/http-skill/SKILL.md +161 -0
- package/server/skills/maps-skill/SKILL.md +170 -0
- package/server/skills/memory-skill/SKILL.md +154 -0
- package/server/skills/scheduler-skill/SKILL.md +84 -0
- package/server/skills/whatsapp-skill/SKILL.md +283 -0
- package/server/uv.lock +2916 -0
- package/server/whatsapp-rpc/.dockerignore +30 -0
- package/server/whatsapp-rpc/Dockerfile +44 -0
- package/server/whatsapp-rpc/Dockerfile.web +17 -0
- package/server/whatsapp-rpc/README.md +139 -0
- package/server/whatsapp-rpc/cli.js +95 -0
- package/server/whatsapp-rpc/configs/config.yaml +7 -0
- package/server/whatsapp-rpc/docker-compose.yml +35 -0
- package/server/whatsapp-rpc/docs/API.md +410 -0
- package/server/whatsapp-rpc/go.mod +67 -0
- package/server/whatsapp-rpc/go.sum +203 -0
- package/server/whatsapp-rpc/package.json +30 -0
- package/server/whatsapp-rpc/schema.json +1294 -0
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
- package/server/whatsapp-rpc/scripts/cli.js +162 -0
- package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
- package/server/whatsapp-rpc/src/go/config/config.go +49 -0
- package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
- package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
- package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
- package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
- package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
- package/server/whatsapp-rpc/web/app.py +609 -0
- package/server/whatsapp-rpc/web/requirements.txt +6 -0
- package/server/whatsapp-rpc/web/rpc_client.py +427 -0
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
- package/server/whatsapp-rpc/web/templates/base.html +150 -0
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
- package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
- package/server/whatsapp-rpc/web/templates/groups.html +328 -0
- package/server/whatsapp-rpc/web/templates/messages.html +465 -0
- package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
- package/server/whatsapp-rpc/web/templates/send.html +259 -0
- package/server/whatsapp-rpc/web/templates/settings.html +459 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useApiKeys Hook - WebSocket-based API key management
|
|
3
|
+
*
|
|
4
|
+
* Provides API key validation, storage, and retrieval via WebSocket.
|
|
5
|
+
* This replaces the REST-based ApiKeyManagerService for real-time operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useState } from 'react';
|
|
9
|
+
import { useWebSocket } from '../contexts/WebSocketContext';
|
|
10
|
+
|
|
11
|
+
export interface ApiKeyValidationResult {
|
|
12
|
+
isValid: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
models?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseApiKeysResult {
|
|
18
|
+
// Validate and store API key
|
|
19
|
+
validateApiKey: (provider: string, apiKey: string) => Promise<ApiKeyValidationResult>;
|
|
20
|
+
|
|
21
|
+
// Save API key without validation
|
|
22
|
+
saveApiKey: (provider: string, apiKey: string) => Promise<ApiKeyValidationResult>;
|
|
23
|
+
|
|
24
|
+
// Get stored API key
|
|
25
|
+
getStoredApiKey: (provider: string) => Promise<string | null>;
|
|
26
|
+
|
|
27
|
+
// Check if API key exists
|
|
28
|
+
hasStoredKey: (provider: string) => Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
// Get stored models for a provider
|
|
31
|
+
getStoredModels: (provider: string) => Promise<string[] | null>;
|
|
32
|
+
|
|
33
|
+
// Remove stored API key
|
|
34
|
+
removeApiKey: (provider: string) => Promise<void>;
|
|
35
|
+
|
|
36
|
+
// Validate Google Maps API key
|
|
37
|
+
validateGoogleMapsKey: (apiKey: string) => Promise<ApiKeyValidationResult>;
|
|
38
|
+
|
|
39
|
+
// Get AI models for a provider
|
|
40
|
+
getAiModels: (provider: string, apiKey: string) => Promise<string[]>;
|
|
41
|
+
|
|
42
|
+
// State
|
|
43
|
+
isValidating: boolean;
|
|
44
|
+
validationError: string | null;
|
|
45
|
+
isConnected: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const useApiKeys = (): UseApiKeysResult => {
|
|
49
|
+
const {
|
|
50
|
+
validateApiKey: wsValidateApiKey,
|
|
51
|
+
getStoredApiKey: wsGetStoredApiKey,
|
|
52
|
+
saveApiKey: wsSaveApiKey,
|
|
53
|
+
deleteApiKey: wsDeleteApiKey,
|
|
54
|
+
validateMapsKey: wsValidateMapsKey,
|
|
55
|
+
getAiModels: wsGetAiModels,
|
|
56
|
+
isConnected
|
|
57
|
+
} = useWebSocket();
|
|
58
|
+
|
|
59
|
+
const [isValidating, setIsValidating] = useState(false);
|
|
60
|
+
const [validationError, setValidationError] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate API key and store if valid
|
|
64
|
+
*/
|
|
65
|
+
const validateApiKey = useCallback(async (
|
|
66
|
+
provider: string,
|
|
67
|
+
apiKey: string
|
|
68
|
+
): Promise<ApiKeyValidationResult> => {
|
|
69
|
+
setIsValidating(true);
|
|
70
|
+
setValidationError(null);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await wsValidateApiKey(provider, apiKey);
|
|
74
|
+
|
|
75
|
+
if (!result.valid) {
|
|
76
|
+
setValidationError(result.message || 'Validation failed');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
isValid: result.valid,
|
|
81
|
+
error: result.message,
|
|
82
|
+
models: result.models
|
|
83
|
+
};
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
const errorMsg = error.message || 'Validation failed';
|
|
86
|
+
setValidationError(errorMsg);
|
|
87
|
+
return {
|
|
88
|
+
isValid: false,
|
|
89
|
+
error: errorMsg
|
|
90
|
+
};
|
|
91
|
+
} finally {
|
|
92
|
+
setIsValidating(false);
|
|
93
|
+
}
|
|
94
|
+
}, [wsValidateApiKey]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Save API key without validation (for keys that can't be validated beforehand)
|
|
98
|
+
*/
|
|
99
|
+
const saveApiKey = useCallback(async (
|
|
100
|
+
provider: string,
|
|
101
|
+
apiKey: string
|
|
102
|
+
): Promise<ApiKeyValidationResult> => {
|
|
103
|
+
try {
|
|
104
|
+
const success = await wsSaveApiKey(provider, apiKey);
|
|
105
|
+
return {
|
|
106
|
+
isValid: success,
|
|
107
|
+
error: success ? undefined : 'Failed to save API key'
|
|
108
|
+
};
|
|
109
|
+
} catch (error: any) {
|
|
110
|
+
return {
|
|
111
|
+
isValid: false,
|
|
112
|
+
error: error.message || 'Failed to save API key'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}, [wsSaveApiKey]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get stored API key for a provider
|
|
119
|
+
*/
|
|
120
|
+
const getStoredApiKey = useCallback(async (
|
|
121
|
+
provider: string
|
|
122
|
+
): Promise<string | null> => {
|
|
123
|
+
try {
|
|
124
|
+
const result = await wsGetStoredApiKey(provider);
|
|
125
|
+
return result.hasKey ? (result.apiKey || null) : null;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn(`Error retrieving API key for ${provider}:`, error);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}, [wsGetStoredApiKey]);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if a stored key exists for a provider
|
|
134
|
+
*/
|
|
135
|
+
const hasStoredKey = useCallback(async (
|
|
136
|
+
provider: string
|
|
137
|
+
): Promise<boolean> => {
|
|
138
|
+
try {
|
|
139
|
+
const result = await wsGetStoredApiKey(provider);
|
|
140
|
+
return result.hasKey;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}, [wsGetStoredApiKey]);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get stored models for a provider
|
|
148
|
+
*/
|
|
149
|
+
const getStoredModels = useCallback(async (
|
|
150
|
+
provider: string
|
|
151
|
+
): Promise<string[] | null> => {
|
|
152
|
+
try {
|
|
153
|
+
// Get models directly from stored API key response (includes models from DB)
|
|
154
|
+
const result = await wsGetStoredApiKey(provider);
|
|
155
|
+
if (result.hasKey && result.models && result.models.length > 0) {
|
|
156
|
+
return result.models;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn(`Error retrieving models for ${provider}:`, error);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}, [wsGetStoredApiKey]);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Remove stored API key
|
|
167
|
+
*/
|
|
168
|
+
const removeApiKey = useCallback(async (
|
|
169
|
+
provider: string
|
|
170
|
+
): Promise<void> => {
|
|
171
|
+
try {
|
|
172
|
+
await wsDeleteApiKey(provider);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn(`Error removing API key for ${provider}:`, error);
|
|
175
|
+
}
|
|
176
|
+
}, [wsDeleteApiKey]);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validate Google Maps API key
|
|
180
|
+
*/
|
|
181
|
+
const validateGoogleMapsKey = useCallback(async (
|
|
182
|
+
apiKey: string
|
|
183
|
+
): Promise<ApiKeyValidationResult> => {
|
|
184
|
+
setIsValidating(true);
|
|
185
|
+
setValidationError(null);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const result = await wsValidateMapsKey(apiKey);
|
|
189
|
+
|
|
190
|
+
if (!result.valid) {
|
|
191
|
+
setValidationError(result.message || 'Validation failed');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
isValid: result.valid,
|
|
196
|
+
error: result.message
|
|
197
|
+
};
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
const errorMsg = error.message || 'Validation failed';
|
|
200
|
+
setValidationError(errorMsg);
|
|
201
|
+
return {
|
|
202
|
+
isValid: false,
|
|
203
|
+
error: errorMsg
|
|
204
|
+
};
|
|
205
|
+
} finally {
|
|
206
|
+
setIsValidating(false);
|
|
207
|
+
}
|
|
208
|
+
}, [wsValidateMapsKey]);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get available AI models for a provider
|
|
212
|
+
*/
|
|
213
|
+
const getAiModels = useCallback(async (
|
|
214
|
+
provider: string,
|
|
215
|
+
apiKey: string
|
|
216
|
+
): Promise<string[]> => {
|
|
217
|
+
try {
|
|
218
|
+
return await wsGetAiModels(provider, apiKey);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.warn(`Error fetching AI models for ${provider}:`, error);
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}, [wsGetAiModels]);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
validateApiKey,
|
|
227
|
+
saveApiKey,
|
|
228
|
+
getStoredApiKey,
|
|
229
|
+
hasStoredKey,
|
|
230
|
+
getStoredModels,
|
|
231
|
+
removeApiKey,
|
|
232
|
+
validateGoogleMapsKey,
|
|
233
|
+
getAiModels,
|
|
234
|
+
isValidating,
|
|
235
|
+
validationError,
|
|
236
|
+
isConnected
|
|
237
|
+
};
|
|
238
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
3
|
+
import { theme as baseTheme, lightColors, darkColors } from '../styles/theme';
|
|
4
|
+
|
|
5
|
+
export const useAppTheme = () => {
|
|
6
|
+
const { isDarkMode } = useTheme();
|
|
7
|
+
|
|
8
|
+
const dynamicTheme = useMemo(() => {
|
|
9
|
+
return {
|
|
10
|
+
...baseTheme,
|
|
11
|
+
colors: isDarkMode ? darkColors : lightColors,
|
|
12
|
+
isDarkMode,
|
|
13
|
+
};
|
|
14
|
+
}, [isDarkMode]);
|
|
15
|
+
|
|
16
|
+
return dynamicTheme;
|
|
17
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { nodeDefinitions } from '../nodeDefinitions';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'component_palette_collapsed_sections';
|
|
5
|
+
|
|
6
|
+
export const useComponentPalette = () => {
|
|
7
|
+
const [collapsedSections, setCollapsedSections] = useState<Record<string, boolean>>(() => {
|
|
8
|
+
// Try to load from localStorage first
|
|
9
|
+
try {
|
|
10
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
11
|
+
if (saved) {
|
|
12
|
+
return JSON.parse(saved);
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
// Ignore parsing errors
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Default: all sections collapsed
|
|
19
|
+
const initialCollapsed: Record<string, boolean> = {};
|
|
20
|
+
const categories = new Set(Object.values(nodeDefinitions).flatMap(def => def.group || []));
|
|
21
|
+
categories.forEach(category => {
|
|
22
|
+
initialCollapsed[category] = true;
|
|
23
|
+
});
|
|
24
|
+
return initialCollapsed;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
28
|
+
|
|
29
|
+
// Persist collapsed sections to localStorage
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsedSections));
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore storage errors
|
|
35
|
+
}
|
|
36
|
+
}, [collapsedSections]);
|
|
37
|
+
|
|
38
|
+
const toggleSection = useCallback((sectionId: string) => {
|
|
39
|
+
setCollapsedSections(prev => ({
|
|
40
|
+
...prev,
|
|
41
|
+
[sectionId]: !prev[sectionId]
|
|
42
|
+
}));
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
collapsedSections,
|
|
47
|
+
searchQuery,
|
|
48
|
+
setSearchQuery,
|
|
49
|
+
toggleSection,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { Node, Edge } from 'reactflow';
|
|
3
|
+
import { generateUniqueLabel } from './useDragAndDrop';
|
|
4
|
+
import { nodeDefinitions } from '../nodeDefinitions';
|
|
5
|
+
|
|
6
|
+
interface UseCopyPasteProps {
|
|
7
|
+
nodes: Node[];
|
|
8
|
+
edges: Edge[];
|
|
9
|
+
setNodes: (nodes: Node[] | ((nodes: Node[]) => Node[])) => void;
|
|
10
|
+
setEdges: (edges: Edge[] | ((edges: Edge[]) => Edge[])) => void;
|
|
11
|
+
saveNodeParameters?: (nodeId: string, parameters: Record<string, any>) => Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ClipboardData {
|
|
15
|
+
nodes: Node[];
|
|
16
|
+
edges: Edge[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook for copy/paste functionality with n8n-style auto-labeling.
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - Copy selected nodes and their connecting edges
|
|
24
|
+
* - Paste with offset position and unique labels
|
|
25
|
+
* - Persist node parameters to database
|
|
26
|
+
*/
|
|
27
|
+
export const useCopyPaste = ({
|
|
28
|
+
nodes,
|
|
29
|
+
edges,
|
|
30
|
+
setNodes,
|
|
31
|
+
setEdges,
|
|
32
|
+
saveNodeParameters
|
|
33
|
+
}: UseCopyPasteProps) => {
|
|
34
|
+
// In-memory clipboard (simpler than browser clipboard API)
|
|
35
|
+
const clipboardRef = useRef<ClipboardData | null>(null);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Copy selected nodes and their connecting edges to clipboard.
|
|
39
|
+
*/
|
|
40
|
+
const copySelectedNodes = useCallback(() => {
|
|
41
|
+
const selectedNodes = nodes.filter(n => n.selected);
|
|
42
|
+
if (selectedNodes.length === 0) {
|
|
43
|
+
console.log('[CopyPaste] No nodes selected to copy');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get edges that connect selected nodes (both ends must be selected)
|
|
48
|
+
const selectedNodeIds = new Set(selectedNodes.map(n => n.id));
|
|
49
|
+
const selectedEdges = edges.filter(
|
|
50
|
+
e => selectedNodeIds.has(e.source) && selectedNodeIds.has(e.target)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
clipboardRef.current = {
|
|
54
|
+
nodes: selectedNodes,
|
|
55
|
+
edges: selectedEdges,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
console.log(`[CopyPaste] Copied ${selectedNodes.length} nodes and ${selectedEdges.length} edges`);
|
|
59
|
+
}, [nodes, edges]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Paste nodes from clipboard with offset and unique labels.
|
|
63
|
+
*/
|
|
64
|
+
const pasteNodes = useCallback(async () => {
|
|
65
|
+
if (!clipboardRef.current) {
|
|
66
|
+
console.log('[CopyPaste] Nothing in clipboard to paste');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { nodes: copiedNodes, edges: copiedEdges } = clipboardRef.current;
|
|
71
|
+
|
|
72
|
+
// Generate ID mapping (old ID -> new ID)
|
|
73
|
+
const idMap = new Map<string, string>();
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
|
|
76
|
+
copiedNodes.forEach((node, index) => {
|
|
77
|
+
// Use timestamp + index to ensure unique IDs
|
|
78
|
+
idMap.set(node.id, `${node.type}-${now + index}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Offset for pasted nodes to avoid stacking on original
|
|
82
|
+
const PASTE_OFFSET = 50;
|
|
83
|
+
|
|
84
|
+
// Create new nodes with offset, new IDs, and unique labels
|
|
85
|
+
const newNodes: Node[] = [];
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < copiedNodes.length; i++) {
|
|
88
|
+
const node = copiedNodes[i];
|
|
89
|
+
const newId = idMap.get(node.id)!;
|
|
90
|
+
|
|
91
|
+
// Always use the original display name from node definition as base
|
|
92
|
+
// This ensures "WhatsApp Receive 2" copies become "WhatsApp Receive 3", not "WhatsApp Receive 2 1"
|
|
93
|
+
const baseDisplayName = nodeDefinitions[node.type!]?.displayName || node.type!;
|
|
94
|
+
|
|
95
|
+
// Generate unique label considering existing nodes AND nodes we're about to add
|
|
96
|
+
const allNodes = [...nodes, ...newNodes];
|
|
97
|
+
const uniqueLabel = generateUniqueLabel(baseDisplayName, node.type!, allNodes);
|
|
98
|
+
|
|
99
|
+
const newNode: Node = {
|
|
100
|
+
...node,
|
|
101
|
+
id: newId,
|
|
102
|
+
position: {
|
|
103
|
+
x: node.position.x + PASTE_OFFSET,
|
|
104
|
+
y: node.position.y + PASTE_OFFSET,
|
|
105
|
+
},
|
|
106
|
+
selected: true,
|
|
107
|
+
data: {
|
|
108
|
+
...node.data,
|
|
109
|
+
label: uniqueLabel,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Save parameters for new node to database
|
|
114
|
+
if (saveNodeParameters && newNode.data) {
|
|
115
|
+
try {
|
|
116
|
+
await saveNodeParameters(newId, newNode.data);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(`[CopyPaste] Failed to save parameters for ${newId}:`, error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
newNodes.push(newNode);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create new edges with updated source/target IDs
|
|
126
|
+
const newEdges: Edge[] = copiedEdges.map(edge => ({
|
|
127
|
+
...edge,
|
|
128
|
+
id: `e-${idMap.get(edge.source)}-${idMap.get(edge.target)}-${Date.now()}`,
|
|
129
|
+
source: idMap.get(edge.source)!,
|
|
130
|
+
target: idMap.get(edge.target)!,
|
|
131
|
+
selected: false,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// Deselect existing nodes and add new ones as selected
|
|
135
|
+
setNodes(nds => [
|
|
136
|
+
...nds.map(n => ({ ...n, selected: false })),
|
|
137
|
+
...newNodes,
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
setEdges(eds => [...eds, ...newEdges]);
|
|
141
|
+
|
|
142
|
+
console.log(`[CopyPaste] Pasted ${newNodes.length} nodes and ${newEdges.length} edges`);
|
|
143
|
+
}, [nodes, setNodes, setEdges, saveNodeParameters]);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if clipboard has content.
|
|
147
|
+
*/
|
|
148
|
+
const hasClipboard = clipboardRef.current !== null && clipboardRef.current.nodes.length > 0;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
copySelectedNodes,
|
|
152
|
+
pasteNodes,
|
|
153
|
+
hasClipboard,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Node } from 'reactflow';
|
|
3
|
+
import { snapToGrid, getDefaultNodePosition } from '../utils/workflow';
|
|
4
|
+
import { theme } from '../styles/theme';
|
|
5
|
+
import { nodeDefinitions } from '../nodeDefinitions';
|
|
6
|
+
|
|
7
|
+
interface UseDragAndDropProps {
|
|
8
|
+
nodes: Node[];
|
|
9
|
+
setNodes: (nodes: Node[] | ((nodes: Node[]) => Node[])) => void;
|
|
10
|
+
saveNodeParameters?: (nodeId: string, parameters: Record<string, any>) => Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a unique label for a new node (n8n pattern).
|
|
15
|
+
* First node of type: "Cron Scheduler"
|
|
16
|
+
* Second node of type: "Cron Scheduler 1"
|
|
17
|
+
* Third node of type: "Cron Scheduler 2"
|
|
18
|
+
*/
|
|
19
|
+
export const generateUniqueLabel = (displayName: string, nodeType: string, existingNodes: Node[]): string => {
|
|
20
|
+
// Collect all labels from nodes of the same type
|
|
21
|
+
const existingLabels = existingNodes
|
|
22
|
+
.filter(n => n.type === nodeType)
|
|
23
|
+
.map(n => n.data?.label as string | undefined)
|
|
24
|
+
.filter((label): label is string => !!label);
|
|
25
|
+
|
|
26
|
+
// If no nodes have this base displayName, use it as-is
|
|
27
|
+
if (!existingLabels.includes(displayName)) {
|
|
28
|
+
return displayName;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find next available suffix
|
|
32
|
+
let suffix = 1;
|
|
33
|
+
while (existingLabels.includes(`${displayName} ${suffix}`)) {
|
|
34
|
+
suffix++;
|
|
35
|
+
}
|
|
36
|
+
return `${displayName} ${suffix}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const useDragAndDrop = ({ nodes, setNodes, saveNodeParameters }: UseDragAndDropProps) => {
|
|
40
|
+
const onDragOver = useCallback((event: React.DragEvent) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.dataTransfer.dropEffect = 'move';
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const onDrop = useCallback(
|
|
46
|
+
async (event: React.DragEvent) => {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const nodeData = JSON.parse(event.dataTransfer.getData('application/reactflow'));
|
|
51
|
+
if (!nodeData || !nodeData.type) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const reactFlowBounds = (event.target as Element).getBoundingClientRect();
|
|
56
|
+
|
|
57
|
+
let position = {
|
|
58
|
+
x: event.clientX - reactFlowBounds.left - theme.constants.dragOffset.x,
|
|
59
|
+
y: event.clientY - reactFlowBounds.top - theme.constants.dragOffset.y,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Snap to grid for better alignment
|
|
63
|
+
position = snapToGrid(position);
|
|
64
|
+
|
|
65
|
+
// If no nodes exist, use default position
|
|
66
|
+
if (nodes.length === 0) {
|
|
67
|
+
position = getDefaultNodePosition(nodes.length);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get node definition to access displayName
|
|
71
|
+
const nodeDef = nodeDefinitions[nodeData.type];
|
|
72
|
+
const displayName = nodeDef?.displayName || nodeData.type;
|
|
73
|
+
|
|
74
|
+
// Generate unique label for template variable resolution (n8n pattern)
|
|
75
|
+
const uniqueLabel = generateUniqueLabel(displayName, nodeData.type, nodes);
|
|
76
|
+
|
|
77
|
+
const newNode: Node = {
|
|
78
|
+
id: `${nodeData.type}-${Date.now()}`,
|
|
79
|
+
type: nodeData.type,
|
|
80
|
+
position,
|
|
81
|
+
data: {
|
|
82
|
+
...(nodeData.data || {}),
|
|
83
|
+
label: uniqueLabel, // Add unique label for template resolution
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Save node parameters to database immediately via WebSocket (including hidden parameters)
|
|
88
|
+
if (newNode.data && Object.keys(newNode.data).length > 0 && saveNodeParameters) {
|
|
89
|
+
try {
|
|
90
|
+
await saveNodeParameters(newNode.id, newNode.data);
|
|
91
|
+
console.log(`[DragAndDrop] Saved default parameters for node ${newNode.id}:`, newNode.data);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`[DragAndDrop] Failed to save parameters for node ${newNode.id}:`, error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setNodes((nds) => nds.concat(newNode));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Error dropping node:', error);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[setNodes, nodes, saveNodeParameters]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const handleComponentDragStart = useCallback((event: React.DragEvent, definition: any) => {
|
|
106
|
+
const defaults: any = {};
|
|
107
|
+
// Handle both old (parameters) and new (properties) interface formats
|
|
108
|
+
const properties = definition.properties || definition.parameters || [];
|
|
109
|
+
properties.forEach((param: any) => {
|
|
110
|
+
defaults[param.name] = param.default;
|
|
111
|
+
});
|
|
112
|
+
event.dataTransfer.setData('application/reactflow', JSON.stringify({
|
|
113
|
+
type: definition.name || definition.type, // Handle both new (name) and old (type) formats
|
|
114
|
+
data: defaults
|
|
115
|
+
}));
|
|
116
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
onDragOver,
|
|
121
|
+
onDrop,
|
|
122
|
+
handleComponentDragStart,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useAppStore } from '../store/useAppStore';
|
|
3
|
+
import { nodeDefinitions } from '../nodeDefinitions';
|
|
4
|
+
import { Node } from 'reactflow';
|
|
5
|
+
|
|
6
|
+
interface DragVariableHookReturn {
|
|
7
|
+
handleVariableDragStart: (
|
|
8
|
+
e: React.DragEvent,
|
|
9
|
+
sourceNodeId: string,
|
|
10
|
+
propertyPath: string,
|
|
11
|
+
value: any
|
|
12
|
+
) => void;
|
|
13
|
+
getTemplateVariableName: (sourceNodeId: string) => string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook for handling drag-and-drop of template variables from connected nodes.
|
|
18
|
+
*
|
|
19
|
+
* Template variables use the SOURCE NODE LABEL as the key to ensure uniqueness
|
|
20
|
+
* when multiple nodes of the same type are connected to the same target.
|
|
21
|
+
* This follows the n8n pattern where each node has a unique label.
|
|
22
|
+
*
|
|
23
|
+
* Format: {{normalizedLabel.propertyPath}}
|
|
24
|
+
* Example: {{cronscheduler.data}} or {{cronscheduler1.data}}
|
|
25
|
+
*
|
|
26
|
+
* @param targetNodeId - The ID of the node receiving the drag (target of edge)
|
|
27
|
+
*/
|
|
28
|
+
export const useDragVariable = (_targetNodeId: string): DragVariableHookReturn => {
|
|
29
|
+
const { currentWorkflow } = useAppStore();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get template variable name for a source node.
|
|
33
|
+
* Uses node label (n8n pattern) for uniqueness when multiple same-type nodes connect.
|
|
34
|
+
* Falls back to displayName -> nodeType -> nodeId for backward compatibility.
|
|
35
|
+
*/
|
|
36
|
+
const getTemplateVariableName = useCallback((sourceNodeId: string): string => {
|
|
37
|
+
if (!currentWorkflow) return sourceNodeId;
|
|
38
|
+
|
|
39
|
+
// Find the source node by ID
|
|
40
|
+
const sourceNode = (currentWorkflow.nodes || []).find(
|
|
41
|
+
(node: Node) => node.id === sourceNodeId
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!sourceNode) return sourceNodeId;
|
|
45
|
+
|
|
46
|
+
// Priority: node.data.label > displayName > nodeType > nodeId
|
|
47
|
+
const label = sourceNode.data?.label;
|
|
48
|
+
if (label && typeof label === 'string') {
|
|
49
|
+
// Normalize label: lowercase, remove spaces
|
|
50
|
+
return label.toLowerCase().replace(/\s+/g, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback to displayName from node definition
|
|
54
|
+
const nodeDef = nodeDefinitions[sourceNode.type || ''];
|
|
55
|
+
if (nodeDef?.displayName) {
|
|
56
|
+
return nodeDef.displayName.toLowerCase().replace(/\s+/g, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Ultimate fallback: node type or ID
|
|
60
|
+
return (sourceNode.type || sourceNodeId).toLowerCase().replace(/\s+/g, '');
|
|
61
|
+
}, [currentWorkflow]);
|
|
62
|
+
|
|
63
|
+
const handleVariableDragStart = useCallback((
|
|
64
|
+
e: React.DragEvent,
|
|
65
|
+
sourceNodeId: string,
|
|
66
|
+
propertyPath: string,
|
|
67
|
+
value: any
|
|
68
|
+
) => {
|
|
69
|
+
const templateName = getTemplateVariableName(sourceNodeId);
|
|
70
|
+
const variableTemplate = `{{${templateName}.${propertyPath}}}`;
|
|
71
|
+
|
|
72
|
+
e.dataTransfer.setData('text/plain', variableTemplate);
|
|
73
|
+
e.dataTransfer.setData('application/json', JSON.stringify({
|
|
74
|
+
type: 'nodeVariable',
|
|
75
|
+
nodeId: sourceNodeId, // Include source node ID for reference
|
|
76
|
+
nodeName: templateName,
|
|
77
|
+
key: propertyPath,
|
|
78
|
+
variableTemplate,
|
|
79
|
+
dataType: typeof value
|
|
80
|
+
}));
|
|
81
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
82
|
+
}, [getTemplateVariableName]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
handleVariableDragStart,
|
|
86
|
+
getTemplateVariableName
|
|
87
|
+
};
|
|
88
|
+
};
|