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,1874 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { NodeParameter } from '../types/NodeTypes';
|
|
3
|
+
import { INodeProperties, INodePropertyOption } from '../types/INodeProperties';
|
|
4
|
+
import APIKeyValidator from './APIKeyValidator';
|
|
5
|
+
import CodeEditor from './ui/CodeEditor';
|
|
6
|
+
import DynamicParameterService from '../services/dynamicParameterService';
|
|
7
|
+
import { useAppStore } from '../store/useAppStore';
|
|
8
|
+
import { ANDROID_SERVICE_NODE_TYPES } from '../nodeDefinitions/androidServiceNodes';
|
|
9
|
+
import { nodeDefinitions } from '../nodeDefinitions';
|
|
10
|
+
import { useAppTheme } from '../hooks/useAppTheme';
|
|
11
|
+
import { API_CONFIG } from '../config/api';
|
|
12
|
+
import { useWebSocket } from '../contexts/WebSocketContext';
|
|
13
|
+
import { useApiKeys } from '../hooks/useApiKeys';
|
|
14
|
+
|
|
15
|
+
// Map node types to provider keys for AI model nodes
|
|
16
|
+
const NODE_TYPE_TO_PROVIDER: Record<string, string> = {
|
|
17
|
+
'openaiChatModel': 'openai',
|
|
18
|
+
'anthropicChatModel': 'anthropic',
|
|
19
|
+
'claudeChatModel': 'anthropic',
|
|
20
|
+
'googleChatModel': 'gemini',
|
|
21
|
+
'geminiChatModel': 'gemini',
|
|
22
|
+
'azureChatModel': 'azure_openai',
|
|
23
|
+
'cohereChatModel': 'cohere',
|
|
24
|
+
'ollamaChatModel': 'ollama',
|
|
25
|
+
'mistralChatModel': 'mistral',
|
|
26
|
+
'openrouterChatModel': 'openrouter',
|
|
27
|
+
'groqChatModel': 'groq',
|
|
28
|
+
'cerebrasChatModel': 'cerebras'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Collection Renderer - n8n official style
|
|
32
|
+
const CollectionRenderer: React.FC<{
|
|
33
|
+
parameter: any;
|
|
34
|
+
value: any;
|
|
35
|
+
onChange: (value: any) => void;
|
|
36
|
+
allParameters?: Record<string, any>;
|
|
37
|
+
theme: ReturnType<typeof useAppTheme>;
|
|
38
|
+
}> = ({ parameter, value, onChange, allParameters, theme }) => {
|
|
39
|
+
const [showAddOption, setShowAddOption] = useState(false);
|
|
40
|
+
const currentValue = value || {};
|
|
41
|
+
const addedOptions = Object.keys(currentValue).filter(key => currentValue[key] !== undefined);
|
|
42
|
+
const availableOptions = parameter.options?.filter((opt: any) => !addedOptions.includes(opt.name)) || [];
|
|
43
|
+
|
|
44
|
+
const addOption = (optionName: string) => {
|
|
45
|
+
const option = parameter.options?.find((opt: any) => opt.name === optionName);
|
|
46
|
+
if (option) {
|
|
47
|
+
onChange({
|
|
48
|
+
...currentValue,
|
|
49
|
+
[optionName]: option.default
|
|
50
|
+
});
|
|
51
|
+
setShowAddOption(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const removeOption = (optionName: string) => {
|
|
56
|
+
const newValue = { ...currentValue };
|
|
57
|
+
delete newValue[optionName];
|
|
58
|
+
onChange(newValue);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const updateOption = (optionName: string, optionValue: any) => {
|
|
62
|
+
onChange({
|
|
63
|
+
...currentValue,
|
|
64
|
+
[optionName]: optionValue
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
{addedOptions.length === 0 && (
|
|
71
|
+
<div style={{
|
|
72
|
+
fontSize: '14px',
|
|
73
|
+
color: theme.colors.textSecondary,
|
|
74
|
+
marginBottom: '12px',
|
|
75
|
+
padding: '8px 0'
|
|
76
|
+
}}>
|
|
77
|
+
No properties
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{addedOptions.map((optionName) => {
|
|
82
|
+
const option = parameter.options?.find((opt: any) => opt.name === optionName);
|
|
83
|
+
if (!option) return null;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div key={optionName} style={{
|
|
87
|
+
marginBottom: '16px',
|
|
88
|
+
padding: '12px',
|
|
89
|
+
border: `1px solid ${theme.colors.border}`,
|
|
90
|
+
borderRadius: '4px',
|
|
91
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
92
|
+
position: 'relative'
|
|
93
|
+
}}>
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => removeOption(optionName)}
|
|
96
|
+
style={{
|
|
97
|
+
position: 'absolute',
|
|
98
|
+
top: '6px',
|
|
99
|
+
right: '6px',
|
|
100
|
+
background: 'none',
|
|
101
|
+
border: 'none',
|
|
102
|
+
color: theme.colors.textSecondary,
|
|
103
|
+
cursor: 'pointer',
|
|
104
|
+
fontSize: '14px',
|
|
105
|
+
padding: '2px 4px',
|
|
106
|
+
borderRadius: '2px'
|
|
107
|
+
}}
|
|
108
|
+
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.border}
|
|
109
|
+
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
110
|
+
title="Remove"
|
|
111
|
+
>
|
|
112
|
+
✕
|
|
113
|
+
</button>
|
|
114
|
+
<ParameterRenderer
|
|
115
|
+
parameter={option}
|
|
116
|
+
value={currentValue[optionName]}
|
|
117
|
+
onChange={(newValue) => updateOption(optionName, newValue)}
|
|
118
|
+
allParameters={allParameters}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
|
|
124
|
+
{availableOptions.length > 0 && (
|
|
125
|
+
<div style={{ position: 'relative' }}>
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => setShowAddOption(!showAddOption)}
|
|
128
|
+
style={{
|
|
129
|
+
width: '100%',
|
|
130
|
+
padding: '10px 12px',
|
|
131
|
+
border: `1px solid ${theme.colors.border}`,
|
|
132
|
+
borderRadius: '4px',
|
|
133
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
134
|
+
color: theme.colors.textSecondary,
|
|
135
|
+
cursor: 'pointer',
|
|
136
|
+
fontSize: '14px',
|
|
137
|
+
display: 'flex',
|
|
138
|
+
alignItems: 'center',
|
|
139
|
+
justifyContent: 'space-between',
|
|
140
|
+
transition: 'all 0.2s ease'
|
|
141
|
+
}}
|
|
142
|
+
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.borderHover}
|
|
143
|
+
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
|
|
144
|
+
>
|
|
145
|
+
{parameter.placeholder || 'Add Option'}
|
|
146
|
+
<span style={{
|
|
147
|
+
transform: showAddOption ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
148
|
+
transition: 'transform 0.2s ease'
|
|
149
|
+
}}>
|
|
150
|
+
▼
|
|
151
|
+
</span>
|
|
152
|
+
</button>
|
|
153
|
+
|
|
154
|
+
{showAddOption && (
|
|
155
|
+
<div style={{
|
|
156
|
+
position: 'absolute',
|
|
157
|
+
top: '100%',
|
|
158
|
+
left: 0,
|
|
159
|
+
right: 0,
|
|
160
|
+
zIndex: 1000,
|
|
161
|
+
backgroundColor: theme.colors.background,
|
|
162
|
+
border: `1px solid ${theme.colors.border}`,
|
|
163
|
+
borderRadius: '4px',
|
|
164
|
+
marginTop: '2px',
|
|
165
|
+
boxShadow: `0 4px 6px -1px ${theme.colors.shadow}`,
|
|
166
|
+
maxHeight: '200px',
|
|
167
|
+
overflowY: 'auto'
|
|
168
|
+
}}>
|
|
169
|
+
{availableOptions.map((option: any, index: number) => (
|
|
170
|
+
<button
|
|
171
|
+
key={option.name}
|
|
172
|
+
onClick={() => addOption(option.name)}
|
|
173
|
+
style={{
|
|
174
|
+
width: '100%',
|
|
175
|
+
padding: '10px 12px',
|
|
176
|
+
border: 'none',
|
|
177
|
+
backgroundColor: 'transparent',
|
|
178
|
+
color: theme.colors.text,
|
|
179
|
+
cursor: 'pointer',
|
|
180
|
+
fontSize: '14px',
|
|
181
|
+
textAlign: 'left',
|
|
182
|
+
borderBottom: index < availableOptions.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
|
|
183
|
+
}}
|
|
184
|
+
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
|
|
185
|
+
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
186
|
+
>
|
|
187
|
+
<div style={{ fontWeight: '500' }}>
|
|
188
|
+
{option.displayName}
|
|
189
|
+
</div>
|
|
190
|
+
{option.description && (
|
|
191
|
+
<div style={{
|
|
192
|
+
fontSize: '12px',
|
|
193
|
+
color: theme.colors.textSecondary,
|
|
194
|
+
marginTop: '2px'
|
|
195
|
+
}}>
|
|
196
|
+
{option.description}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</button>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Group ID Selector - with Load Groups button and dropdown
|
|
210
|
+
const GroupIdSelector: React.FC<{
|
|
211
|
+
value: string;
|
|
212
|
+
onChange: (value: string) => void;
|
|
213
|
+
onNameChange?: (name: string) => void;
|
|
214
|
+
storedName?: string;
|
|
215
|
+
placeholder?: string;
|
|
216
|
+
theme: ReturnType<typeof useAppTheme>;
|
|
217
|
+
isDragOver: boolean;
|
|
218
|
+
onDragOver: (e: React.DragEvent) => void;
|
|
219
|
+
onDragLeave: (e: React.DragEvent) => void;
|
|
220
|
+
onDrop: (e: React.DragEvent) => void;
|
|
221
|
+
}> = ({ value, onChange, onNameChange, storedName, placeholder, theme, isDragOver, onDragOver, onDragLeave, onDrop }) => {
|
|
222
|
+
const [groups, setGroups] = useState<Array<{ jid: string; name: string; topic?: string; size?: number; is_community?: boolean }>>([]);
|
|
223
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
224
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
225
|
+
const [error, setError] = useState<string | null>(null);
|
|
226
|
+
// Use stored name if available, otherwise local state
|
|
227
|
+
const [localGroupName, setLocalGroupName] = useState<string | null>(null);
|
|
228
|
+
const selectedGroupName = storedName || localGroupName;
|
|
229
|
+
const { getWhatsAppGroups } = useWebSocket();
|
|
230
|
+
|
|
231
|
+
// Sync local state with stored name
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (storedName) {
|
|
234
|
+
setLocalGroupName(storedName);
|
|
235
|
+
}
|
|
236
|
+
}, [storedName]);
|
|
237
|
+
|
|
238
|
+
const handleLoadGroups = async () => {
|
|
239
|
+
setIsLoading(true);
|
|
240
|
+
setError(null);
|
|
241
|
+
try {
|
|
242
|
+
const result = await getWhatsAppGroups();
|
|
243
|
+
console.log('[GroupIdSelector] Raw groups from API:', result.groups?.map(g => ({ name: g.name, jid: g.jid, is_community: g.is_community })));
|
|
244
|
+
if (result.success && result.groups.length > 0) {
|
|
245
|
+
// Filter out communities - they don't have regular chat history
|
|
246
|
+
const regularGroups = result.groups.filter(g => !g.is_community);
|
|
247
|
+
console.log('[GroupIdSelector] After filtering communities:', regularGroups.length, 'groups remaining');
|
|
248
|
+
if (regularGroups.length === 0) {
|
|
249
|
+
setError('Only communities found (no chat history available)');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
setGroups(regularGroups);
|
|
253
|
+
setShowDropdown(true);
|
|
254
|
+
// If we already have a value, try to find its name and update storage
|
|
255
|
+
if (value) {
|
|
256
|
+
const matchingGroup = regularGroups.find(g => g.jid === value);
|
|
257
|
+
if (matchingGroup && matchingGroup.name !== storedName) {
|
|
258
|
+
setLocalGroupName(matchingGroup.name);
|
|
259
|
+
onNameChange?.(matchingGroup.name);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else if (result.error) {
|
|
263
|
+
setError(result.error);
|
|
264
|
+
} else {
|
|
265
|
+
setError('No groups found');
|
|
266
|
+
}
|
|
267
|
+
} catch (err: any) {
|
|
268
|
+
setError(err.message || 'Failed to load groups');
|
|
269
|
+
} finally {
|
|
270
|
+
setIsLoading(false);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const handleSelectGroup = (group: { jid: string; name: string }) => {
|
|
275
|
+
onChange(group.jid);
|
|
276
|
+
setLocalGroupName(group.name);
|
|
277
|
+
onNameChange?.(group.name);
|
|
278
|
+
setShowDropdown(false);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
282
|
+
onChange(e.target.value);
|
|
283
|
+
// Clear group name when user types manually
|
|
284
|
+
setLocalGroupName(null);
|
|
285
|
+
onNameChange?.('');
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Display value: show group name if selected, otherwise show JID
|
|
289
|
+
const displayValue = selectedGroupName || value;
|
|
290
|
+
const isGroupSelected = selectedGroupName !== null && value;
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div style={{ position: 'relative' }}>
|
|
294
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
295
|
+
<input
|
|
296
|
+
type="text"
|
|
297
|
+
value={displayValue}
|
|
298
|
+
onChange={handleInputChange}
|
|
299
|
+
placeholder={placeholder || '123456789@g.us'}
|
|
300
|
+
onDragOver={onDragOver}
|
|
301
|
+
onDragLeave={onDragLeave}
|
|
302
|
+
onDrop={onDrop}
|
|
303
|
+
style={{
|
|
304
|
+
flex: 1,
|
|
305
|
+
padding: '8px 12px',
|
|
306
|
+
border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
|
|
307
|
+
borderRadius: '6px',
|
|
308
|
+
fontSize: '14px',
|
|
309
|
+
backgroundColor: isGroupSelected ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
|
|
310
|
+
color: isGroupSelected ? theme.colors.success : (value && value.includes('{{') ? theme.colors.templateVariable : theme.colors.text),
|
|
311
|
+
outline: 'none',
|
|
312
|
+
transition: 'all 0.2s ease',
|
|
313
|
+
fontFamily: value && value.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
|
|
314
|
+
fontWeight: isGroupSelected ? '500' : 'normal'
|
|
315
|
+
}}
|
|
316
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
317
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
318
|
+
/>
|
|
319
|
+
<button
|
|
320
|
+
onClick={handleLoadGroups}
|
|
321
|
+
disabled={isLoading}
|
|
322
|
+
style={{
|
|
323
|
+
padding: '8px 12px',
|
|
324
|
+
border: `1px solid ${isLoading ? theme.colors.border : `${theme.colors.focus}40`}`,
|
|
325
|
+
borderRadius: '6px',
|
|
326
|
+
backgroundColor: isLoading ? 'transparent' : `${theme.colors.focus}18`,
|
|
327
|
+
color: isLoading ? theme.colors.textMuted : theme.colors.focus,
|
|
328
|
+
cursor: isLoading ? 'wait' : 'pointer',
|
|
329
|
+
fontSize: '13px',
|
|
330
|
+
fontWeight: 600,
|
|
331
|
+
transition: 'all 0.2s ease',
|
|
332
|
+
whiteSpace: 'nowrap',
|
|
333
|
+
opacity: isLoading ? 0.7 : 1
|
|
334
|
+
}}
|
|
335
|
+
onMouseEnter={(e) => {
|
|
336
|
+
if (!isLoading) {
|
|
337
|
+
e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
|
|
338
|
+
}
|
|
339
|
+
}}
|
|
340
|
+
onMouseLeave={(e) => {
|
|
341
|
+
e.currentTarget.style.backgroundColor = isLoading ? 'transparent' : `${theme.colors.focus}18`;
|
|
342
|
+
}}
|
|
343
|
+
title="Load WhatsApp groups"
|
|
344
|
+
>
|
|
345
|
+
{isLoading ? 'Loading...' : 'Load'}
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
{/* Show JID below when group name is displayed */}
|
|
349
|
+
{isGroupSelected && (
|
|
350
|
+
<div style={{
|
|
351
|
+
fontSize: '11px',
|
|
352
|
+
color: theme.colors.textSecondary,
|
|
353
|
+
marginTop: '4px',
|
|
354
|
+
fontFamily: 'monospace'
|
|
355
|
+
}}>
|
|
356
|
+
{value}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{error && (
|
|
361
|
+
<div style={{
|
|
362
|
+
fontSize: '12px',
|
|
363
|
+
color: theme.colors.error,
|
|
364
|
+
marginTop: '4px'
|
|
365
|
+
}}>
|
|
366
|
+
{error}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{showDropdown && groups.length > 0 && (
|
|
371
|
+
<div style={{
|
|
372
|
+
position: 'absolute',
|
|
373
|
+
top: '100%',
|
|
374
|
+
left: 0,
|
|
375
|
+
right: 0,
|
|
376
|
+
marginTop: '4px',
|
|
377
|
+
backgroundColor: theme.colors.background,
|
|
378
|
+
border: `1px solid ${theme.colors.border}`,
|
|
379
|
+
borderRadius: '6px',
|
|
380
|
+
boxShadow: `0 4px 12px ${theme.colors.shadow}`,
|
|
381
|
+
maxHeight: '200px',
|
|
382
|
+
overflowY: 'auto',
|
|
383
|
+
zIndex: 1000
|
|
384
|
+
}}>
|
|
385
|
+
{groups.map((group, index) => (
|
|
386
|
+
<button
|
|
387
|
+
key={group.jid}
|
|
388
|
+
onClick={() => handleSelectGroup(group)}
|
|
389
|
+
style={{
|
|
390
|
+
width: '100%',
|
|
391
|
+
padding: '10px 12px',
|
|
392
|
+
border: 'none',
|
|
393
|
+
backgroundColor: 'transparent',
|
|
394
|
+
color: theme.colors.text,
|
|
395
|
+
cursor: 'pointer',
|
|
396
|
+
fontSize: '13px',
|
|
397
|
+
textAlign: 'left',
|
|
398
|
+
borderBottom: index < groups.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
|
|
399
|
+
}}
|
|
400
|
+
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
|
|
401
|
+
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
|
402
|
+
>
|
|
403
|
+
<div style={{ fontWeight: '500' }}>{group.name}</div>
|
|
404
|
+
<div style={{
|
|
405
|
+
fontSize: '11px',
|
|
406
|
+
color: theme.colors.textSecondary,
|
|
407
|
+
marginTop: '2px',
|
|
408
|
+
fontFamily: 'monospace'
|
|
409
|
+
}}>
|
|
410
|
+
{group.jid}
|
|
411
|
+
{group.size && <span style={{ marginLeft: '8px' }}>({group.size} members)</span>}
|
|
412
|
+
</div>
|
|
413
|
+
</button>
|
|
414
|
+
))}
|
|
415
|
+
<button
|
|
416
|
+
onClick={() => setShowDropdown(false)}
|
|
417
|
+
style={{
|
|
418
|
+
width: '100%',
|
|
419
|
+
padding: '8px 12px',
|
|
420
|
+
border: 'none',
|
|
421
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
422
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
423
|
+
color: theme.colors.textSecondary,
|
|
424
|
+
cursor: 'pointer',
|
|
425
|
+
fontSize: '12px',
|
|
426
|
+
textAlign: 'center'
|
|
427
|
+
}}
|
|
428
|
+
onMouseEnter={(e) => e.currentTarget.style.color = theme.colors.text}
|
|
429
|
+
onMouseLeave={(e) => e.currentTarget.style.color = theme.colors.textSecondary}
|
|
430
|
+
>
|
|
431
|
+
Close
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Sender Number Selector - with Load Members button and dropdown (loads from selected group)
|
|
440
|
+
const SenderNumberSelector: React.FC<{
|
|
441
|
+
value: string;
|
|
442
|
+
onChange: (value: string) => void;
|
|
443
|
+
onNameChange?: (name: string) => void;
|
|
444
|
+
storedName?: string;
|
|
445
|
+
placeholder?: string;
|
|
446
|
+
theme: ReturnType<typeof useAppTheme>;
|
|
447
|
+
isDragOver: boolean;
|
|
448
|
+
onDragOver: (e: React.DragEvent) => void;
|
|
449
|
+
onDragLeave: (e: React.DragEvent) => void;
|
|
450
|
+
onDrop: (e: React.DragEvent) => void;
|
|
451
|
+
groupId: string; // The selected group to load members from
|
|
452
|
+
}> = ({ value, onChange, onNameChange, storedName, placeholder, theme, isDragOver, onDragOver, onDragLeave, onDrop, groupId }) => {
|
|
453
|
+
const [members, setMembers] = useState<Array<{ phone: string; name: string; jid: string; is_admin?: boolean }>>([]);
|
|
454
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
455
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
456
|
+
const [error, setError] = useState<string | null>(null);
|
|
457
|
+
// Use stored name if available, otherwise local state
|
|
458
|
+
const [localMemberName, setLocalMemberName] = useState<string | null>(null);
|
|
459
|
+
const selectedMemberName = storedName || localMemberName;
|
|
460
|
+
const { getWhatsAppGroupInfo } = useWebSocket();
|
|
461
|
+
|
|
462
|
+
// Sync local state with stored name
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
if (storedName) {
|
|
465
|
+
setLocalMemberName(storedName);
|
|
466
|
+
}
|
|
467
|
+
}, [storedName]);
|
|
468
|
+
|
|
469
|
+
const handleLoadMembers = async () => {
|
|
470
|
+
if (!groupId) {
|
|
471
|
+
setError('Select a group first');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
setIsLoading(true);
|
|
476
|
+
setError(null);
|
|
477
|
+
try {
|
|
478
|
+
const result = await getWhatsAppGroupInfo(groupId);
|
|
479
|
+
if (result.success && result.participants && result.participants.length > 0) {
|
|
480
|
+
setMembers(result.participants);
|
|
481
|
+
setShowDropdown(true);
|
|
482
|
+
// If we already have a value, try to find its name and update storage
|
|
483
|
+
if (value) {
|
|
484
|
+
const matchingMember = result.participants.find((m: any) => m.phone === value);
|
|
485
|
+
if (matchingMember) {
|
|
486
|
+
const name = matchingMember.name || matchingMember.phone;
|
|
487
|
+
if (name !== storedName) {
|
|
488
|
+
setLocalMemberName(name);
|
|
489
|
+
onNameChange?.(name);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} else if (result.error) {
|
|
494
|
+
setError(result.error);
|
|
495
|
+
} else {
|
|
496
|
+
setError('No members found');
|
|
497
|
+
}
|
|
498
|
+
} catch (err: any) {
|
|
499
|
+
setError(err.message || 'Failed to load members');
|
|
500
|
+
} finally {
|
|
501
|
+
setIsLoading(false);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const handleSelectMember = (member: { phone: string; name: string }) => {
|
|
506
|
+
const name = member.name || member.phone;
|
|
507
|
+
onChange(member.phone);
|
|
508
|
+
setLocalMemberName(name);
|
|
509
|
+
onNameChange?.(name);
|
|
510
|
+
setShowDropdown(false);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const handleClearSelection = () => {
|
|
514
|
+
onChange('');
|
|
515
|
+
setLocalMemberName(null);
|
|
516
|
+
onNameChange?.('');
|
|
517
|
+
setShowDropdown(false);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
521
|
+
onChange(e.target.value);
|
|
522
|
+
// Clear member name when user types manually
|
|
523
|
+
setLocalMemberName(null);
|
|
524
|
+
onNameChange?.('');
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// Display value: show member name if selected, otherwise show phone
|
|
528
|
+
const displayValue = selectedMemberName || value;
|
|
529
|
+
const isMemberSelected = selectedMemberName !== null && value;
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<div style={{ position: 'relative' }}>
|
|
533
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
534
|
+
<input
|
|
535
|
+
type="text"
|
|
536
|
+
value={displayValue}
|
|
537
|
+
onChange={handleInputChange}
|
|
538
|
+
placeholder={placeholder || 'All members (leave empty)'}
|
|
539
|
+
onDragOver={onDragOver}
|
|
540
|
+
onDragLeave={onDragLeave}
|
|
541
|
+
onDrop={onDrop}
|
|
542
|
+
style={{
|
|
543
|
+
flex: 1,
|
|
544
|
+
padding: '8px 12px',
|
|
545
|
+
border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
|
|
546
|
+
borderRadius: '6px',
|
|
547
|
+
fontSize: '14px',
|
|
548
|
+
backgroundColor: isMemberSelected ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
|
|
549
|
+
color: isMemberSelected ? theme.colors.success : (value && value.includes('{{') ? theme.colors.templateVariable : theme.colors.text),
|
|
550
|
+
outline: 'none',
|
|
551
|
+
transition: 'all 0.2s ease',
|
|
552
|
+
fontFamily: value && value.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
|
|
553
|
+
fontWeight: isMemberSelected ? '500' : 'normal'
|
|
554
|
+
}}
|
|
555
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
556
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
557
|
+
/>
|
|
558
|
+
<button
|
|
559
|
+
onClick={handleLoadMembers}
|
|
560
|
+
disabled={isLoading || !groupId}
|
|
561
|
+
style={{
|
|
562
|
+
padding: '8px 12px',
|
|
563
|
+
border: `1px solid ${(isLoading || !groupId) ? theme.colors.border : `${theme.colors.focus}40`}`,
|
|
564
|
+
borderRadius: '6px',
|
|
565
|
+
backgroundColor: (isLoading || !groupId) ? 'transparent' : `${theme.colors.focus}18`,
|
|
566
|
+
color: (isLoading || !groupId) ? theme.colors.textMuted : theme.colors.focus,
|
|
567
|
+
cursor: (isLoading || !groupId) ? 'not-allowed' : 'pointer',
|
|
568
|
+
fontSize: '13px',
|
|
569
|
+
fontWeight: 600,
|
|
570
|
+
transition: 'all 0.2s ease',
|
|
571
|
+
whiteSpace: 'nowrap',
|
|
572
|
+
opacity: (isLoading || !groupId) ? 0.7 : 1
|
|
573
|
+
}}
|
|
574
|
+
onMouseEnter={(e) => {
|
|
575
|
+
if (!isLoading && groupId) {
|
|
576
|
+
e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
|
|
577
|
+
}
|
|
578
|
+
}}
|
|
579
|
+
onMouseLeave={(e) => {
|
|
580
|
+
e.currentTarget.style.backgroundColor = (isLoading || !groupId) ? 'transparent' : `${theme.colors.focus}18`;
|
|
581
|
+
}}
|
|
582
|
+
title={groupId ? "Load group members" : "Select a group first"}
|
|
583
|
+
>
|
|
584
|
+
{isLoading ? 'Loading...' : 'Load'}
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
{/* Show phone below when member name is displayed */}
|
|
588
|
+
{isMemberSelected && (
|
|
589
|
+
<div style={{
|
|
590
|
+
fontSize: '11px',
|
|
591
|
+
color: theme.colors.textSecondary,
|
|
592
|
+
marginTop: '4px',
|
|
593
|
+
fontFamily: 'monospace'
|
|
594
|
+
}}>
|
|
595
|
+
{value}
|
|
596
|
+
</div>
|
|
597
|
+
)}
|
|
598
|
+
|
|
599
|
+
{error && (
|
|
600
|
+
<div style={{
|
|
601
|
+
fontSize: '12px',
|
|
602
|
+
color: theme.colors.error,
|
|
603
|
+
marginTop: '4px'
|
|
604
|
+
}}>
|
|
605
|
+
{error}
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
{showDropdown && members.length > 0 && (
|
|
610
|
+
<div style={{
|
|
611
|
+
position: 'absolute',
|
|
612
|
+
top: '100%',
|
|
613
|
+
left: 0,
|
|
614
|
+
right: 0,
|
|
615
|
+
marginTop: '4px',
|
|
616
|
+
backgroundColor: theme.colors.background,
|
|
617
|
+
border: `1px solid ${theme.colors.border}`,
|
|
618
|
+
borderRadius: '6px',
|
|
619
|
+
boxShadow: `0 4px 12px ${theme.colors.shadow}`,
|
|
620
|
+
maxHeight: '250px',
|
|
621
|
+
overflowY: 'auto',
|
|
622
|
+
zIndex: 1000
|
|
623
|
+
}}>
|
|
624
|
+
{/* All Members option */}
|
|
625
|
+
<button
|
|
626
|
+
onClick={handleClearSelection}
|
|
627
|
+
style={{
|
|
628
|
+
width: '100%',
|
|
629
|
+
padding: '10px 12px',
|
|
630
|
+
border: 'none',
|
|
631
|
+
backgroundColor: !value ? theme.colors.backgroundAlt : 'transparent',
|
|
632
|
+
color: theme.colors.text,
|
|
633
|
+
cursor: 'pointer',
|
|
634
|
+
fontSize: '13px',
|
|
635
|
+
textAlign: 'left',
|
|
636
|
+
borderBottom: `1px solid ${theme.colors.border}`
|
|
637
|
+
}}
|
|
638
|
+
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
|
|
639
|
+
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = !value ? theme.colors.backgroundAlt : 'transparent'}
|
|
640
|
+
>
|
|
641
|
+
<div style={{ fontWeight: '500', color: theme.colors.textSecondary }}>All Members</div>
|
|
642
|
+
<div style={{ fontSize: '11px', color: theme.colors.textMuted, marginTop: '2px' }}>
|
|
643
|
+
Receive from anyone in group
|
|
644
|
+
</div>
|
|
645
|
+
</button>
|
|
646
|
+
{members.map((member, index) => (
|
|
647
|
+
<button
|
|
648
|
+
key={member.jid || member.phone}
|
|
649
|
+
onClick={() => handleSelectMember(member)}
|
|
650
|
+
style={{
|
|
651
|
+
width: '100%',
|
|
652
|
+
padding: '10px 12px',
|
|
653
|
+
border: 'none',
|
|
654
|
+
backgroundColor: value === member.phone ? theme.colors.backgroundAlt : 'transparent',
|
|
655
|
+
color: theme.colors.text,
|
|
656
|
+
cursor: 'pointer',
|
|
657
|
+
fontSize: '13px',
|
|
658
|
+
textAlign: 'left',
|
|
659
|
+
borderBottom: index < members.length - 1 ? `1px solid ${theme.colors.border}` : 'none'
|
|
660
|
+
}}
|
|
661
|
+
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = theme.colors.backgroundAlt}
|
|
662
|
+
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = value === member.phone ? theme.colors.backgroundAlt : 'transparent'}
|
|
663
|
+
>
|
|
664
|
+
<div style={{ fontWeight: '500' }}>
|
|
665
|
+
{member.name || member.phone}
|
|
666
|
+
{member.is_admin && <span style={{ marginLeft: '8px', fontSize: '10px', color: theme.colors.warning }}>(Admin)</span>}
|
|
667
|
+
</div>
|
|
668
|
+
<div style={{
|
|
669
|
+
fontSize: '11px',
|
|
670
|
+
color: theme.colors.textSecondary,
|
|
671
|
+
marginTop: '2px',
|
|
672
|
+
fontFamily: 'monospace'
|
|
673
|
+
}}>
|
|
674
|
+
{member.phone}
|
|
675
|
+
</div>
|
|
676
|
+
</button>
|
|
677
|
+
))}
|
|
678
|
+
<button
|
|
679
|
+
onClick={() => setShowDropdown(false)}
|
|
680
|
+
style={{
|
|
681
|
+
width: '100%',
|
|
682
|
+
padding: '8px 12px',
|
|
683
|
+
border: 'none',
|
|
684
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
685
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
686
|
+
color: theme.colors.textSecondary,
|
|
687
|
+
cursor: 'pointer',
|
|
688
|
+
fontSize: '12px',
|
|
689
|
+
textAlign: 'center'
|
|
690
|
+
}}
|
|
691
|
+
onMouseEnter={(e) => e.currentTarget.style.color = theme.colors.text}
|
|
692
|
+
onMouseLeave={(e) => e.currentTarget.style.color = theme.colors.textSecondary}
|
|
693
|
+
>
|
|
694
|
+
Close
|
|
695
|
+
</button>
|
|
696
|
+
</div>
|
|
697
|
+
)}
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// Fixed Collection Renderer - n8n style fixed collection
|
|
703
|
+
const FixedCollectionRenderer: React.FC<{
|
|
704
|
+
parameter: any;
|
|
705
|
+
value: any;
|
|
706
|
+
onChange: (value: any) => void;
|
|
707
|
+
allParameters?: Record<string, any>;
|
|
708
|
+
theme: ReturnType<typeof useAppTheme>;
|
|
709
|
+
}> = ({ parameter, value, onChange, allParameters, theme }) => {
|
|
710
|
+
const currentValue = value || {};
|
|
711
|
+
|
|
712
|
+
return (
|
|
713
|
+
<div style={{
|
|
714
|
+
border: `1px solid ${theme.colors.border}`,
|
|
715
|
+
borderRadius: '6px',
|
|
716
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
717
|
+
padding: '12px'
|
|
718
|
+
}}>
|
|
719
|
+
{parameter.options?.map((option: any) => {
|
|
720
|
+
const optionValue = currentValue[option.name] || {};
|
|
721
|
+
|
|
722
|
+
return (
|
|
723
|
+
<div key={option.name} style={{ marginBottom: '16px' }}>
|
|
724
|
+
<div style={{
|
|
725
|
+
fontSize: '14px',
|
|
726
|
+
fontWeight: '500',
|
|
727
|
+
color: theme.colors.text,
|
|
728
|
+
marginBottom: '8px'
|
|
729
|
+
}}>
|
|
730
|
+
{option.displayName}
|
|
731
|
+
</div>
|
|
732
|
+
<div style={{
|
|
733
|
+
border: `1px solid ${theme.colors.border}`,
|
|
734
|
+
borderRadius: '6px',
|
|
735
|
+
backgroundColor: theme.colors.background,
|
|
736
|
+
padding: '12px'
|
|
737
|
+
}}>
|
|
738
|
+
{option.values?.map((valueParam: any) => (
|
|
739
|
+
<ParameterRenderer
|
|
740
|
+
key={valueParam.name}
|
|
741
|
+
parameter={valueParam}
|
|
742
|
+
value={optionValue[valueParam.name]}
|
|
743
|
+
onChange={(newValue) => {
|
|
744
|
+
onChange({
|
|
745
|
+
...currentValue,
|
|
746
|
+
[option.name]: {
|
|
747
|
+
...optionValue,
|
|
748
|
+
[valueParam.name]: newValue
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}}
|
|
752
|
+
allParameters={allParameters}
|
|
753
|
+
/>
|
|
754
|
+
))}
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
);
|
|
758
|
+
})}
|
|
759
|
+
</div>
|
|
760
|
+
);
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
interface ParameterRendererProps {
|
|
764
|
+
parameter: NodeParameter | INodeProperties;
|
|
765
|
+
value: any;
|
|
766
|
+
onChange: (value: any) => void;
|
|
767
|
+
allParameters?: Record<string, any>;
|
|
768
|
+
onParameterChange?: (paramName: string, value: any) => void;
|
|
769
|
+
onClosePanel?: () => void;
|
|
770
|
+
isLoadingParameters?: boolean;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Type guard to check if parameter is INodeProperties
|
|
774
|
+
const isINodeProperties = (param: NodeParameter | INodeProperties): param is INodeProperties => {
|
|
775
|
+
return 'typeOptions' in param;
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const ParameterRenderer: React.FC<ParameterRendererProps> = ({
|
|
779
|
+
parameter,
|
|
780
|
+
value,
|
|
781
|
+
onChange,
|
|
782
|
+
allParameters,
|
|
783
|
+
onParameterChange,
|
|
784
|
+
isLoadingParameters = false,
|
|
785
|
+
}) => {
|
|
786
|
+
const theme = useAppTheme();
|
|
787
|
+
// Don't use default while loading - wait for actual saved value to load
|
|
788
|
+
// This prevents showing template code briefly before saved code appears
|
|
789
|
+
const currentValue = isLoadingParameters ? (value ?? '') : (value !== undefined ? value : parameter.default);
|
|
790
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
791
|
+
const [dynamicOptions, setDynamicOptions] = useState<INodePropertyOption[]>([]);
|
|
792
|
+
const [nodeParameters, setNodeParameters] = useState<Record<string, any>>({});
|
|
793
|
+
|
|
794
|
+
const { selectedNode } = useAppStore();
|
|
795
|
+
const { getNodeParameters } = useWebSocket();
|
|
796
|
+
const { getStoredApiKey, hasStoredKey, getStoredModels } = useApiKeys();
|
|
797
|
+
|
|
798
|
+
// Don't render hidden parameters
|
|
799
|
+
if (parameter.type === 'hidden') {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Load node parameters for expression resolution
|
|
804
|
+
useEffect(() => {
|
|
805
|
+
const loadParameters = async () => {
|
|
806
|
+
if (selectedNode?.id) {
|
|
807
|
+
const result = await getNodeParameters(selectedNode.id);
|
|
808
|
+
if (result?.parameters) setNodeParameters(result.parameters);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
loadParameters();
|
|
812
|
+
}, [selectedNode?.id, getNodeParameters]);
|
|
813
|
+
|
|
814
|
+
// Auto-load stored API key and models when provider changes
|
|
815
|
+
// Use ref to track previous provider to prevent infinite loops
|
|
816
|
+
const prevProviderRef = React.useRef<string | null>(null);
|
|
817
|
+
// Track if we've done initial auto-select after parameters loaded
|
|
818
|
+
const hasAutoSelectedRef = React.useRef(false);
|
|
819
|
+
|
|
820
|
+
// Reset auto-select tracking when node changes
|
|
821
|
+
useEffect(() => {
|
|
822
|
+
hasAutoSelectedRef.current = false;
|
|
823
|
+
prevProviderRef.current = null;
|
|
824
|
+
}, [selectedNode?.id]);
|
|
825
|
+
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
const loadStoredKeyForProvider = async () => {
|
|
828
|
+
// Only run for apiKey or model parameters
|
|
829
|
+
if (parameter.name !== 'apiKey' && parameter.name !== 'model') return;
|
|
830
|
+
|
|
831
|
+
// Don't run while parameters are still loading from database
|
|
832
|
+
if (isLoadingParameters) return;
|
|
833
|
+
|
|
834
|
+
// Get provider from allParameters or derive from node type
|
|
835
|
+
let provider = allParameters?.provider;
|
|
836
|
+
if (!provider && selectedNode) {
|
|
837
|
+
const nodeType = selectedNode.type || selectedNode.data?.nodeType;
|
|
838
|
+
if (nodeType) {
|
|
839
|
+
provider = NODE_TYPE_TO_PROVIDER[nodeType];
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (!provider) return;
|
|
843
|
+
|
|
844
|
+
// Distinguish between initial load (prevProvider was null) and actual user-initiated provider change
|
|
845
|
+
// On initial load: respect saved model if it exists
|
|
846
|
+
// On provider change: reset to first model to prevent mismatched provider/model
|
|
847
|
+
const isInitialLoad = prevProviderRef.current === null;
|
|
848
|
+
const isActualProviderChange = !isInitialLoad && prevProviderRef.current !== provider;
|
|
849
|
+
const shouldAutoSelectModel = parameter.name === 'model' &&
|
|
850
|
+
(isActualProviderChange || isInitialLoad || !hasAutoSelectedRef.current);
|
|
851
|
+
|
|
852
|
+
// Skip if provider hasn't changed (except for initial model load)
|
|
853
|
+
if (!isActualProviderChange && !isInitialLoad && parameter.name !== 'model') return;
|
|
854
|
+
if (!isActualProviderChange && !isInitialLoad && hasAutoSelectedRef.current) return;
|
|
855
|
+
|
|
856
|
+
prevProviderRef.current = provider;
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
const hasKey = await hasStoredKey(provider);
|
|
860
|
+
|
|
861
|
+
if (hasKey) {
|
|
862
|
+
// Auto-load API key for apiKey parameter - always update when provider changes
|
|
863
|
+
if (parameter.name === 'apiKey' && isActualProviderChange) {
|
|
864
|
+
const storedKey = await getStoredApiKey(provider);
|
|
865
|
+
if (storedKey) {
|
|
866
|
+
onChange(storedKey);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Auto-load models for model parameter
|
|
871
|
+
if (shouldAutoSelectModel && selectedNode) {
|
|
872
|
+
const models = await getStoredModels(provider);
|
|
873
|
+
if (models?.length) {
|
|
874
|
+
const modelOptions = DynamicParameterService.createModelOptions(models);
|
|
875
|
+
DynamicParameterService.updateParameterOptions(selectedNode.id, 'model', modelOptions);
|
|
876
|
+
|
|
877
|
+
// Extract model ID (handles both string and object formats)
|
|
878
|
+
const getModelId = (model: any) => typeof model === 'string' ? model : model.id;
|
|
879
|
+
const firstModelId = getModelId(models[0]);
|
|
880
|
+
|
|
881
|
+
// When user actively changes provider, reset to first model
|
|
882
|
+
// to prevent mismatched provider/model combinations (e.g., OpenAI model with Anthropic provider)
|
|
883
|
+
if (isActualProviderChange) {
|
|
884
|
+
onChange(firstModelId);
|
|
885
|
+
} else {
|
|
886
|
+
// Initial load or no provider change - only auto-select if no saved model exists
|
|
887
|
+
const savedModel = value || allParameters?.model;
|
|
888
|
+
if (!savedModel || savedModel === '') {
|
|
889
|
+
onChange(firstModelId);
|
|
890
|
+
}
|
|
891
|
+
// If saved model exists, keep it (don't call onChange)
|
|
892
|
+
}
|
|
893
|
+
hasAutoSelectedRef.current = true;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
897
|
+
// No stored key for this provider - clear the fields
|
|
898
|
+
if (parameter.name === 'apiKey') {
|
|
899
|
+
onChange('');
|
|
900
|
+
}
|
|
901
|
+
if (parameter.name === 'model') {
|
|
902
|
+
onChange('');
|
|
903
|
+
hasAutoSelectedRef.current = true;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.warn('Error loading stored key info:', error);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
loadStoredKeyForProvider();
|
|
912
|
+
}, [allParameters?.provider, parameter.name, hasStoredKey, getStoredApiKey, getStoredModels, selectedNode?.id, selectedNode?.type, onChange, isLoadingParameters, value, allParameters?.model]);
|
|
913
|
+
|
|
914
|
+
// Merge database params with current form params (current takes precedence)
|
|
915
|
+
const resolvedParameters = { ...nodeParameters, ...allParameters };
|
|
916
|
+
|
|
917
|
+
// Helper functions to get values from both interface types
|
|
918
|
+
const getMin = () => (parameter as any).min || (parameter as any).typeOptions?.minValue || 0;
|
|
919
|
+
const getMax = () => (parameter as any).max || (parameter as any).typeOptions?.maxValue || 100;
|
|
920
|
+
const getStep = () => (parameter as any).step || (parameter as any).typeOptions?.numberStepSize || 1;
|
|
921
|
+
|
|
922
|
+
// Load dynamic options based on loadOptionsMethod
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
const loadDynamicOptions = async () => {
|
|
925
|
+
if (!selectedNode || !isINodeProperties(parameter) || !parameter.typeOptions?.loadOptionsMethod) return;
|
|
926
|
+
|
|
927
|
+
const dependsOn = parameter.typeOptions.loadOptionsDependsOn || [];
|
|
928
|
+
const allParamsResolved = { ...nodeParameters, ...allParameters };
|
|
929
|
+
|
|
930
|
+
// Check if all dependencies are satisfied
|
|
931
|
+
const hasAllDependencies = dependsOn.every((dep: string) => allParamsResolved[dep]);
|
|
932
|
+
if (dependsOn.length > 0 && !hasAllDependencies) return;
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
// Get the node definition to access methods
|
|
936
|
+
const nodeType = selectedNode.data?.nodeType || selectedNode.type;
|
|
937
|
+
const nodeDef = nodeType ? nodeDefinitions[nodeType] : null;
|
|
938
|
+
|
|
939
|
+
if (nodeDef?.methods?.loadOptions?.[parameter.typeOptions.loadOptionsMethod]) {
|
|
940
|
+
const loadMethod = nodeDef.methods.loadOptions[parameter.typeOptions.loadOptionsMethod];
|
|
941
|
+
|
|
942
|
+
// Create context for the load method
|
|
943
|
+
const context = {
|
|
944
|
+
getCurrentNodeParameter: (paramName: string) => allParamsResolved[paramName]
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// Call the load method with context
|
|
948
|
+
const options = await loadMethod.call(context);
|
|
949
|
+
setDynamicOptions(options);
|
|
950
|
+
|
|
951
|
+
// Also update the DynamicParameterService for consistency
|
|
952
|
+
DynamicParameterService.updateParameterOptions(selectedNode.id, parameter.name, options);
|
|
953
|
+
|
|
954
|
+
// Auto-select first option if current value is empty and options are available
|
|
955
|
+
if (options.length > 0 && (!currentValue || currentValue === '')) {
|
|
956
|
+
console.log(`[ParameterRenderer] Auto-selecting first option for ${parameter.name}:`, options[0].value);
|
|
957
|
+
onChange(options[0].value);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
console.error('Error loading dynamic options:', error);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
loadDynamicOptions();
|
|
966
|
+
}, [selectedNode?.id, isINodeProperties(parameter) && parameter.typeOptions?.loadOptionsMethod, nodeParameters, allParameters, parameter.name]);
|
|
967
|
+
|
|
968
|
+
// Load default parameters for Android service nodes when service_id or action changes
|
|
969
|
+
useEffect(() => {
|
|
970
|
+
const loadDefaultParameters = async () => {
|
|
971
|
+
if (!selectedNode || parameter.name !== 'parameters') return;
|
|
972
|
+
|
|
973
|
+
const nodeType = selectedNode.data?.nodeType || selectedNode.type;
|
|
974
|
+
if (!ANDROID_SERVICE_NODE_TYPES.includes(nodeType)) return;
|
|
975
|
+
|
|
976
|
+
// Merge database params with current form params (current takes precedence)
|
|
977
|
+
const allParamsResolved = { ...nodeParameters, ...allParameters };
|
|
978
|
+
const serviceId = allParamsResolved.service_id;
|
|
979
|
+
const action = allParamsResolved.action;
|
|
980
|
+
|
|
981
|
+
if (!serviceId || !action) {
|
|
982
|
+
console.log('[AndroidService] Skipping - missing serviceId or action:', { serviceId, action });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
console.log('[AndroidService] Fetching default parameters for:', { serviceId, action });
|
|
988
|
+
const response = await fetch(`${API_CONFIG.PYTHON_BASE_URL}/api/android/services/${serviceId}/actions/${action}/parameters`, {
|
|
989
|
+
credentials: 'include'
|
|
990
|
+
});
|
|
991
|
+
const data = await response.json();
|
|
992
|
+
console.log('[AndroidService] Default parameters response:', data);
|
|
993
|
+
|
|
994
|
+
if (data.success && data.default_parameters) {
|
|
995
|
+
// Always update with new defaults when service/action changes
|
|
996
|
+
console.log('[AndroidService] Setting parameters to:', data.default_parameters);
|
|
997
|
+
onChange(data.default_parameters);
|
|
998
|
+
}
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
console.error('[AndroidService] Error loading default parameters:', error);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
loadDefaultParameters();
|
|
1005
|
+
}, [
|
|
1006
|
+
selectedNode?.id,
|
|
1007
|
+
parameter.name,
|
|
1008
|
+
allParameters?.service_id,
|
|
1009
|
+
allParameters?.action,
|
|
1010
|
+
nodeParameters?.service_id,
|
|
1011
|
+
nodeParameters?.action
|
|
1012
|
+
]);
|
|
1013
|
+
|
|
1014
|
+
// Subscribe to dynamic parameter updates
|
|
1015
|
+
useEffect(() => {
|
|
1016
|
+
if (!selectedNode) return;
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
const unsubscribe = DynamicParameterService.subscribe((nodeId, parameterName, options) => {
|
|
1020
|
+
|
|
1021
|
+
if (nodeId === selectedNode.id && parameterName === parameter.name) {
|
|
1022
|
+
setDynamicOptions(options);
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// Check for existing dynamic options
|
|
1027
|
+
const existingOptions = DynamicParameterService.getParameterOptions(selectedNode.id, parameter.name);
|
|
1028
|
+
|
|
1029
|
+
if (existingOptions) {
|
|
1030
|
+
setDynamicOptions(existingOptions);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return unsubscribe;
|
|
1034
|
+
}, [selectedNode?.id, parameter.name]);
|
|
1035
|
+
|
|
1036
|
+
// Handle API key validation success
|
|
1037
|
+
const handleApiKeyValidationSuccess = (models: string[]) => {
|
|
1038
|
+
|
|
1039
|
+
if (!selectedNode) {
|
|
1040
|
+
console.warn('ParameterRenderer: No selected node for dynamic options update');
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Always update the 'model' parameter with dynamic options when API key validation succeeds
|
|
1045
|
+
// This callback can be triggered from any parameter (usually the apiKey parameter)
|
|
1046
|
+
const modelOptions = DynamicParameterService.createModelOptions(models);
|
|
1047
|
+
DynamicParameterService.updateParameterOptions(selectedNode.id, 'model', modelOptions);
|
|
1048
|
+
|
|
1049
|
+
// If this callback is triggered from the model parameter itself and it's empty, auto-select first model
|
|
1050
|
+
if (parameter.name === 'model' && !currentValue && models.length > 0) {
|
|
1051
|
+
onChange(models[0]);
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
1056
|
+
e.preventDefault();
|
|
1057
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
1058
|
+
setIsDragOver(true);
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
1062
|
+
e.preventDefault();
|
|
1063
|
+
setIsDragOver(false);
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
1067
|
+
e.preventDefault();
|
|
1068
|
+
setIsDragOver(false);
|
|
1069
|
+
|
|
1070
|
+
// Check if this is a coordinate parameter (lat/lng) for special handling
|
|
1071
|
+
const paramName = parameter.name.toLowerCase();
|
|
1072
|
+
const isCoordinate = paramName.includes('lat') || paramName.includes('lng') ||
|
|
1073
|
+
paramName.includes('lon') || paramName === 'latitude' ||
|
|
1074
|
+
paramName === 'longitude';
|
|
1075
|
+
|
|
1076
|
+
// Try to get JSON data first (from connected node outputs)
|
|
1077
|
+
const jsonData = e.dataTransfer.getData('application/json');
|
|
1078
|
+
if (jsonData) {
|
|
1079
|
+
try {
|
|
1080
|
+
const parsedData = JSON.parse(jsonData);
|
|
1081
|
+
if (parsedData.type === 'nodeVariable') {
|
|
1082
|
+
// For coordinate parameters, try to extract actual numeric value from connected node data
|
|
1083
|
+
if (isCoordinate && typeof parsedData.dataType === 'string' && parsedData.dataType === 'number') {
|
|
1084
|
+
// Look for actual coordinate values in global execution data
|
|
1085
|
+
// This is a simplified approach - in production you'd want more robust data access
|
|
1086
|
+
|
|
1087
|
+
// For now, use template string but mark it for coordinate processing
|
|
1088
|
+
const existingValue = currentValue || '';
|
|
1089
|
+
const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
|
|
1090
|
+
const newValue = existingValue + (needsSpace ? ' ' : '') + parsedData.variableTemplate;
|
|
1091
|
+
onChange(newValue);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Handle variable template data - use the template string
|
|
1096
|
+
const existingValue = currentValue || '';
|
|
1097
|
+
// Add smart spacing - add space if existing content doesn't end with space
|
|
1098
|
+
const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
|
|
1099
|
+
const newValue = existingValue + (needsSpace ? ' ' : '') + parsedData.variableTemplate;
|
|
1100
|
+
onChange(newValue);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (parsedData.type === 'nodeOutput') {
|
|
1104
|
+
// Handle node output data - use the actual value for direct mapping
|
|
1105
|
+
onChange(parsedData.value);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
console.warn('Failed to parse JSON drag data:', err);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Fallback to existing text/plain format (OutputPanel drag-drop)
|
|
1114
|
+
const data = e.dataTransfer.getData('text/plain');
|
|
1115
|
+
if (data && data.startsWith('{{') && data.endsWith('}}')) {
|
|
1116
|
+
// For coordinate parameters, allow template strings but process them appropriately
|
|
1117
|
+
if (isCoordinate) {
|
|
1118
|
+
onChange(data); // Set the template directly for coordinate resolution
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Append to existing content instead of replacing
|
|
1123
|
+
const existingValue = currentValue || '';
|
|
1124
|
+
// Add smart spacing - add space if existing content doesn't end with space
|
|
1125
|
+
const needsSpace = existingValue && !existingValue.endsWith(' ') && existingValue.length > 0;
|
|
1126
|
+
const newValue = existingValue + (needsSpace ? ' ' : '') + data;
|
|
1127
|
+
onChange(newValue);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
const renderInput = () => {
|
|
1132
|
+
switch (parameter.type) {
|
|
1133
|
+
case 'string':
|
|
1134
|
+
// Check if this should be a textarea based on typeOptions.rows
|
|
1135
|
+
const shouldUseTextarea = (parameter as any).typeOptions?.rows > 1;
|
|
1136
|
+
// Check if this should be a password field
|
|
1137
|
+
const isPassword = (parameter as any).typeOptions?.password;
|
|
1138
|
+
// Check if this is a code editor
|
|
1139
|
+
const isCodeEditor = (parameter as any).typeOptions?.editor === 'code';
|
|
1140
|
+
|
|
1141
|
+
if (shouldUseTextarea) {
|
|
1142
|
+
// Use CodeEditor for code editing
|
|
1143
|
+
if (isCodeEditor) {
|
|
1144
|
+
// Show loading state while parameters are being fetched
|
|
1145
|
+
if (isLoadingParameters) {
|
|
1146
|
+
return (
|
|
1147
|
+
<div style={{
|
|
1148
|
+
height: '100%',
|
|
1149
|
+
minHeight: '200px',
|
|
1150
|
+
display: 'flex',
|
|
1151
|
+
alignItems: 'center',
|
|
1152
|
+
justifyContent: 'center',
|
|
1153
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
1154
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1155
|
+
borderRadius: theme.borderRadius.md,
|
|
1156
|
+
color: theme.colors.textMuted,
|
|
1157
|
+
fontSize: '14px'
|
|
1158
|
+
}}>
|
|
1159
|
+
Loading code...
|
|
1160
|
+
</div>
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
// Get language from typeOptions or default to python
|
|
1164
|
+
const codeLanguage = (parameter as any).typeOptions?.editorLanguage || 'python';
|
|
1165
|
+
return (
|
|
1166
|
+
<CodeEditor
|
|
1167
|
+
value={currentValue || ''}
|
|
1168
|
+
onChange={onChange}
|
|
1169
|
+
language={codeLanguage}
|
|
1170
|
+
placeholder={parameter.placeholder}
|
|
1171
|
+
/>
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Regular textarea for non-code
|
|
1176
|
+
return (
|
|
1177
|
+
<textarea
|
|
1178
|
+
value={currentValue || ''}
|
|
1179
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1180
|
+
placeholder={parameter.placeholder}
|
|
1181
|
+
rows={(parameter as any).typeOptions?.rows || 3}
|
|
1182
|
+
spellCheck={true}
|
|
1183
|
+
onDragOver={handleDragOver}
|
|
1184
|
+
onDragLeave={handleDragLeave}
|
|
1185
|
+
onDrop={handleDrop}
|
|
1186
|
+
style={{
|
|
1187
|
+
width: '100%',
|
|
1188
|
+
padding: '10px 12px',
|
|
1189
|
+
border: isDragOver ? `2px solid ${theme.accent.cyan}` : `1px solid ${theme.colors.border}`,
|
|
1190
|
+
borderRadius: '6px',
|
|
1191
|
+
fontSize: '14px',
|
|
1192
|
+
backgroundColor: isDragOver ? `${theme.accent.cyan}10` : theme.colors.backgroundAlt,
|
|
1193
|
+
color: currentValue && currentValue.includes('{{') ? theme.accent.yellow : theme.colors.text,
|
|
1194
|
+
outline: 'none',
|
|
1195
|
+
transition: 'all 0.2s ease',
|
|
1196
|
+
fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
|
|
1197
|
+
resize: 'vertical',
|
|
1198
|
+
minHeight: '80px',
|
|
1199
|
+
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)',
|
|
1200
|
+
lineHeight: '1.5'
|
|
1201
|
+
}}
|
|
1202
|
+
onFocus={(e) => {
|
|
1203
|
+
e.target.style.borderColor = theme.accent.cyan;
|
|
1204
|
+
e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
|
|
1205
|
+
}}
|
|
1206
|
+
onBlur={(e) => {
|
|
1207
|
+
e.target.style.borderColor = theme.colors.border;
|
|
1208
|
+
e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
|
|
1209
|
+
}}
|
|
1210
|
+
/>
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Check if this parameter has API key validation
|
|
1215
|
+
const validationArray = (parameter as any).validation;
|
|
1216
|
+
const apiKeyValidation = validationArray?.find((v: any) => v.type === 'apiKey' && v.showValidateButton);
|
|
1217
|
+
|
|
1218
|
+
if (apiKeyValidation) {
|
|
1219
|
+
// Resolve provider expression if it's a template like {{ $parameter["provider"] }}
|
|
1220
|
+
let resolvedProvider = apiKeyValidation.provider;
|
|
1221
|
+
if (typeof resolvedProvider === 'string' && resolvedProvider.includes('$parameter[')) {
|
|
1222
|
+
// Extract parameter name from expression like {{ $parameter["provider"] }}
|
|
1223
|
+
const match = resolvedProvider.match(/\$parameter\["([^"]+)"\]|\$parameter\['([^']+)'\]/);
|
|
1224
|
+
if (match) {
|
|
1225
|
+
const paramName = match[1] || match[2];
|
|
1226
|
+
resolvedProvider = resolvedParameters[paramName] || resolvedProvider;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const resolvedValidationConfig = {
|
|
1231
|
+
...apiKeyValidation,
|
|
1232
|
+
provider: resolvedProvider
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
return (
|
|
1236
|
+
<APIKeyValidator
|
|
1237
|
+
value={currentValue || ''}
|
|
1238
|
+
onChange={onChange}
|
|
1239
|
+
placeholder={parameter.placeholder}
|
|
1240
|
+
validationConfig={resolvedValidationConfig}
|
|
1241
|
+
onValidationSuccess={handleApiKeyValidationSuccess}
|
|
1242
|
+
isDragOver={isDragOver}
|
|
1243
|
+
onDragOver={handleDragOver}
|
|
1244
|
+
onDragLeave={handleDragLeave}
|
|
1245
|
+
onDrop={handleDrop}
|
|
1246
|
+
/>
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Check if this parameter has dynamic options (like models after API key validation)
|
|
1251
|
+
|
|
1252
|
+
if (dynamicOptions.length > 0 && parameter.type === 'string') {
|
|
1253
|
+
// Check if OpenRouter (has [FREE] tagged models)
|
|
1254
|
+
const hasFreeModels = dynamicOptions.some(opt => String(opt.label || opt.value).includes('[FREE]'));
|
|
1255
|
+
|
|
1256
|
+
if (hasFreeModels) {
|
|
1257
|
+
// Group into Free and Paid for OpenRouter using native select with optgroup
|
|
1258
|
+
const freeModels = dynamicOptions.filter(opt => String(opt.label || opt.value).includes('[FREE]'));
|
|
1259
|
+
const paidModels = dynamicOptions.filter(opt => !String(opt.label || opt.value).includes('[FREE]'));
|
|
1260
|
+
|
|
1261
|
+
return (
|
|
1262
|
+
<select
|
|
1263
|
+
value={currentValue || ''}
|
|
1264
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1265
|
+
style={{
|
|
1266
|
+
width: '100%',
|
|
1267
|
+
padding: theme.spacing.sm,
|
|
1268
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1269
|
+
borderRadius: theme.borderRadius.md,
|
|
1270
|
+
fontSize: theme.fontSize.sm,
|
|
1271
|
+
outline: 'none',
|
|
1272
|
+
transition: 'border-color 0.2s ease',
|
|
1273
|
+
cursor: 'pointer',
|
|
1274
|
+
backgroundColor: theme.colors.background,
|
|
1275
|
+
color: theme.colors.text,
|
|
1276
|
+
fontFamily: 'system-ui, sans-serif'
|
|
1277
|
+
}}
|
|
1278
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
1279
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
1280
|
+
>
|
|
1281
|
+
{!currentValue && (
|
|
1282
|
+
<option value="" disabled>
|
|
1283
|
+
Select a model...
|
|
1284
|
+
</option>
|
|
1285
|
+
)}
|
|
1286
|
+
<optgroup label={`Free Models (${freeModels.length})`}>
|
|
1287
|
+
{freeModels.map((option) => (
|
|
1288
|
+
<option key={String(option.value)} value={String(option.value)}>
|
|
1289
|
+
{option.label || option.name || String(option.value)}
|
|
1290
|
+
</option>
|
|
1291
|
+
))}
|
|
1292
|
+
</optgroup>
|
|
1293
|
+
<optgroup label={`Paid Models (${paidModels.length})`}>
|
|
1294
|
+
{paidModels.map((option) => (
|
|
1295
|
+
<option key={String(option.value)} value={String(option.value)}>
|
|
1296
|
+
{option.label || option.name || String(option.value)}
|
|
1297
|
+
</option>
|
|
1298
|
+
))}
|
|
1299
|
+
</optgroup>
|
|
1300
|
+
</select>
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Use native select for non-OpenRouter (original working code)
|
|
1305
|
+
return (
|
|
1306
|
+
<select
|
|
1307
|
+
value={currentValue || ''}
|
|
1308
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1309
|
+
style={{
|
|
1310
|
+
width: '100%',
|
|
1311
|
+
padding: '8px 12px',
|
|
1312
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1313
|
+
borderRadius: '6px',
|
|
1314
|
+
fontSize: '14px',
|
|
1315
|
+
outline: 'none',
|
|
1316
|
+
transition: 'border-color 0.2s ease',
|
|
1317
|
+
cursor: 'pointer',
|
|
1318
|
+
backgroundColor: theme.colors.background,
|
|
1319
|
+
color: theme.colors.text,
|
|
1320
|
+
fontFamily: 'system-ui, sans-serif'
|
|
1321
|
+
}}
|
|
1322
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
1323
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
1324
|
+
>
|
|
1325
|
+
{!currentValue && (
|
|
1326
|
+
<option value="" disabled>
|
|
1327
|
+
Select a model...
|
|
1328
|
+
</option>
|
|
1329
|
+
)}
|
|
1330
|
+
{dynamicOptions.map((option) => (
|
|
1331
|
+
<option key={String(option.value)} value={String(option.value)}>
|
|
1332
|
+
{option.label || option.name || String(option.value)}
|
|
1333
|
+
</option>
|
|
1334
|
+
))}
|
|
1335
|
+
</select>
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Log why we're not using dynamic options for this parameter
|
|
1340
|
+
if (parameter.type === 'string' && parameter.name === 'model') {
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Special case for group_id parameter - add Load Groups button
|
|
1344
|
+
if (parameter.name === 'group_id') {
|
|
1345
|
+
const storedGroupName = allParameters?.group_name || '';
|
|
1346
|
+
return (
|
|
1347
|
+
<GroupIdSelector
|
|
1348
|
+
value={currentValue || ''}
|
|
1349
|
+
onChange={onChange}
|
|
1350
|
+
onNameChange={(name) => onParameterChange?.('group_name', name)}
|
|
1351
|
+
storedName={storedGroupName}
|
|
1352
|
+
placeholder={parameter.placeholder}
|
|
1353
|
+
theme={theme}
|
|
1354
|
+
isDragOver={isDragOver}
|
|
1355
|
+
onDragOver={handleDragOver}
|
|
1356
|
+
onDragLeave={handleDragLeave}
|
|
1357
|
+
onDrop={handleDrop}
|
|
1358
|
+
/>
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Special case for senderNumber parameter - add Load Members button (uses group_id)
|
|
1363
|
+
if (parameter.name === 'senderNumber') {
|
|
1364
|
+
const groupId = resolvedParameters?.group_id || allParameters?.group_id || '';
|
|
1365
|
+
const storedSenderName = allParameters?.sender_name || '';
|
|
1366
|
+
return (
|
|
1367
|
+
<SenderNumberSelector
|
|
1368
|
+
value={currentValue || ''}
|
|
1369
|
+
onChange={onChange}
|
|
1370
|
+
onNameChange={(name) => onParameterChange?.('sender_name', name)}
|
|
1371
|
+
storedName={storedSenderName}
|
|
1372
|
+
placeholder={parameter.placeholder}
|
|
1373
|
+
theme={theme}
|
|
1374
|
+
isDragOver={isDragOver}
|
|
1375
|
+
onDragOver={handleDragOver}
|
|
1376
|
+
onDragLeave={handleDragLeave}
|
|
1377
|
+
onDrop={handleDrop}
|
|
1378
|
+
groupId={groupId}
|
|
1379
|
+
/>
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return (
|
|
1384
|
+
<input
|
|
1385
|
+
type={isPassword ? "password" : "text"}
|
|
1386
|
+
value={currentValue || ''}
|
|
1387
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1388
|
+
placeholder={parameter.placeholder}
|
|
1389
|
+
onDragOver={handleDragOver}
|
|
1390
|
+
onDragLeave={handleDragLeave}
|
|
1391
|
+
onDrop={handleDrop}
|
|
1392
|
+
style={{
|
|
1393
|
+
width: '100%',
|
|
1394
|
+
padding: '10px 12px',
|
|
1395
|
+
border: isDragOver ? `2px solid ${theme.accent.cyan}` : `1px solid ${theme.colors.border}`,
|
|
1396
|
+
borderRadius: '6px',
|
|
1397
|
+
fontSize: '14px',
|
|
1398
|
+
backgroundColor: isDragOver ? `${theme.accent.cyan}10` : theme.colors.backgroundAlt,
|
|
1399
|
+
color: currentValue && currentValue.includes('{{') ? theme.accent.yellow : theme.colors.text,
|
|
1400
|
+
outline: 'none',
|
|
1401
|
+
transition: 'all 0.2s ease',
|
|
1402
|
+
fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif',
|
|
1403
|
+
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)'
|
|
1404
|
+
}}
|
|
1405
|
+
onFocus={(e) => {
|
|
1406
|
+
e.target.style.borderColor = theme.accent.cyan;
|
|
1407
|
+
e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
|
|
1408
|
+
}}
|
|
1409
|
+
onBlur={(e) => {
|
|
1410
|
+
e.target.style.borderColor = theme.colors.border;
|
|
1411
|
+
e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
|
|
1412
|
+
}}
|
|
1413
|
+
/>
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
case 'number':
|
|
1417
|
+
|
|
1418
|
+
return (
|
|
1419
|
+
<input
|
|
1420
|
+
type="number"
|
|
1421
|
+
value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
|
|
1422
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
1423
|
+
min={getMin()}
|
|
1424
|
+
max={getMax()}
|
|
1425
|
+
step={getStep()}
|
|
1426
|
+
onDragOver={handleDragOver}
|
|
1427
|
+
onDragLeave={handleDragLeave}
|
|
1428
|
+
onDrop={handleDrop}
|
|
1429
|
+
style={{
|
|
1430
|
+
width: '100%',
|
|
1431
|
+
padding: '8px 12px',
|
|
1432
|
+
border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
|
|
1433
|
+
borderRadius: '6px',
|
|
1434
|
+
fontSize: '14px',
|
|
1435
|
+
backgroundColor: isDragOver ? theme.colors.focusRing : theme.colors.background,
|
|
1436
|
+
color: theme.colors.text,
|
|
1437
|
+
outline: 'none',
|
|
1438
|
+
transition: 'all 0.2s ease',
|
|
1439
|
+
fontFamily: 'system-ui, sans-serif'
|
|
1440
|
+
}}
|
|
1441
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
1442
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
1443
|
+
/>
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
case 'boolean':
|
|
1447
|
+
return (
|
|
1448
|
+
<label style={{
|
|
1449
|
+
display: 'flex',
|
|
1450
|
+
alignItems: 'center',
|
|
1451
|
+
gap: '8px',
|
|
1452
|
+
cursor: 'pointer',
|
|
1453
|
+
fontSize: '14px',
|
|
1454
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1455
|
+
color: theme.colors.text
|
|
1456
|
+
}}>
|
|
1457
|
+
<input
|
|
1458
|
+
type="checkbox"
|
|
1459
|
+
checked={currentValue || false}
|
|
1460
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
1461
|
+
style={{ width: '16px', height: '16px', accentColor: theme.colors.focus }}
|
|
1462
|
+
/>
|
|
1463
|
+
{parameter.displayName}
|
|
1464
|
+
</label>
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
case 'select':
|
|
1468
|
+
case 'options':
|
|
1469
|
+
// Use dynamic options if available, otherwise use static options
|
|
1470
|
+
const optionsToRender = dynamicOptions.length > 0 ? dynamicOptions : (parameter.options || []);
|
|
1471
|
+
const selectOptions = optionsToRender.filter((option): option is import('../types/INodeProperties').INodePropertyOption =>
|
|
1472
|
+
'value' in option
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
return (
|
|
1476
|
+
<select
|
|
1477
|
+
value={currentValue || parameter.default}
|
|
1478
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1479
|
+
style={{
|
|
1480
|
+
width: '100%',
|
|
1481
|
+
padding: '10px 12px',
|
|
1482
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1483
|
+
borderRadius: '6px',
|
|
1484
|
+
fontSize: '14px',
|
|
1485
|
+
outline: 'none',
|
|
1486
|
+
transition: 'all 0.2s ease',
|
|
1487
|
+
cursor: 'pointer',
|
|
1488
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
1489
|
+
color: theme.colors.text,
|
|
1490
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1491
|
+
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.05)',
|
|
1492
|
+
appearance: 'none',
|
|
1493
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23657b83' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
|
|
1494
|
+
backgroundRepeat: 'no-repeat',
|
|
1495
|
+
backgroundPosition: 'right 12px center',
|
|
1496
|
+
paddingRight: '36px'
|
|
1497
|
+
}}
|
|
1498
|
+
onFocus={(e) => {
|
|
1499
|
+
e.target.style.borderColor = theme.accent.cyan;
|
|
1500
|
+
e.target.style.boxShadow = `0 0 0 3px ${theme.accent.cyan}20, inset 0 1px 2px rgba(0,0,0,0.05)`;
|
|
1501
|
+
}}
|
|
1502
|
+
onBlur={(e) => {
|
|
1503
|
+
e.target.style.borderColor = theme.colors.border;
|
|
1504
|
+
e.target.style.boxShadow = 'inset 0 1px 2px rgba(0,0,0,0.05)';
|
|
1505
|
+
}}
|
|
1506
|
+
>
|
|
1507
|
+
{selectOptions.map((option) => (
|
|
1508
|
+
<option key={String(option.value)} value={String(option.value)}>
|
|
1509
|
+
{option.label || option.name || String(option.value)}
|
|
1510
|
+
</option>
|
|
1511
|
+
))}
|
|
1512
|
+
</select>
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
case 'slider':
|
|
1516
|
+
return (
|
|
1517
|
+
<div>
|
|
1518
|
+
<input
|
|
1519
|
+
type="range"
|
|
1520
|
+
min={getMin()}
|
|
1521
|
+
max={getMax()}
|
|
1522
|
+
step={getStep()}
|
|
1523
|
+
value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
|
|
1524
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
1525
|
+
style={{
|
|
1526
|
+
width: '100%',
|
|
1527
|
+
height: '8px',
|
|
1528
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
1529
|
+
borderRadius: '8px',
|
|
1530
|
+
outline: 'none',
|
|
1531
|
+
accentColor: theme.colors.focus
|
|
1532
|
+
}}
|
|
1533
|
+
/>
|
|
1534
|
+
<div style={{
|
|
1535
|
+
textAlign: 'center',
|
|
1536
|
+
fontSize: '12px',
|
|
1537
|
+
color: theme.colors.textSecondary,
|
|
1538
|
+
marginTop: '4px',
|
|
1539
|
+
fontFamily: 'system-ui, sans-serif'
|
|
1540
|
+
}}>
|
|
1541
|
+
{currentValue !== undefined ? currentValue : (parameter.default || 0)}
|
|
1542
|
+
{parameter.type === 'slider' ? '%' : ''}
|
|
1543
|
+
</div>
|
|
1544
|
+
</div>
|
|
1545
|
+
);
|
|
1546
|
+
|
|
1547
|
+
case 'percentage':
|
|
1548
|
+
return (
|
|
1549
|
+
<div>
|
|
1550
|
+
<input
|
|
1551
|
+
type="range"
|
|
1552
|
+
min={getMin()}
|
|
1553
|
+
max={getMax()}
|
|
1554
|
+
step={getStep()}
|
|
1555
|
+
value={currentValue !== undefined ? currentValue : (parameter.default || 0)}
|
|
1556
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
1557
|
+
style={{
|
|
1558
|
+
width: '100%',
|
|
1559
|
+
height: '8px',
|
|
1560
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
1561
|
+
borderRadius: '8px',
|
|
1562
|
+
outline: 'none',
|
|
1563
|
+
accentColor: theme.colors.success
|
|
1564
|
+
}}
|
|
1565
|
+
/>
|
|
1566
|
+
<div style={{
|
|
1567
|
+
textAlign: 'center',
|
|
1568
|
+
fontSize: '12px',
|
|
1569
|
+
color: theme.colors.textSecondary,
|
|
1570
|
+
marginTop: '4px',
|
|
1571
|
+
fontFamily: 'system-ui, sans-serif'
|
|
1572
|
+
}}>
|
|
1573
|
+
{currentValue !== undefined ? currentValue : (parameter.default || 0)}%
|
|
1574
|
+
</div>
|
|
1575
|
+
</div>
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
case 'text':
|
|
1579
|
+
return (
|
|
1580
|
+
<input
|
|
1581
|
+
type="text"
|
|
1582
|
+
value={currentValue || ''}
|
|
1583
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1584
|
+
placeholder={parameter.placeholder}
|
|
1585
|
+
onDragOver={handleDragOver}
|
|
1586
|
+
onDragLeave={handleDragLeave}
|
|
1587
|
+
onDrop={handleDrop}
|
|
1588
|
+
style={{
|
|
1589
|
+
width: '100%',
|
|
1590
|
+
padding: '8px 12px',
|
|
1591
|
+
border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
|
|
1592
|
+
borderRadius: '6px',
|
|
1593
|
+
fontSize: '14px',
|
|
1594
|
+
backgroundColor: isDragOver ? theme.colors.focusRing : theme.colors.background,
|
|
1595
|
+
color: currentValue && currentValue.includes('{{') ? theme.colors.templateVariable : theme.colors.text,
|
|
1596
|
+
outline: 'none',
|
|
1597
|
+
transition: 'all 0.2s ease',
|
|
1598
|
+
fontFamily: currentValue && currentValue.includes('{{') ? 'monospace' : 'system-ui, sans-serif'
|
|
1599
|
+
}}
|
|
1600
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
1601
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
1602
|
+
/>
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
case 'file':
|
|
1606
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
1607
|
+
|
|
1608
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1609
|
+
const file = e.target.files?.[0];
|
|
1610
|
+
if (!file) return;
|
|
1611
|
+
|
|
1612
|
+
const reader = new FileReader();
|
|
1613
|
+
reader.onload = () => {
|
|
1614
|
+
const base64 = (reader.result as string).split(',')[1]; // Remove data:mime;base64, prefix
|
|
1615
|
+
// Store as object with base64 data, filename, and mime type
|
|
1616
|
+
onChange({
|
|
1617
|
+
type: 'upload',
|
|
1618
|
+
data: base64,
|
|
1619
|
+
filename: file.name,
|
|
1620
|
+
mimeType: file.type || 'application/octet-stream'
|
|
1621
|
+
});
|
|
1622
|
+
};
|
|
1623
|
+
reader.readAsDataURL(file);
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
const isUploadedFile = currentValue && typeof currentValue === 'object' && currentValue.type === 'upload';
|
|
1627
|
+
|
|
1628
|
+
// Determine file accept type based on context (e.g., messageType for WhatsApp)
|
|
1629
|
+
const getFileAcceptType = () => {
|
|
1630
|
+
const messageType = allParameters?.messageType;
|
|
1631
|
+
if (messageType) {
|
|
1632
|
+
switch (messageType) {
|
|
1633
|
+
case 'image':
|
|
1634
|
+
return 'image/*';
|
|
1635
|
+
case 'video':
|
|
1636
|
+
return 'video/*';
|
|
1637
|
+
case 'audio':
|
|
1638
|
+
return 'audio/*,.ogg,.opus,.mp3,.wav,.m4a';
|
|
1639
|
+
case 'document':
|
|
1640
|
+
return '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.zip,.rar';
|
|
1641
|
+
case 'sticker':
|
|
1642
|
+
return 'image/webp,.webp';
|
|
1643
|
+
default:
|
|
1644
|
+
return (parameter as any).typeOptions?.accept || '*/*';
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return (parameter as any).typeOptions?.accept || '*/*';
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
return (
|
|
1651
|
+
<div>
|
|
1652
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
1653
|
+
<input
|
|
1654
|
+
type="text"
|
|
1655
|
+
value={isUploadedFile ? `[Uploaded] ${currentValue.filename}` : (currentValue || '')}
|
|
1656
|
+
onChange={(e) => onChange(e.target.value)}
|
|
1657
|
+
placeholder={parameter.placeholder || 'Enter file path or upload'}
|
|
1658
|
+
onDragOver={handleDragOver}
|
|
1659
|
+
onDragLeave={handleDragLeave}
|
|
1660
|
+
onDrop={handleDrop}
|
|
1661
|
+
readOnly={isUploadedFile}
|
|
1662
|
+
style={{
|
|
1663
|
+
flex: 1,
|
|
1664
|
+
padding: '8px 12px',
|
|
1665
|
+
border: isDragOver ? `2px solid ${theme.colors.focus}` : `1px solid ${theme.colors.border}`,
|
|
1666
|
+
borderRadius: '6px',
|
|
1667
|
+
fontSize: '13px',
|
|
1668
|
+
backgroundColor: isUploadedFile ? theme.colors.backgroundAlt : (isDragOver ? theme.colors.focusRing : theme.colors.background),
|
|
1669
|
+
color: isUploadedFile ? theme.colors.success : (currentValue && currentValue.includes?.('{{') ? theme.colors.templateVariable : theme.colors.text),
|
|
1670
|
+
outline: 'none',
|
|
1671
|
+
transition: 'all 0.2s ease',
|
|
1672
|
+
fontFamily: 'monospace'
|
|
1673
|
+
}}
|
|
1674
|
+
onFocus={(e) => e.target.style.borderColor = theme.colors.focus}
|
|
1675
|
+
onBlur={(e) => e.target.style.borderColor = theme.colors.border}
|
|
1676
|
+
/>
|
|
1677
|
+
<input
|
|
1678
|
+
key={`file-input-${allParameters?.messageType || 'default'}`}
|
|
1679
|
+
ref={fileInputRef}
|
|
1680
|
+
type="file"
|
|
1681
|
+
onChange={handleFileUpload}
|
|
1682
|
+
style={{ display: 'none' }}
|
|
1683
|
+
accept={getFileAcceptType()}
|
|
1684
|
+
/>
|
|
1685
|
+
<button
|
|
1686
|
+
onClick={() => fileInputRef.current?.click()}
|
|
1687
|
+
style={{
|
|
1688
|
+
padding: '8px 12px',
|
|
1689
|
+
border: `1px solid ${theme.colors.focus}40`,
|
|
1690
|
+
borderRadius: '6px',
|
|
1691
|
+
backgroundColor: `${theme.colors.focus}18`,
|
|
1692
|
+
color: theme.colors.focus,
|
|
1693
|
+
cursor: 'pointer',
|
|
1694
|
+
fontSize: '13px',
|
|
1695
|
+
fontWeight: 600,
|
|
1696
|
+
transition: 'all 0.2s ease',
|
|
1697
|
+
whiteSpace: 'nowrap'
|
|
1698
|
+
}}
|
|
1699
|
+
onMouseEnter={(e) => {
|
|
1700
|
+
e.currentTarget.style.backgroundColor = `${theme.colors.focus}30`;
|
|
1701
|
+
}}
|
|
1702
|
+
onMouseLeave={(e) => {
|
|
1703
|
+
e.currentTarget.style.backgroundColor = `${theme.colors.focus}18`;
|
|
1704
|
+
}}
|
|
1705
|
+
title="Upload file"
|
|
1706
|
+
>
|
|
1707
|
+
Upload
|
|
1708
|
+
</button>
|
|
1709
|
+
{isUploadedFile && (
|
|
1710
|
+
<button
|
|
1711
|
+
onClick={() => {
|
|
1712
|
+
onChange('');
|
|
1713
|
+
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
1714
|
+
}}
|
|
1715
|
+
style={{
|
|
1716
|
+
padding: '8px 10px',
|
|
1717
|
+
border: `1px solid ${theme.colors.error}40`,
|
|
1718
|
+
borderRadius: '6px',
|
|
1719
|
+
backgroundColor: `${theme.colors.error}18`,
|
|
1720
|
+
color: theme.colors.error,
|
|
1721
|
+
cursor: 'pointer',
|
|
1722
|
+
fontSize: '13px',
|
|
1723
|
+
fontWeight: 600,
|
|
1724
|
+
transition: 'all 0.2s ease'
|
|
1725
|
+
}}
|
|
1726
|
+
onMouseEnter={(e) => {
|
|
1727
|
+
e.currentTarget.style.backgroundColor = `${theme.colors.error}30`;
|
|
1728
|
+
}}
|
|
1729
|
+
onMouseLeave={(e) => {
|
|
1730
|
+
e.currentTarget.style.backgroundColor = `${theme.colors.error}18`;
|
|
1731
|
+
}}
|
|
1732
|
+
title="Clear uploaded file"
|
|
1733
|
+
>
|
|
1734
|
+
X
|
|
1735
|
+
</button>
|
|
1736
|
+
)}
|
|
1737
|
+
</div>
|
|
1738
|
+
<div style={{
|
|
1739
|
+
fontSize: '11px',
|
|
1740
|
+
color: theme.colors.textSecondary,
|
|
1741
|
+
marginTop: '4px',
|
|
1742
|
+
fontStyle: 'italic'
|
|
1743
|
+
}}>
|
|
1744
|
+
{isUploadedFile
|
|
1745
|
+
? `Size: ${(currentValue.data.length * 0.75 / 1024).toFixed(1)} KB | Type: ${currentValue.mimeType}`
|
|
1746
|
+
: 'Enter server path or click Upload to select a file'}
|
|
1747
|
+
</div>
|
|
1748
|
+
</div>
|
|
1749
|
+
);
|
|
1750
|
+
|
|
1751
|
+
case 'array':
|
|
1752
|
+
const arrayValue = Array.isArray(currentValue) ? currentValue : [];
|
|
1753
|
+
return (
|
|
1754
|
+
<div>
|
|
1755
|
+
<div style={{
|
|
1756
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1757
|
+
borderRadius: '6px',
|
|
1758
|
+
backgroundColor: theme.colors.background,
|
|
1759
|
+
maxHeight: '120px',
|
|
1760
|
+
overflowY: 'auto'
|
|
1761
|
+
}}>
|
|
1762
|
+
{parameter.options?.map((option) => (
|
|
1763
|
+
<label key={option.value} style={{
|
|
1764
|
+
display: 'flex',
|
|
1765
|
+
alignItems: 'center',
|
|
1766
|
+
gap: '8px',
|
|
1767
|
+
padding: '8px 12px',
|
|
1768
|
+
cursor: 'pointer',
|
|
1769
|
+
fontSize: '14px',
|
|
1770
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1771
|
+
color: theme.colors.text,
|
|
1772
|
+
borderBottom: `1px solid ${theme.colors.border}`
|
|
1773
|
+
}}>
|
|
1774
|
+
<input
|
|
1775
|
+
type="checkbox"
|
|
1776
|
+
checked={arrayValue.includes(option.value)}
|
|
1777
|
+
onChange={(e) => {
|
|
1778
|
+
if (e.target.checked) {
|
|
1779
|
+
onChange([...arrayValue, option.value]);
|
|
1780
|
+
} else {
|
|
1781
|
+
onChange(arrayValue.filter((v: any) => v !== option.value));
|
|
1782
|
+
}
|
|
1783
|
+
}}
|
|
1784
|
+
style={{ width: '16px', height: '16px', accentColor: theme.colors.focus }}
|
|
1785
|
+
/>
|
|
1786
|
+
{option.label}
|
|
1787
|
+
</label>
|
|
1788
|
+
))}
|
|
1789
|
+
</div>
|
|
1790
|
+
<div style={{
|
|
1791
|
+
fontSize: '11px',
|
|
1792
|
+
color: theme.colors.textSecondary,
|
|
1793
|
+
marginTop: '4px'
|
|
1794
|
+
}}>
|
|
1795
|
+
Selected: {arrayValue.length} item{arrayValue.length !== 1 ? 's' : ''}
|
|
1796
|
+
</div>
|
|
1797
|
+
</div>
|
|
1798
|
+
);
|
|
1799
|
+
|
|
1800
|
+
case 'collection':
|
|
1801
|
+
return <CollectionRenderer parameter={parameter} value={currentValue} onChange={onChange} allParameters={allParameters} theme={theme} />;
|
|
1802
|
+
|
|
1803
|
+
case 'fixedCollection':
|
|
1804
|
+
return <FixedCollectionRenderer parameter={parameter} value={currentValue} onChange={onChange} allParameters={allParameters} theme={theme} />;
|
|
1805
|
+
|
|
1806
|
+
case 'notice':
|
|
1807
|
+
// Info/notice display - shows informational text without input
|
|
1808
|
+
return (
|
|
1809
|
+
<div style={{
|
|
1810
|
+
padding: '10px 12px',
|
|
1811
|
+
backgroundColor: theme.colors.backgroundAlt,
|
|
1812
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1813
|
+
borderRadius: '6px',
|
|
1814
|
+
fontSize: '13px',
|
|
1815
|
+
color: theme.colors.textSecondary,
|
|
1816
|
+
lineHeight: '1.5'
|
|
1817
|
+
}}>
|
|
1818
|
+
{parameter.default || parameter.description || ''}
|
|
1819
|
+
</div>
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
default:
|
|
1823
|
+
return <div style={{ color: theme.colors.error, fontSize: '14px', padding: '8px 12px', backgroundColor: `${theme.colors.error}15`, border: `1px solid ${theme.colors.error}30`, borderRadius: '6px' }}>Unsupported parameter type: {parameter.type}</div>;
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
return (
|
|
1828
|
+
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
1829
|
+
{parameter.type !== 'boolean' && (
|
|
1830
|
+
<label style={{
|
|
1831
|
+
display: 'flex',
|
|
1832
|
+
alignItems: 'center',
|
|
1833
|
+
gap: '6px',
|
|
1834
|
+
marginBottom: '8px',
|
|
1835
|
+
fontSize: '13px',
|
|
1836
|
+
fontWeight: 600,
|
|
1837
|
+
color: theme.colors.text,
|
|
1838
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1839
|
+
flexShrink: 0
|
|
1840
|
+
}}>
|
|
1841
|
+
<span>{parameter.displayName}</span>
|
|
1842
|
+
{parameter.required && (
|
|
1843
|
+
<span style={{
|
|
1844
|
+
color: theme.accent.red,
|
|
1845
|
+
fontSize: '14px',
|
|
1846
|
+
fontWeight: 700
|
|
1847
|
+
}}>*</span>
|
|
1848
|
+
)}
|
|
1849
|
+
</label>
|
|
1850
|
+
)}
|
|
1851
|
+
|
|
1852
|
+
<div style={{ flex: 1, minHeight: 0 }}>
|
|
1853
|
+
{renderInput()}
|
|
1854
|
+
</div>
|
|
1855
|
+
|
|
1856
|
+
{parameter.description && (
|
|
1857
|
+
<div style={{
|
|
1858
|
+
fontSize: '12px',
|
|
1859
|
+
color: theme.colors.textSecondary,
|
|
1860
|
+
marginTop: '6px',
|
|
1861
|
+
lineHeight: '1.5',
|
|
1862
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1863
|
+
paddingLeft: '2px',
|
|
1864
|
+
flexShrink: 0
|
|
1865
|
+
}}>
|
|
1866
|
+
{parameter.description}
|
|
1867
|
+
</div>
|
|
1868
|
+
)}
|
|
1869
|
+
|
|
1870
|
+
</div>
|
|
1871
|
+
);
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
export default ParameterRenderer;
|