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.
Files changed (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,1074 @@
1
+ /**
2
+ * Console Panel - n8n-style debug output panel with chat input
3
+ *
4
+ * Displays console log entries from Console nodes during workflow execution.
5
+ * Includes chat input section for triggering chatTrigger nodes.
6
+ * Shows in a collapsible bottom bar section with clear and filter options.
7
+ * Chat and Console are split 50/50 side by side.
8
+ * Supports resizing by dragging the top edge.
9
+ */
10
+
11
+ import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
12
+ import { Node } from 'reactflow';
13
+ import { useWebSocket, ConsoleLogEntry } from '../../contexts/WebSocketContext';
14
+ import { useAppTheme } from '../../hooks/useAppTheme';
15
+ import { useTheme } from '../../contexts/ThemeContext';
16
+ import Prism from 'prismjs';
17
+ import 'prismjs/components/prism-json';
18
+
19
+ // Node types that can be targeted by chat messages
20
+ const CHAT_TRIGGER_TYPES = ['chatTrigger'];
21
+ // Node types that produce console output
22
+ const CONSOLE_NODE_TYPES = ['console'];
23
+
24
+ interface ConsolePanelProps {
25
+ isOpen: boolean;
26
+ onToggle: () => void;
27
+ defaultHeight?: number;
28
+ minHeight?: number;
29
+ maxHeight?: number;
30
+ nodes?: Node[]; // Workflow nodes for dropdown selection
31
+ }
32
+
33
+ // Storage keys for persisting state
34
+ const CONSOLE_HEIGHT_KEY = 'console_panel_height';
35
+ const CHAT_WIDTH_KEY = 'console_chat_width_percent';
36
+
37
+ const ConsolePanel: React.FC<ConsolePanelProps> = ({
38
+ isOpen,
39
+ onToggle,
40
+ defaultHeight = 250,
41
+ minHeight = 100,
42
+ maxHeight = 600,
43
+ nodes = []
44
+ }) => {
45
+ const theme = useAppTheme();
46
+ const { isDarkMode } = useTheme();
47
+ const { consoleLogs, clearConsoleLogs, terminalLogs, clearTerminalLogs, sendChatMessage, chatMessages, clearChatMessages } = useWebSocket();
48
+
49
+ // Filter nodes to get chatTrigger and console nodes
50
+ const chatTriggerNodes = useMemo(() =>
51
+ nodes.filter(n => CHAT_TRIGGER_TYPES.includes(n.type || '')),
52
+ [nodes]
53
+ );
54
+ const consoleNodes = useMemo(() =>
55
+ nodes.filter(n => CONSOLE_NODE_TYPES.includes(n.type || '')),
56
+ [nodes]
57
+ );
58
+
59
+ // Selected node states (stored as node ID, empty string means "all")
60
+ const [selectedChatTriggerId, setSelectedChatTriggerId] = useState<string>('');
61
+ const [selectedConsoleId, setSelectedConsoleId] = useState<string>('');
62
+ const [filter, setFilter] = useState('');
63
+ const [terminalFilter, setTerminalFilter] = useState('');
64
+ const [terminalLogLevel, setTerminalLogLevel] = useState<'all' | 'error' | 'warning' | 'info' | 'debug'>('all');
65
+ const [autoScroll, setAutoScroll] = useState(true);
66
+ const [prettyPrint, setPrettyPrint] = useState(true);
67
+ const [consoleTab, setConsoleTab] = useState<'console' | 'terminal'>('console');
68
+ const logsEndRef = useRef<HTMLDivElement>(null);
69
+ const chatEndRef = useRef<HTMLDivElement>(null);
70
+ const chatInputRef = useRef<HTMLInputElement>(null);
71
+
72
+ // Chat input state
73
+ const [chatInput, setChatInput] = useState('');
74
+ const [isSending, setIsSending] = useState(false);
75
+
76
+ // Panel height state with localStorage persistence
77
+ const [panelHeight, setPanelHeight] = useState(() => {
78
+ try {
79
+ const saved = localStorage.getItem(CONSOLE_HEIGHT_KEY);
80
+ if (saved) {
81
+ const height = parseInt(saved, 10);
82
+ if (!isNaN(height) && height >= minHeight && height <= maxHeight) {
83
+ return height;
84
+ }
85
+ }
86
+ } catch {
87
+ // Ignore localStorage errors
88
+ }
89
+ return defaultHeight;
90
+ });
91
+
92
+ // Chat section width (percentage) with localStorage persistence
93
+ const [chatWidthPercent, setChatWidthPercent] = useState(() => {
94
+ try {
95
+ const saved = localStorage.getItem(CHAT_WIDTH_KEY);
96
+ if (saved) {
97
+ const width = parseFloat(saved);
98
+ if (!isNaN(width) && width >= 20 && width <= 80) {
99
+ return width;
100
+ }
101
+ }
102
+ } catch {
103
+ // Ignore localStorage errors
104
+ }
105
+ return 50; // Default 50%
106
+ });
107
+
108
+ // Vertical resize state (panel height)
109
+ const [isResizing, setIsResizing] = useState(false);
110
+ const resizeStartY = useRef(0);
111
+ const resizeStartHeight = useRef(0);
112
+
113
+ // Horizontal resize state (chat/console split)
114
+ const [isHorizontalResizing, setIsHorizontalResizing] = useState(false);
115
+ const resizeStartX = useRef(0);
116
+ const resizeStartWidth = useRef(0);
117
+ const containerRef = useRef<HTMLDivElement>(null);
118
+
119
+ // Save height to localStorage when it changes
120
+ useEffect(() => {
121
+ try {
122
+ localStorage.setItem(CONSOLE_HEIGHT_KEY, panelHeight.toString());
123
+ } catch {
124
+ // Ignore localStorage errors
125
+ }
126
+ }, [panelHeight]);
127
+
128
+ // Save chat width to localStorage when it changes
129
+ useEffect(() => {
130
+ try {
131
+ localStorage.setItem(CHAT_WIDTH_KEY, chatWidthPercent.toString());
132
+ } catch {
133
+ // Ignore localStorage errors
134
+ }
135
+ }, [chatWidthPercent]);
136
+
137
+ // Handle vertical resize start (panel height)
138
+ const handleResizeStart = useCallback((e: React.MouseEvent) => {
139
+ e.preventDefault();
140
+ e.stopPropagation();
141
+ setIsResizing(true);
142
+ resizeStartY.current = e.clientY;
143
+ resizeStartHeight.current = panelHeight;
144
+ }, [panelHeight]);
145
+
146
+ // Handle horizontal resize start (chat/console split)
147
+ const handleHorizontalResizeStart = useCallback((e: React.MouseEvent) => {
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ setIsHorizontalResizing(true);
151
+ resizeStartX.current = e.clientX;
152
+ resizeStartWidth.current = chatWidthPercent;
153
+ }, [chatWidthPercent]);
154
+
155
+ // Handle vertical resize move
156
+ useEffect(() => {
157
+ if (!isResizing) return;
158
+
159
+ const handleMouseMove = (e: MouseEvent) => {
160
+ const delta = resizeStartY.current - e.clientY;
161
+ const newHeight = Math.min(maxHeight, Math.max(minHeight, resizeStartHeight.current + delta));
162
+ setPanelHeight(newHeight);
163
+ };
164
+
165
+ const handleMouseUp = () => {
166
+ setIsResizing(false);
167
+ };
168
+
169
+ document.addEventListener('mousemove', handleMouseMove);
170
+ document.addEventListener('mouseup', handleMouseUp);
171
+
172
+ // Add cursor style to body during resize
173
+ document.body.style.cursor = 'ns-resize';
174
+ document.body.style.userSelect = 'none';
175
+
176
+ return () => {
177
+ document.removeEventListener('mousemove', handleMouseMove);
178
+ document.removeEventListener('mouseup', handleMouseUp);
179
+ document.body.style.cursor = '';
180
+ document.body.style.userSelect = '';
181
+ };
182
+ }, [isResizing, minHeight, maxHeight]);
183
+
184
+ // Handle horizontal resize move
185
+ useEffect(() => {
186
+ if (!isHorizontalResizing || !containerRef.current) return;
187
+
188
+ const handleMouseMove = (e: MouseEvent) => {
189
+ const container = containerRef.current;
190
+ if (!container) return;
191
+
192
+ const containerRect = container.getBoundingClientRect();
193
+ const newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
194
+ const clampedWidth = Math.min(80, Math.max(20, newWidth));
195
+ setChatWidthPercent(clampedWidth);
196
+ };
197
+
198
+ const handleMouseUp = () => {
199
+ setIsHorizontalResizing(false);
200
+ };
201
+
202
+ document.addEventListener('mousemove', handleMouseMove);
203
+ document.addEventListener('mouseup', handleMouseUp);
204
+
205
+ document.body.style.cursor = 'ew-resize';
206
+ document.body.style.userSelect = 'none';
207
+
208
+ return () => {
209
+ document.removeEventListener('mousemove', handleMouseMove);
210
+ document.removeEventListener('mouseup', handleMouseUp);
211
+ document.body.style.cursor = '';
212
+ document.body.style.userSelect = '';
213
+ };
214
+ }, [isHorizontalResizing]);
215
+
216
+ // Filter console logs based on search input and selected console node
217
+ const filteredLogs = useMemo(() => {
218
+ let logs = consoleLogs;
219
+
220
+ // Filter by selected console node (if one is selected)
221
+ if (selectedConsoleId) {
222
+ logs = logs.filter(log => log.node_id === selectedConsoleId);
223
+ }
224
+
225
+ // Filter by search text
226
+ if (filter) {
227
+ const lowerFilter = filter.toLowerCase();
228
+ logs = logs.filter(log =>
229
+ log.label.toLowerCase().includes(lowerFilter) ||
230
+ log.formatted.toLowerCase().includes(lowerFilter) ||
231
+ log.node_id.toLowerCase().includes(lowerFilter)
232
+ );
233
+ }
234
+
235
+ return logs;
236
+ }, [consoleLogs, filter, selectedConsoleId]);
237
+
238
+ // Filter terminal logs based on search input and log level
239
+ const filteredTerminalLogs = useMemo(() => {
240
+ let filtered = terminalLogs;
241
+
242
+ // Filter by log level
243
+ if (terminalLogLevel !== 'all') {
244
+ const levelPriority: Record<string, number> = { error: 0, warning: 1, info: 2, debug: 3 };
245
+ const selectedPriority = levelPriority[terminalLogLevel] ?? 2;
246
+ filtered = filtered.filter(log => (levelPriority[log.level] ?? 2) <= selectedPriority);
247
+ }
248
+
249
+ // Filter by search text
250
+ if (terminalFilter) {
251
+ const lowerFilter = terminalFilter.toLowerCase();
252
+ filtered = filtered.filter(log =>
253
+ log.message.toLowerCase().includes(lowerFilter) ||
254
+ (log.source?.toLowerCase().includes(lowerFilter))
255
+ );
256
+ }
257
+
258
+ return filtered;
259
+ }, [terminalLogs, terminalFilter, terminalLogLevel]);
260
+
261
+ // Auto-scroll to bottom when new logs arrive
262
+ useEffect(() => {
263
+ if (autoScroll && isOpen && logsEndRef.current) {
264
+ logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
265
+ }
266
+ }, [filteredLogs.length, autoScroll, isOpen]);
267
+
268
+ // Auto-scroll chat when new messages arrive
269
+ useEffect(() => {
270
+ if (isOpen && chatEndRef.current) {
271
+ chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
272
+ }
273
+ }, [chatMessages?.length, isOpen]);
274
+
275
+ const handleClearConsole = useCallback(() => {
276
+ clearConsoleLogs();
277
+ }, [clearConsoleLogs]);
278
+
279
+ const handleClearChat = useCallback(() => {
280
+ clearChatMessages();
281
+ }, [clearChatMessages]);
282
+
283
+ // Handle chat message send
284
+ const handleSendChat = useCallback(async () => {
285
+ const message = chatInput.trim();
286
+ if (!message || isSending) return;
287
+
288
+ setIsSending(true);
289
+ try {
290
+ // Pass selected chatTrigger node ID if one is selected (empty string means broadcast to all)
291
+ await sendChatMessage(message, selectedChatTriggerId || undefined);
292
+ setChatInput('');
293
+ } catch (error) {
294
+ console.error('Failed to send chat message:', error);
295
+ } finally {
296
+ setIsSending(false);
297
+ }
298
+ }, [chatInput, isSending, sendChatMessage, selectedChatTriggerId]);
299
+
300
+ // Handle Enter key in chat input
301
+ const handleChatKeyDown = useCallback((e: React.KeyboardEvent) => {
302
+ if (e.key === 'Enter' && !e.shiftKey) {
303
+ e.preventDefault();
304
+ handleSendChat();
305
+ }
306
+ }, [handleSendChat]);
307
+
308
+ const formatTimestamp = useCallback((timestamp: string) => {
309
+ try {
310
+ const date = new Date(timestamp);
311
+ const timeStr = date.toLocaleTimeString('en-US', {
312
+ hour12: false,
313
+ hour: '2-digit',
314
+ minute: '2-digit',
315
+ second: '2-digit'
316
+ });
317
+ // Add milliseconds manually
318
+ const ms = date.getMilliseconds().toString().padStart(3, '0');
319
+ return `${timeStr}.${ms}`;
320
+ } catch {
321
+ return timestamp;
322
+ }
323
+ }, []);
324
+
325
+ const getFormatColor = useCallback((format: ConsoleLogEntry['format']) => {
326
+ switch (format) {
327
+ case 'json':
328
+ case 'json_compact':
329
+ return isDarkMode ? theme.dracula.cyan : '#0891b2';
330
+ case 'text':
331
+ return isDarkMode ? theme.dracula.foreground : theme.colors.text;
332
+ case 'table':
333
+ return isDarkMode ? theme.dracula.green : '#059669';
334
+ default:
335
+ return theme.colors.text;
336
+ }
337
+ }, [isDarkMode, theme]);
338
+
339
+ // Format text for pretty printing - converts escaped newlines and formats JSON
340
+ const formatForDisplay = useCallback((text: string): { formatted: string; isJson: boolean } => {
341
+ if (!prettyPrint) return { formatted: text, isJson: false };
342
+
343
+ // Convert escaped newlines to actual newlines
344
+ let formatted = text.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
345
+
346
+ // Try to parse and pretty-print JSON
347
+ const trimmed = formatted.trim();
348
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
349
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
350
+ try {
351
+ const parsed = JSON.parse(trimmed);
352
+ formatted = JSON.stringify(parsed, null, 2);
353
+ return { formatted, isJson: true };
354
+ } catch {
355
+ // Not valid JSON, return with converted newlines
356
+ }
357
+ }
358
+
359
+ return { formatted, isJson: false };
360
+ }, [prettyPrint]);
361
+
362
+ // Highlight JSON with Prism
363
+ const highlightJson = useCallback((code: string): string => {
364
+ return Prism.highlight(code, Prism.languages.json, 'json');
365
+ }, []);
366
+
367
+ // Prism token styles following CodeEditor.tsx convention
368
+ const prismStyles = `
369
+ .console-json-output .token.string { color: ${theme.dracula.yellow}; }
370
+ .console-json-output .token.number { color: ${theme.dracula.purple}; }
371
+ .console-json-output .token.boolean { color: ${theme.dracula.purple}; }
372
+ .console-json-output .token.null { color: ${theme.dracula.purple}; }
373
+ .console-json-output .token.property { color: ${theme.dracula.cyan}; }
374
+ .console-json-output .token.punctuation { color: ${theme.colors.text}; }
375
+ .console-json-output .token.operator { color: ${theme.dracula.pink}; }
376
+ `;
377
+
378
+ // Resize handle style
379
+ const resizeHandleStyle: React.CSSProperties = {
380
+ position: 'absolute',
381
+ top: 0,
382
+ left: 0,
383
+ right: 0,
384
+ height: '6px',
385
+ cursor: 'ns-resize',
386
+ backgroundColor: isResizing
387
+ ? (isDarkMode ? theme.dracula.purple : theme.colors.primary)
388
+ : 'transparent',
389
+ transition: isResizing ? 'none' : 'background-color 0.15s ease',
390
+ zIndex: 10
391
+ };
392
+
393
+ // Panel header with toggle
394
+ const panelHeaderStyle: React.CSSProperties = {
395
+ display: 'flex',
396
+ alignItems: 'center',
397
+ justifyContent: 'space-between',
398
+ padding: '6px 12px',
399
+ backgroundColor: isDarkMode ? theme.dracula.currentLine : theme.colors.backgroundPanel,
400
+ borderTop: `1px solid ${theme.colors.border}`,
401
+ cursor: 'pointer',
402
+ userSelect: 'none'
403
+ };
404
+
405
+ const headerLeftStyle: React.CSSProperties = {
406
+ display: 'flex',
407
+ alignItems: 'center',
408
+ gap: '8px'
409
+ };
410
+
411
+ const titleStyle: React.CSSProperties = {
412
+ fontSize: theme.fontSize.sm,
413
+ fontWeight: theme.fontWeight.semibold,
414
+ color: theme.colors.text,
415
+ display: 'flex',
416
+ alignItems: 'center',
417
+ gap: '6px'
418
+ };
419
+
420
+ const badgeStyle: React.CSSProperties = {
421
+ backgroundColor: isDarkMode ? `${theme.dracula.purple}40` : `${theme.colors.primary}20`,
422
+ color: isDarkMode ? theme.dracula.purple : theme.colors.primary,
423
+ padding: '1px 6px',
424
+ borderRadius: '10px',
425
+ fontSize: theme.fontSize.xs,
426
+ fontWeight: theme.fontWeight.medium
427
+ };
428
+
429
+ const contentStyle: React.CSSProperties = {
430
+ height: isOpen ? `${panelHeight}px` : '0px',
431
+ overflow: 'hidden',
432
+ backgroundColor: isDarkMode ? theme.dracula.background : theme.colors.background,
433
+ transition: (isResizing || isHorizontalResizing) ? 'none' : 'height 0.2s ease-in-out',
434
+ display: 'flex',
435
+ flexDirection: 'row' // Side by side
436
+ };
437
+
438
+ const chatSectionStyle: React.CSSProperties = {
439
+ width: `${chatWidthPercent}%`,
440
+ display: 'flex',
441
+ flexDirection: 'column',
442
+ overflow: 'hidden',
443
+ position: 'relative'
444
+ };
445
+
446
+ const consoleSectionStyle: React.CSSProperties = {
447
+ flex: 1,
448
+ display: 'flex',
449
+ flexDirection: 'column',
450
+ overflow: 'hidden'
451
+ };
452
+
453
+ const horizontalResizeHandleStyle: React.CSSProperties = {
454
+ width: '6px',
455
+ cursor: 'ew-resize',
456
+ backgroundColor: isHorizontalResizing
457
+ ? (isDarkMode ? theme.dracula.purple : theme.colors.primary)
458
+ : (isDarkMode ? theme.dracula.selection : theme.colors.border),
459
+ transition: isHorizontalResizing ? 'none' : 'background-color 0.15s ease',
460
+ flexShrink: 0
461
+ };
462
+
463
+ const sectionHeaderStyle: React.CSSProperties = {
464
+ display: 'flex',
465
+ alignItems: 'center',
466
+ justifyContent: 'space-between',
467
+ padding: '6px 12px',
468
+ backgroundColor: isDarkMode ? theme.dracula.currentLine : theme.colors.backgroundPanel,
469
+ borderBottom: `1px solid ${isDarkMode ? theme.dracula.selection : theme.colors.border}`,
470
+ minHeight: '32px'
471
+ };
472
+
473
+ const sectionTitleStyle: React.CSSProperties = {
474
+ fontSize: theme.fontSize.xs,
475
+ fontWeight: theme.fontWeight.semibold,
476
+ color: theme.colors.text,
477
+ display: 'flex',
478
+ alignItems: 'center',
479
+ gap: '6px'
480
+ };
481
+
482
+ const buttonStyle: React.CSSProperties = {
483
+ background: 'none',
484
+ border: 'none',
485
+ cursor: 'pointer',
486
+ padding: '2px 6px',
487
+ borderRadius: theme.borderRadius.sm,
488
+ fontSize: theme.fontSize.xs,
489
+ color: theme.colors.textSecondary,
490
+ transition: theme.transitions.fast
491
+ };
492
+
493
+ const clearButtonStyle: React.CSSProperties = {
494
+ ...buttonStyle,
495
+ backgroundColor: isDarkMode ? `${theme.dracula.red}20` : `${theme.colors.error}15`,
496
+ color: isDarkMode ? theme.dracula.red : theme.colors.error
497
+ };
498
+
499
+ const filterInputStyle: React.CSSProperties = {
500
+ padding: '2px 6px',
501
+ fontSize: theme.fontSize.xs,
502
+ backgroundColor: isDarkMode ? theme.dracula.background : theme.colors.background,
503
+ border: `1px solid ${theme.colors.border}`,
504
+ borderRadius: theme.borderRadius.sm,
505
+ color: theme.colors.text,
506
+ width: '100px',
507
+ outline: 'none'
508
+ };
509
+
510
+ // Shared select/dropdown style using theme
511
+ const selectStyle: React.CSSProperties = {
512
+ padding: '2px 4px',
513
+ fontSize: theme.fontSize.xs,
514
+ backgroundColor: isDarkMode ? theme.dracula.currentLine : theme.colors.background,
515
+ color: theme.colors.text,
516
+ border: `1px solid ${isDarkMode ? theme.dracula.selection : theme.colors.border}`,
517
+ borderRadius: theme.borderRadius.sm,
518
+ cursor: 'pointer',
519
+ outline: 'none',
520
+ maxWidth: '120px'
521
+ };
522
+
523
+ const logEntryStyle: React.CSSProperties = {
524
+ display: 'flex',
525
+ padding: '4px 12px',
526
+ borderBottom: `1px solid ${isDarkMode ? theme.dracula.selection : theme.colors.border}`,
527
+ gap: '12px',
528
+ alignItems: 'flex-start'
529
+ };
530
+
531
+ const timestampStyle: React.CSSProperties = {
532
+ color: isDarkMode ? theme.dracula.comment : theme.colors.textMuted,
533
+ fontSize: theme.fontSize.xs,
534
+ whiteSpace: 'nowrap',
535
+ minWidth: '90px'
536
+ };
537
+
538
+ const labelStyle: React.CSSProperties = {
539
+ color: isDarkMode ? theme.dracula.yellow : theme.colors.warning,
540
+ fontSize: theme.fontSize.sm,
541
+ fontWeight: theme.fontWeight.medium,
542
+ minWidth: '80px',
543
+ maxWidth: '120px',
544
+ overflow: 'hidden',
545
+ textOverflow: 'ellipsis',
546
+ whiteSpace: 'nowrap'
547
+ };
548
+
549
+ const nodeInfoStyle: React.CSSProperties = {
550
+ color: isDarkMode ? theme.dracula.pink : theme.colors.categoryTrigger,
551
+ fontSize: theme.fontSize.xs,
552
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace',
553
+ minWidth: '80px',
554
+ maxWidth: '120px',
555
+ overflow: 'hidden',
556
+ textOverflow: 'ellipsis',
557
+ whiteSpace: 'nowrap',
558
+ opacity: 0.85
559
+ };
560
+
561
+ const emptyStyle: React.CSSProperties = {
562
+ padding: '24px',
563
+ textAlign: 'center',
564
+ color: theme.colors.textMuted,
565
+ fontSize: theme.fontSize.xs,
566
+ flex: 1,
567
+ display: 'flex',
568
+ alignItems: 'center',
569
+ justifyContent: 'center'
570
+ };
571
+
572
+ const chevronStyle: React.CSSProperties = {
573
+ transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
574
+ transition: 'transform 0.2s ease',
575
+ color: theme.colors.textSecondary
576
+ };
577
+
578
+ // Chat styles
579
+ const chatMessagesStyle: React.CSSProperties = {
580
+ flex: 1,
581
+ overflow: 'auto',
582
+ padding: '12px 16px'
583
+ };
584
+
585
+ const chatInputContainerStyle: React.CSSProperties = {
586
+ display: 'flex',
587
+ gap: '10px',
588
+ padding: '10px 16px',
589
+ borderTop: `1px solid ${isDarkMode ? theme.dracula.selection : theme.colors.border}`,
590
+ backgroundColor: isDarkMode ? theme.dracula.currentLine : theme.colors.backgroundPanel
591
+ };
592
+
593
+ const chatInputStyle: React.CSSProperties = {
594
+ flex: 1,
595
+ padding: '8px 12px',
596
+ fontSize: theme.fontSize.sm,
597
+ backgroundColor: isDarkMode ? theme.dracula.background : theme.colors.background,
598
+ border: `1px solid ${isDarkMode ? theme.dracula.selection : theme.colors.border}`,
599
+ borderRadius: '8px',
600
+ color: theme.colors.text,
601
+ outline: 'none'
602
+ };
603
+
604
+ const sendButtonStyle: React.CSSProperties = {
605
+ padding: '6px 12px',
606
+ fontSize: theme.fontSize.xs,
607
+ fontWeight: theme.fontWeight.medium,
608
+ backgroundColor: isDarkMode ? theme.dracula.green : theme.colors.success,
609
+ color: isDarkMode ? theme.dracula.background : 'white',
610
+ border: 'none',
611
+ borderRadius: '6px',
612
+ cursor: isSending ? 'not-allowed' : 'pointer',
613
+ opacity: isSending ? 0.7 : 1,
614
+ transition: 'all 0.15s ease'
615
+ };
616
+
617
+ const chatMessageStyle = (isUser: boolean): React.CSSProperties => ({
618
+ padding: '8px 12px',
619
+ marginBottom: '8px',
620
+ borderRadius: isUser ? '12px 12px 4px 12px' : '12px 12px 12px 4px',
621
+ backgroundColor: isUser
622
+ ? (isDarkMode ? `${theme.dracula.purple}40` : `${theme.colors.primary}20`)
623
+ : (isDarkMode ? theme.dracula.selection : theme.colors.backgroundPanel),
624
+ maxWidth: '80%',
625
+ marginLeft: isUser ? 'auto' : '0',
626
+ marginRight: isUser ? '0' : 'auto',
627
+ wordBreak: 'break-word',
628
+ boxShadow: isDarkMode ? 'none' : '0 1px 2px rgba(0,0,0,0.05)'
629
+ });
630
+
631
+ const chatMessageTextStyle: React.CSSProperties = {
632
+ fontSize: theme.fontSize.xs,
633
+ color: theme.colors.text,
634
+ margin: 0
635
+ };
636
+
637
+ const chatMessageTimeStyle: React.CSSProperties = {
638
+ fontSize: '9px',
639
+ color: theme.colors.textMuted,
640
+ marginTop: '2px'
641
+ };
642
+
643
+ return (
644
+ <div style={{ position: 'relative' }}>
645
+ {/* Prism syntax highlighting styles for JSON */}
646
+ <style>{prismStyles}</style>
647
+
648
+ {/* Resize Handle - Only visible when open */}
649
+ {isOpen && (
650
+ <div
651
+ style={resizeHandleStyle}
652
+ onMouseDown={handleResizeStart}
653
+ onMouseEnter={e => {
654
+ if (!isResizing) {
655
+ e.currentTarget.style.backgroundColor = isDarkMode
656
+ ? `${theme.dracula.purple}40`
657
+ : `${theme.colors.primary}30`;
658
+ }
659
+ }}
660
+ onMouseLeave={e => {
661
+ if (!isResizing) {
662
+ e.currentTarget.style.backgroundColor = 'transparent';
663
+ }
664
+ }}
665
+ />
666
+ )}
667
+
668
+ {/* Panel Header - Always visible */}
669
+ <div
670
+ style={panelHeaderStyle}
671
+ onClick={onToggle}
672
+ >
673
+ <div style={headerLeftStyle}>
674
+ <span style={chevronStyle}>
675
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
676
+ <path d="M2 4L6 8L10 4" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
677
+ </svg>
678
+ </span>
679
+ <span style={titleStyle}>
680
+ Chat / Console
681
+ {(consoleLogs.length > 0 || (chatMessages && chatMessages.length > 0)) && (
682
+ <span style={badgeStyle}>
683
+ {consoleLogs.length + (chatMessages?.length || 0)}
684
+ </span>
685
+ )}
686
+ </span>
687
+ </div>
688
+ </div>
689
+
690
+ {/* Panel Content - Resizable Split */}
691
+ <div ref={containerRef} style={contentStyle}>
692
+ {/* Chat Section - Left */}
693
+ <div style={chatSectionStyle}>
694
+ <div style={sectionHeaderStyle}>
695
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
696
+ <span style={sectionTitleStyle}>
697
+ Chat
698
+ {chatMessages && chatMessages.length > 0 && (
699
+ <span style={{
700
+ ...badgeStyle,
701
+ backgroundColor: isDarkMode ? `${theme.dracula.green}40` : `${theme.colors.success}20`,
702
+ color: isDarkMode ? theme.dracula.green : theme.colors.success
703
+ }}>
704
+ {chatMessages.length}
705
+ </span>
706
+ )}
707
+ </span>
708
+ {/* ChatTrigger node selector dropdown */}
709
+ {chatTriggerNodes.length > 0 && (
710
+ <select
711
+ value={selectedChatTriggerId}
712
+ onChange={e => setSelectedChatTriggerId(e.target.value)}
713
+ onClick={e => e.stopPropagation()}
714
+ style={selectStyle}
715
+ title="Select chatTrigger node to target"
716
+ >
717
+ <option value="">All Triggers</option>
718
+ {chatTriggerNodes.map(node => (
719
+ <option key={node.id} value={node.id}>
720
+ {node.data?.label || node.id}
721
+ </option>
722
+ ))}
723
+ </select>
724
+ )}
725
+ </div>
726
+ {chatMessages && chatMessages.length > 0 && (
727
+ <button
728
+ style={clearButtonStyle}
729
+ onClick={handleClearChat}
730
+ onMouseEnter={e => {
731
+ e.currentTarget.style.backgroundColor = isDarkMode ? `${theme.dracula.red}35` : `${theme.colors.error}25`;
732
+ }}
733
+ onMouseLeave={e => {
734
+ e.currentTarget.style.backgroundColor = isDarkMode ? `${theme.dracula.red}20` : `${theme.colors.error}15`;
735
+ }}
736
+ >
737
+ Clear
738
+ </button>
739
+ )}
740
+ </div>
741
+ <div style={chatMessagesStyle}>
742
+ {(!chatMessages || chatMessages.length === 0) ? (
743
+ <div style={emptyStyle}>
744
+ Send a message to trigger chatTrigger nodes
745
+ </div>
746
+ ) : (
747
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
748
+ {chatMessages.map((msg, index) => (
749
+ <div key={`${msg.timestamp}-${index}`} style={chatMessageStyle(msg.role === 'user')}>
750
+ <pre style={{
751
+ ...chatMessageTextStyle,
752
+ margin: 0,
753
+ whiteSpace: 'pre-wrap',
754
+ wordBreak: 'break-word',
755
+ lineHeight: 1.15,
756
+ fontFamily: 'inherit',
757
+ fontSize: 'inherit'
758
+ }}>
759
+ {msg.message}
760
+ </pre>
761
+ <div style={chatMessageTimeStyle}>
762
+ {formatTimestamp(msg.timestamp)}
763
+ </div>
764
+ </div>
765
+ ))}
766
+ <div ref={chatEndRef} />
767
+ </div>
768
+ )}
769
+ </div>
770
+ <div style={chatInputContainerStyle}>
771
+ <input
772
+ ref={chatInputRef}
773
+ type="text"
774
+ placeholder="Type a message..."
775
+ value={chatInput}
776
+ onChange={e => setChatInput(e.target.value)}
777
+ onKeyDown={handleChatKeyDown}
778
+ style={chatInputStyle}
779
+ disabled={isSending}
780
+ />
781
+ <button
782
+ style={sendButtonStyle}
783
+ onClick={handleSendChat}
784
+ disabled={isSending || !chatInput.trim()}
785
+ >
786
+ {isSending ? '...' : 'Send'}
787
+ </button>
788
+ </div>
789
+ </div>
790
+
791
+ {/* Horizontal Resize Handle */}
792
+ <div
793
+ style={horizontalResizeHandleStyle}
794
+ onMouseDown={handleHorizontalResizeStart}
795
+ onMouseEnter={e => {
796
+ if (!isHorizontalResizing) {
797
+ e.currentTarget.style.backgroundColor = isDarkMode
798
+ ? `${theme.dracula.purple}60`
799
+ : `${theme.colors.primary}40`;
800
+ }
801
+ }}
802
+ onMouseLeave={e => {
803
+ if (!isHorizontalResizing) {
804
+ e.currentTarget.style.backgroundColor = isDarkMode
805
+ ? theme.dracula.selection
806
+ : theme.colors.border;
807
+ }
808
+ }}
809
+ />
810
+
811
+ {/* Console/Terminal Section - Right */}
812
+ <div style={consoleSectionStyle}>
813
+ <div style={sectionHeaderStyle}>
814
+ {/* Tab Buttons */}
815
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
816
+ <button
817
+ onClick={() => setConsoleTab('console')}
818
+ style={{
819
+ ...buttonStyle,
820
+ padding: '3px 8px',
821
+ backgroundColor: consoleTab === 'console'
822
+ ? (isDarkMode ? `${theme.dracula.purple}40` : `${theme.colors.primary}20`)
823
+ : 'transparent',
824
+ color: consoleTab === 'console'
825
+ ? (isDarkMode ? theme.dracula.purple : theme.colors.primary)
826
+ : theme.colors.textSecondary,
827
+ fontWeight: consoleTab === 'console' ? '600' : '400',
828
+ borderRadius: '4px'
829
+ }}
830
+ >
831
+ Console
832
+ {consoleLogs.length > 0 && (
833
+ <span style={{ ...badgeStyle, marginLeft: '4px', padding: '0 4px' }}>{consoleLogs.length}</span>
834
+ )}
835
+ </button>
836
+ <button
837
+ onClick={() => setConsoleTab('terminal')}
838
+ style={{
839
+ ...buttonStyle,
840
+ padding: '3px 8px',
841
+ backgroundColor: consoleTab === 'terminal'
842
+ ? (isDarkMode ? `${theme.dracula.cyan}40` : `${theme.colors.info}20`)
843
+ : 'transparent',
844
+ color: consoleTab === 'terminal'
845
+ ? (isDarkMode ? theme.dracula.cyan : theme.colors.info)
846
+ : theme.colors.textSecondary,
847
+ fontWeight: consoleTab === 'terminal' ? '600' : '400',
848
+ borderRadius: '4px'
849
+ }}
850
+ >
851
+ Terminal
852
+ {terminalLogs.length > 0 && (
853
+ <span style={{
854
+ ...badgeStyle,
855
+ marginLeft: '4px',
856
+ padding: '0 4px',
857
+ backgroundColor: isDarkMode ? `${theme.dracula.cyan}40` : `${theme.colors.info}20`,
858
+ color: isDarkMode ? theme.dracula.cyan : theme.colors.info
859
+ }}>{terminalLogs.length}</span>
860
+ )}
861
+ </button>
862
+ </div>
863
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
864
+ {consoleTab === 'terminal' && (
865
+ <select
866
+ value={terminalLogLevel}
867
+ onChange={e => setTerminalLogLevel(e.target.value as 'all' | 'error' | 'warning' | 'info' | 'debug')}
868
+ onClick={e => e.stopPropagation()}
869
+ style={selectStyle}
870
+ >
871
+ <option value="all">All Levels</option>
872
+ <option value="error">Error</option>
873
+ <option value="warning">Warning+</option>
874
+ <option value="info">Info+</option>
875
+ <option value="debug">Debug+</option>
876
+ </select>
877
+ )}
878
+ {/* Console node selector dropdown */}
879
+ {consoleTab === 'console' && consoleNodes.length > 0 && (
880
+ <select
881
+ value={selectedConsoleId}
882
+ onChange={e => setSelectedConsoleId(e.target.value)}
883
+ onClick={e => e.stopPropagation()}
884
+ style={selectStyle}
885
+ title="Filter logs by Console node"
886
+ >
887
+ <option value="">All Consoles</option>
888
+ {consoleNodes.map(node => (
889
+ <option key={node.id} value={node.id}>
890
+ {node.data?.label || node.id}
891
+ </option>
892
+ ))}
893
+ </select>
894
+ )}
895
+ <input
896
+ type="text"
897
+ placeholder="Filter..."
898
+ value={consoleTab === 'console' ? filter : terminalFilter}
899
+ onChange={e => consoleTab === 'console' ? setFilter(e.target.value) : setTerminalFilter(e.target.value)}
900
+ style={filterInputStyle}
901
+ onClick={e => e.stopPropagation()}
902
+ />
903
+ <label
904
+ style={{
905
+ ...buttonStyle,
906
+ display: 'flex',
907
+ alignItems: 'center',
908
+ gap: '3px',
909
+ cursor: 'pointer'
910
+ }}
911
+ >
912
+ <input
913
+ type="checkbox"
914
+ checked={autoScroll}
915
+ onChange={e => setAutoScroll(e.target.checked)}
916
+ style={{ cursor: 'pointer', width: '12px', height: '12px' }}
917
+ />
918
+ Auto
919
+ </label>
920
+ {consoleTab === 'console' && (
921
+ <label
922
+ style={{
923
+ ...buttonStyle,
924
+ display: 'flex',
925
+ alignItems: 'center',
926
+ gap: '3px',
927
+ cursor: 'pointer',
928
+ backgroundColor: prettyPrint
929
+ ? (isDarkMode ? `${theme.dracula.cyan}30` : `${theme.colors.info}20`)
930
+ : 'transparent'
931
+ }}
932
+ title="Format JSON and convert escaped newlines"
933
+ >
934
+ <input
935
+ type="checkbox"
936
+ checked={prettyPrint}
937
+ onChange={e => setPrettyPrint(e.target.checked)}
938
+ style={{ cursor: 'pointer', width: '12px', height: '12px' }}
939
+ />
940
+ Pretty
941
+ </label>
942
+ )}
943
+ {((consoleTab === 'console' && consoleLogs.length > 0) || (consoleTab === 'terminal' && terminalLogs.length > 0)) && (
944
+ <button
945
+ style={clearButtonStyle}
946
+ onClick={consoleTab === 'console' ? handleClearConsole : clearTerminalLogs}
947
+ onMouseEnter={e => {
948
+ e.currentTarget.style.backgroundColor = isDarkMode ? `${theme.dracula.red}35` : `${theme.colors.error}25`;
949
+ }}
950
+ onMouseLeave={e => {
951
+ e.currentTarget.style.backgroundColor = isDarkMode ? `${theme.dracula.red}20` : `${theme.colors.error}15`;
952
+ }}
953
+ >
954
+ Clear
955
+ </button>
956
+ )}
957
+ </div>
958
+ </div>
959
+ <div style={{ flex: 1, overflow: 'auto', fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace', fontSize: theme.fontSize.sm }}>
960
+ {consoleTab === 'console' ? (
961
+ // Console Logs Tab
962
+ filteredLogs.length === 0 ? (
963
+ <div style={emptyStyle}>
964
+ {consoleLogs.length === 0
965
+ ? 'Add a Console node to see debug output'
966
+ : 'No logs match the filter'}
967
+ </div>
968
+ ) : (
969
+ filteredLogs.slice().reverse().map((log, index) => (
970
+ <div key={`${log.node_id}-${log.timestamp}-${index}`} style={logEntryStyle}>
971
+ <span style={timestampStyle}>{formatTimestamp(log.timestamp)}</span>
972
+ <span style={labelStyle} title={log.label || log.node_id}>
973
+ {log.label || log.node_id}
974
+ </span>
975
+ {log.source_node_label && (
976
+ <span style={nodeInfoStyle} title={`Source: ${log.source_node_type} (${log.source_node_id})`}>
977
+ {log.source_node_label}
978
+ </span>
979
+ )}
980
+ {(() => {
981
+ const { formatted, isJson } = formatForDisplay(log.formatted);
982
+ return isJson && prettyPrint ? (
983
+ <pre
984
+ className="console-json-output"
985
+ style={{
986
+ margin: 0,
987
+ flex: 1,
988
+ overflow: 'auto',
989
+ whiteSpace: 'pre-wrap',
990
+ wordBreak: 'break-word'
991
+ }}
992
+ dangerouslySetInnerHTML={{ __html: highlightJson(formatted) }}
993
+ />
994
+ ) : (
995
+ <pre
996
+ style={{
997
+ margin: 0,
998
+ flex: 1,
999
+ overflow: 'auto',
1000
+ color: getFormatColor(log.format),
1001
+ whiteSpace: 'pre-wrap',
1002
+ wordBreak: 'break-word',
1003
+ lineHeight: 1.15,
1004
+ fontFamily: 'inherit',
1005
+ fontSize: 'inherit'
1006
+ }}
1007
+ >
1008
+ {formatted}
1009
+ </pre>
1010
+ );
1011
+ })()}
1012
+ </div>
1013
+ ))
1014
+ )
1015
+ ) : (
1016
+ // Terminal Logs Tab
1017
+ filteredTerminalLogs.length === 0 ? (
1018
+ <div style={emptyStyle}>
1019
+ {terminalLogs.length === 0
1020
+ ? 'Server logs will appear here'
1021
+ : 'No logs match the filter'}
1022
+ </div>
1023
+ ) : (
1024
+ <div style={{ minWidth: 'max-content' }}>
1025
+ {filteredTerminalLogs.slice().reverse().map((log, index) => (
1026
+ <div key={`${log.timestamp}-${index}`} style={{
1027
+ padding: '3px 12px',
1028
+ borderBottom: `1px solid ${isDarkMode ? theme.dracula.selection : theme.colors.border}`,
1029
+ backgroundColor: log.level === 'error'
1030
+ ? (isDarkMode ? `${theme.dracula.red}10` : `${theme.colors.error}05`)
1031
+ : log.level === 'warning'
1032
+ ? (isDarkMode ? `${theme.dracula.orange}10` : `${theme.colors.warning}05`)
1033
+ : 'transparent',
1034
+ whiteSpace: 'nowrap'
1035
+ }}>
1036
+ <span style={{
1037
+ color: isDarkMode ? theme.dracula.comment : theme.colors.textMuted,
1038
+ fontSize: theme.fontSize.xs
1039
+ }}>{formatTimestamp(log.timestamp)}</span>
1040
+ {log.source && (
1041
+ <span style={{
1042
+ color: isDarkMode ? theme.dracula.cyan : theme.colors.info,
1043
+ fontSize: theme.fontSize.xs,
1044
+ marginLeft: theme.spacing.sm
1045
+ }}>
1046
+ [{log.source}]
1047
+ </span>
1048
+ )}
1049
+ <span style={{
1050
+ color: theme.colors.text,
1051
+ fontSize: theme.fontSize.sm,
1052
+ marginLeft: theme.spacing.sm
1053
+ }}>
1054
+ {log.message}
1055
+ {log.details && (
1056
+ <span style={{ color: isDarkMode ? theme.dracula.comment : theme.colors.textMuted, marginLeft: theme.spacing.sm }}>
1057
+ {typeof log.details === 'string' ? log.details : JSON.stringify(log.details)}
1058
+ </span>
1059
+ )}
1060
+ </span>
1061
+ </div>
1062
+ ))}
1063
+ </div>
1064
+ )
1065
+ )}
1066
+ <div ref={logsEndRef} />
1067
+ </div>
1068
+ </div>
1069
+ </div>
1070
+ </div>
1071
+ );
1072
+ };
1073
+
1074
+ export default ConsolePanel;