@nodetool-ai/websocket 0.7.0-rc.11

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 (261) hide show
  1. package/dist/agent/agent-runtime.d.ts +47 -0
  2. package/dist/agent/agent-runtime.d.ts.map +1 -0
  3. package/dist/agent/agent-runtime.js +577 -0
  4. package/dist/agent/agent-runtime.js.map +1 -0
  5. package/dist/agent/codex-agent.d.ts +41 -0
  6. package/dist/agent/codex-agent.d.ts.map +1 -0
  7. package/dist/agent/codex-agent.js +334 -0
  8. package/dist/agent/codex-agent.js.map +1 -0
  9. package/dist/agent/index.d.ts +6 -0
  10. package/dist/agent/index.d.ts.map +1 -0
  11. package/dist/agent/index.js +4 -0
  12. package/dist/agent/index.js.map +1 -0
  13. package/dist/agent/llm-agent.d.ts +51 -0
  14. package/dist/agent/llm-agent.d.ts.map +1 -0
  15. package/dist/agent/llm-agent.js +526 -0
  16. package/dist/agent/llm-agent.js.map +1 -0
  17. package/dist/agent/mcp-tool-server.d.ts +33 -0
  18. package/dist/agent/mcp-tool-server.d.ts.map +1 -0
  19. package/dist/agent/mcp-tool-server.js +292 -0
  20. package/dist/agent/mcp-tool-server.js.map +1 -0
  21. package/dist/agent/opencode-agent.d.ts +32 -0
  22. package/dist/agent/opencode-agent.d.ts.map +1 -0
  23. package/dist/agent/opencode-agent.js +332 -0
  24. package/dist/agent/opencode-agent.js.map +1 -0
  25. package/dist/agent/pi-agent.d.ts +42 -0
  26. package/dist/agent/pi-agent.d.ts.map +1 -0
  27. package/dist/agent/pi-agent.js +429 -0
  28. package/dist/agent/pi-agent.js.map +1 -0
  29. package/dist/agent/sdk-provider.d.ts +46 -0
  30. package/dist/agent/sdk-provider.d.ts.map +1 -0
  31. package/dist/agent/sdk-provider.js +50 -0
  32. package/dist/agent/sdk-provider.js.map +1 -0
  33. package/dist/agent/socket-route.d.ts +11 -0
  34. package/dist/agent/socket-route.d.ts.map +1 -0
  35. package/dist/agent/socket-route.js +262 -0
  36. package/dist/agent/socket-route.js.map +1 -0
  37. package/dist/agent/transport.d.ts +39 -0
  38. package/dist/agent/transport.d.ts.map +1 -0
  39. package/dist/agent/transport.js +17 -0
  40. package/dist/agent/transport.js.map +1 -0
  41. package/dist/agent/types.d.ts +11 -0
  42. package/dist/agent/types.d.ts.map +1 -0
  43. package/dist/agent/types.js +10 -0
  44. package/dist/agent/types.js.map +1 -0
  45. package/dist/collection-api.d.ts +14 -0
  46. package/dist/collection-api.d.ts.map +1 -0
  47. package/dist/collection-api.js +75 -0
  48. package/dist/collection-api.js.map +1 -0
  49. package/dist/error-codes.d.ts +3 -0
  50. package/dist/error-codes.d.ts.map +1 -0
  51. package/dist/error-codes.js +2 -0
  52. package/dist/error-codes.js.map +1 -0
  53. package/dist/file-api.d.ts +12 -0
  54. package/dist/file-api.d.ts.map +1 -0
  55. package/dist/file-api.js +76 -0
  56. package/dist/file-api.js.map +1 -0
  57. package/dist/http-api.d.ts +94 -0
  58. package/dist/http-api.d.ts.map +1 -0
  59. package/dist/http-api.js +1472 -0
  60. package/dist/http-api.js.map +1 -0
  61. package/dist/index.d.ts +6 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +6 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/lib/asset-paths.d.ts +10 -0
  66. package/dist/lib/asset-paths.d.ts.map +1 -0
  67. package/dist/lib/asset-paths.js +45 -0
  68. package/dist/lib/asset-paths.js.map +1 -0
  69. package/dist/lib/bridge.d.ts +8 -0
  70. package/dist/lib/bridge.d.ts.map +1 -0
  71. package/dist/lib/bridge.js +70 -0
  72. package/dist/lib/bridge.js.map +1 -0
  73. package/dist/mcp-server.d.ts +32 -0
  74. package/dist/mcp-server.d.ts.map +1 -0
  75. package/dist/mcp-server.js +842 -0
  76. package/dist/mcp-server.js.map +1 -0
  77. package/dist/models-api.d.ts +12 -0
  78. package/dist/models-api.d.ts.map +1 -0
  79. package/dist/models-api.js +730 -0
  80. package/dist/models-api.js.map +1 -0
  81. package/dist/oauth-api.d.ts +28 -0
  82. package/dist/oauth-api.d.ts.map +1 -0
  83. package/dist/oauth-api.js +681 -0
  84. package/dist/oauth-api.js.map +1 -0
  85. package/dist/openai-api.d.ts +63 -0
  86. package/dist/openai-api.d.ts.map +1 -0
  87. package/dist/openai-api.js +359 -0
  88. package/dist/openai-api.js.map +1 -0
  89. package/dist/plugins/websocket.d.ts +14 -0
  90. package/dist/plugins/websocket.d.ts.map +1 -0
  91. package/dist/plugins/websocket.js +431 -0
  92. package/dist/plugins/websocket.js.map +1 -0
  93. package/dist/resolve-media-urls.d.ts +19 -0
  94. package/dist/resolve-media-urls.d.ts.map +1 -0
  95. package/dist/resolve-media-urls.js +109 -0
  96. package/dist/resolve-media-urls.js.map +1 -0
  97. package/dist/routes/assets.d.ts +22 -0
  98. package/dist/routes/assets.d.ts.map +1 -0
  99. package/dist/routes/assets.js +168 -0
  100. package/dist/routes/assets.js.map +1 -0
  101. package/dist/routes/collections.d.ts +16 -0
  102. package/dist/routes/collections.d.ts.map +1 -0
  103. package/dist/routes/collections.js +32 -0
  104. package/dist/routes/collections.js.map +1 -0
  105. package/dist/routes/files.d.ts +12 -0
  106. package/dist/routes/files.d.ts.map +1 -0
  107. package/dist/routes/files.js +11 -0
  108. package/dist/routes/files.js.map +1 -0
  109. package/dist/routes/health.d.ts +4 -0
  110. package/dist/routes/health.d.ts.map +1 -0
  111. package/dist/routes/health.js +41 -0
  112. package/dist/routes/health.js.map +1 -0
  113. package/dist/routes/jobs.d.ts +16 -0
  114. package/dist/routes/jobs.d.ts.map +1 -0
  115. package/dist/routes/jobs.js +25 -0
  116. package/dist/routes/jobs.js.map +1 -0
  117. package/dist/routes/nodes.d.ts +8 -0
  118. package/dist/routes/nodes.d.ts.map +1 -0
  119. package/dist/routes/nodes.js +124 -0
  120. package/dist/routes/nodes.js.map +1 -0
  121. package/dist/routes/oauth.d.ts +8 -0
  122. package/dist/routes/oauth.d.ts.map +1 -0
  123. package/dist/routes/oauth.js +19 -0
  124. package/dist/routes/oauth.js.map +1 -0
  125. package/dist/routes/openai.d.ts +8 -0
  126. package/dist/routes/openai.d.ts.map +1 -0
  127. package/dist/routes/openai.js +20 -0
  128. package/dist/routes/openai.js.map +1 -0
  129. package/dist/routes/storage.d.ts +12 -0
  130. package/dist/routes/storage.d.ts.map +1 -0
  131. package/dist/routes/storage.js +13 -0
  132. package/dist/routes/storage.js.map +1 -0
  133. package/dist/routes/workflows.d.ts +18 -0
  134. package/dist/routes/workflows.d.ts.map +1 -0
  135. package/dist/routes/workflows.js +25 -0
  136. package/dist/routes/workflows.js.map +1 -0
  137. package/dist/routes/workspace.d.ts +15 -0
  138. package/dist/routes/workspace.d.ts.map +1 -0
  139. package/dist/routes/workspace.js +24 -0
  140. package/dist/routes/workspace.js.map +1 -0
  141. package/dist/screenshot-server.d.ts +21 -0
  142. package/dist/screenshot-server.d.ts.map +1 -0
  143. package/dist/screenshot-server.js +222 -0
  144. package/dist/screenshot-server.js.map +1 -0
  145. package/dist/server.d.ts +10 -0
  146. package/dist/server.d.ts.map +1 -0
  147. package/dist/server.js +783 -0
  148. package/dist/server.js.map +1 -0
  149. package/dist/settings-registry.d.ts +29 -0
  150. package/dist/settings-registry.d.ts.map +1 -0
  151. package/dist/settings-registry.js +117 -0
  152. package/dist/settings-registry.js.map +1 -0
  153. package/dist/storage-api.d.ts +5 -0
  154. package/dist/storage-api.d.ts.map +1 -0
  155. package/dist/storage-api.js +221 -0
  156. package/dist/storage-api.js.map +1 -0
  157. package/dist/test-ui-server.d.ts +19 -0
  158. package/dist/test-ui-server.d.ts.map +1 -0
  159. package/dist/test-ui-server.js +1256 -0
  160. package/dist/test-ui-server.js.map +1 -0
  161. package/dist/trpc/context.d.ts +21 -0
  162. package/dist/trpc/context.d.ts.map +1 -0
  163. package/dist/trpc/context.js +10 -0
  164. package/dist/trpc/context.js.map +1 -0
  165. package/dist/trpc/error-formatter.d.ts +18 -0
  166. package/dist/trpc/error-formatter.d.ts.map +1 -0
  167. package/dist/trpc/error-formatter.js +41 -0
  168. package/dist/trpc/error-formatter.js.map +1 -0
  169. package/dist/trpc/index.d.ts +18 -0
  170. package/dist/trpc/index.d.ts.map +1 -0
  171. package/dist/trpc/index.js +13 -0
  172. package/dist/trpc/index.js.map +1 -0
  173. package/dist/trpc/middleware.d.ts +8 -0
  174. package/dist/trpc/middleware.d.ts.map +1 -0
  175. package/dist/trpc/middleware.js +12 -0
  176. package/dist/trpc/middleware.js.map +1 -0
  177. package/dist/trpc/router.d.ts +2543 -0
  178. package/dist/trpc/router.d.ts.map +1 -0
  179. package/dist/trpc/router.js +43 -0
  180. package/dist/trpc/router.js.map +1 -0
  181. package/dist/trpc/routers/assets.d.ts +229 -0
  182. package/dist/trpc/routers/assets.d.ts.map +1 -0
  183. package/dist/trpc/routers/assets.js +263 -0
  184. package/dist/trpc/routers/assets.js.map +1 -0
  185. package/dist/trpc/routers/collections.d.ts +88 -0
  186. package/dist/trpc/routers/collections.d.ts.map +1 -0
  187. package/dist/trpc/routers/collections.js +166 -0
  188. package/dist/trpc/routers/collections.js.map +1 -0
  189. package/dist/trpc/routers/costs.d.ts +129 -0
  190. package/dist/trpc/routers/costs.d.ts.map +1 -0
  191. package/dist/trpc/routers/costs.js +95 -0
  192. package/dist/trpc/routers/costs.js.map +1 -0
  193. package/dist/trpc/routers/files.d.ts +34 -0
  194. package/dist/trpc/routers/files.d.ts.map +1 -0
  195. package/dist/trpc/routers/files.js +92 -0
  196. package/dist/trpc/routers/files.js.map +1 -0
  197. package/dist/trpc/routers/jobs.d.ts +92 -0
  198. package/dist/trpc/routers/jobs.d.ts.map +1 -0
  199. package/dist/trpc/routers/jobs.js +100 -0
  200. package/dist/trpc/routers/jobs.js.map +1 -0
  201. package/dist/trpc/routers/mcp-config.d.ts +69 -0
  202. package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
  203. package/dist/trpc/routers/mcp-config.js +290 -0
  204. package/dist/trpc/routers/mcp-config.js.map +1 -0
  205. package/dist/trpc/routers/messages.d.ts +121 -0
  206. package/dist/trpc/routers/messages.d.ts.map +1 -0
  207. package/dist/trpc/routers/messages.js +113 -0
  208. package/dist/trpc/routers/messages.js.map +1 -0
  209. package/dist/trpc/routers/models.d.ts +748 -0
  210. package/dist/trpc/routers/models.d.ts.map +1 -0
  211. package/dist/trpc/routers/models.js +1107 -0
  212. package/dist/trpc/routers/models.js.map +1 -0
  213. package/dist/trpc/routers/nodes.d.ts +48 -0
  214. package/dist/trpc/routers/nodes.d.ts.map +1 -0
  215. package/dist/trpc/routers/nodes.js +57 -0
  216. package/dist/trpc/routers/nodes.js.map +1 -0
  217. package/dist/trpc/routers/sandboxes.d.ts +64 -0
  218. package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
  219. package/dist/trpc/routers/sandboxes.js +327 -0
  220. package/dist/trpc/routers/sandboxes.js.map +1 -0
  221. package/dist/trpc/routers/settings.d.ts +113 -0
  222. package/dist/trpc/routers/settings.d.ts.map +1 -0
  223. package/dist/trpc/routers/settings.js +193 -0
  224. package/dist/trpc/routers/settings.js.map +1 -0
  225. package/dist/trpc/routers/skills.d.ts +41 -0
  226. package/dist/trpc/routers/skills.d.ts.map +1 -0
  227. package/dist/trpc/routers/skills.js +194 -0
  228. package/dist/trpc/routers/skills.js.map +1 -0
  229. package/dist/trpc/routers/storage.d.ts +44 -0
  230. package/dist/trpc/routers/storage.d.ts.map +1 -0
  231. package/dist/trpc/routers/storage.js +153 -0
  232. package/dist/trpc/routers/storage.js.map +1 -0
  233. package/dist/trpc/routers/threads.d.ts +95 -0
  234. package/dist/trpc/routers/threads.d.ts.map +1 -0
  235. package/dist/trpc/routers/threads.js +150 -0
  236. package/dist/trpc/routers/threads.js.map +1 -0
  237. package/dist/trpc/routers/users.d.ts +79 -0
  238. package/dist/trpc/routers/users.d.ts.map +1 -0
  239. package/dist/trpc/routers/users.js +125 -0
  240. package/dist/trpc/routers/users.js.map +1 -0
  241. package/dist/trpc/routers/workflows.d.ts +681 -0
  242. package/dist/trpc/routers/workflows.d.ts.map +1 -0
  243. package/dist/trpc/routers/workflows.js +725 -0
  244. package/dist/trpc/routers/workflows.js.map +1 -0
  245. package/dist/trpc/routers/workspace.d.ts +127 -0
  246. package/dist/trpc/routers/workspace.d.ts.map +1 -0
  247. package/dist/trpc/routers/workspace.js +203 -0
  248. package/dist/trpc/routers/workspace.js.map +1 -0
  249. package/dist/unified-websocket-runner.d.ts +280 -0
  250. package/dist/unified-websocket-runner.d.ts.map +1 -0
  251. package/dist/unified-websocket-runner.js +3840 -0
  252. package/dist/unified-websocket-runner.js.map +1 -0
  253. package/dist/workspace-api.d.ts +14 -0
  254. package/dist/workspace-api.d.ts.map +1 -0
  255. package/dist/workspace-api.js +104 -0
  256. package/dist/workspace-api.js.map +1 -0
  257. package/dist/ws-adapter.d.ts +25 -0
  258. package/dist/ws-adapter.d.ts.map +1 -0
  259. package/dist/ws-adapter.js +63 -0
  260. package/dist/ws-adapter.js.map +1 -0
  261. package/package.json +92 -0
@@ -0,0 +1,1256 @@
1
+ import { createServer } from "node:http";
2
+ import { createLogger, getDefaultDbPath } from "@nodetool-ai/config";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ import { spawnSync } from "node:child_process";
5
+ import path from "node:path";
6
+ import { WebSocketServer } from "ws";
7
+ import { NodeRegistry, createGraphNodeTypeResolver } from "@nodetool-ai/node-sdk";
8
+ import { registerBaseNodes } from "@nodetool-ai/base-nodes";
9
+ import { registerElevenLabsNodes } from "@nodetool-ai/elevenlabs-nodes";
10
+ import { registerTransformersJsNodes } from "@nodetool-ai/transformers-js-nodes";
11
+ import { UnifiedWebSocketRunner } from "./unified-websocket-runner.js";
12
+ import { ScriptedProvider, autoScript } from "@nodetool-ai/runtime";
13
+ import { handleNodeHttpRequest } from "./http-api.js";
14
+ import { initDb } from "@nodetool-ai/models";
15
+ const log = createLogger("nodetool.websocket.server");
16
+ function htmlPage() {
17
+ return `<!doctype html>
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="utf-8" />
21
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
22
+ <title>NodeTool Pipeline Tester</title>
23
+ <link rel="preconnect" href="https://fonts.googleapis.com">
24
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
26
+ <style>
27
+ html, body, #root { margin: 0; min-height: 100%; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div id="root"></div>
32
+ <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
33
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
34
+ <script src="https://unpkg.com/@mui/material@5.15.19/umd/material-ui.development.js" crossorigin></script>
35
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
36
+ <script type="text/babel" data-presets="env,react">
37
+ function normalizeType(raw) {
38
+ const t = String(raw || '').toLowerCase();
39
+ if (!t) return 'any';
40
+ if (['any', 'object', 'unknown'].includes(t)) return 'any';
41
+ if (['string', 'str', 'text'].includes(t)) return 'str';
42
+ if (['int', 'integer'].includes(t)) return 'int';
43
+ if (['float', 'double', 'number'].includes(t)) return 'float';
44
+ if (['bool', 'boolean'].includes(t)) return 'bool';
45
+ if (['list', 'array'].includes(t)) return 'list';
46
+ if (['dict', 'dictionary', 'map'].includes(t)) return 'dict';
47
+ return t;
48
+ }
49
+
50
+ function outputType(out) {
51
+ return normalizeType(out?.type?.type);
52
+ }
53
+
54
+ function inputType(prop) {
55
+ return normalizeType(prop?.type?.type);
56
+ }
57
+
58
+ function isTypeCompatible(fromType, toType) {
59
+ if (fromType === 'any' || toType === 'any') return true;
60
+ if (fromType === toType) return true;
61
+ if (fromType === 'int' && toType === 'float') return true;
62
+ return false;
63
+ }
64
+
65
+ function chooseLink(aMd, bMd) {
66
+ const outputs = Array.isArray(aMd?.outputs) ? aMd.outputs : [];
67
+ const inputs = Array.isArray(bMd?.properties) ? bMd.properties : [];
68
+ if (!outputs.length || !inputs.length) {
69
+ return {
70
+ sourceHandle: outputs[0]?.name || 'output',
71
+ targetHandle: inputs[0]?.name || 'input',
72
+ warning: 'Fallback handle mapping due to missing metadata slots',
73
+ };
74
+ }
75
+
76
+ for (const out of outputs) {
77
+ for (const inp of inputs) {
78
+ const ot = outputType(out);
79
+ const it = inputType(inp);
80
+ if (isTypeCompatible(ot, it)) {
81
+ return { sourceHandle: out.name, targetHandle: inp.name, warning: null };
82
+ }
83
+ }
84
+ }
85
+
86
+ return {
87
+ sourceHandle: outputs[0].name,
88
+ targetHandle: inputs[0].name,
89
+ warning: 'No compatible type match (' + outputType(outputs[0]) + ' -> ' + inputType(inputs[0]) + '), using first handles',
90
+ };
91
+ }
92
+
93
+ const OUTPUT_NODE_TYPE = 'nodetool.output.Output';
94
+
95
+ const {
96
+ Alert, Box, Button, Chip, CssBaseline, Divider, FormControlLabel, Grid, LinearProgress, List,
97
+ ListItemButton, ListItemText, MenuItem, Paper, Stack, TextField, ThemeProvider, Typography, Checkbox, createTheme
98
+ } = MaterialUI;
99
+ const { useEffect, useMemo, useRef, useState } = React;
100
+
101
+ function propertyEnumValues(prop) {
102
+ if (Array.isArray(prop?.type?.values) && prop.type.values.length > 0) return prop.type.values;
103
+ if (Array.isArray(prop?.type?.type_args?.[0]?.values) && prop.type.type_args[0].values.length > 0) {
104
+ return prop.type.type_args[0].values;
105
+ }
106
+ if (Array.isArray(prop?.values) && prop.values.length > 0) return prop.values;
107
+ if (Array.isArray(prop?.enum) && prop.enum.length > 0) return prop.enum;
108
+ return [];
109
+ }
110
+
111
+ function inputEditorKind(prop) {
112
+ const t = String(prop?.type?.type || 'str').toLowerCase();
113
+ const enumValues = propertyEnumValues(prop);
114
+ if (enumValues.length > 0 || t === 'enum' || t === 'select') return 'enum';
115
+ if (t === 'bool' || t === 'boolean') return 'checkbox';
116
+ if (['int', 'integer', 'float', 'number'].includes(t)) return 'number';
117
+ if (['text'].includes(t)) return 'multiline';
118
+ if (['list', 'dict', 'json', 'record_type', 'dataframe', 'image_size'].includes(t)) return 'json';
119
+ if (t === 'union') return 'json';
120
+ return 'text';
121
+ }
122
+
123
+ function normalizePropertyValue(prop, value) {
124
+ const kind = inputEditorKind(prop);
125
+ if (kind === 'checkbox') return Boolean(value);
126
+ if (kind === 'number') {
127
+ if (value === '' || value === null || value === undefined) return null;
128
+ const n = Number(value);
129
+ if (!Number.isFinite(n)) return value;
130
+ const t = String(prop?.type?.type || '').toLowerCase();
131
+ return (t === 'int' || t === 'integer') ? Math.trunc(n) : n;
132
+ }
133
+ if (kind === 'json') {
134
+ if (value === null || value === undefined) return null;
135
+ if (typeof value !== 'string') return value;
136
+ const raw = value.trim();
137
+ if (!raw) return null;
138
+ try {
139
+ return JSON.parse(raw);
140
+ } catch {
141
+ return value;
142
+ }
143
+ }
144
+ return value;
145
+ }
146
+
147
+ function App() {
148
+ const [metadata, setMetadata] = useState([]);
149
+ const [search, setSearch] = useState('');
150
+ const [selectedNodeType, setSelectedNodeType] = useState('');
151
+ const [steps, setSteps] = useState([]);
152
+ const [savedWorkflows, setSavedWorkflows] = useState([]);
153
+ const [selectedWorkflowId, setSelectedWorkflowId] = useState('');
154
+ const [examples, setExamples] = useState([]);
155
+ const [selectedExampleId, setSelectedExampleId] = useState('');
156
+ const [importedGraph, setImportedGraph] = useState(null);
157
+ const [importedGraphName, setImportedGraphName] = useState('');
158
+ const [workflowName, setWorkflowName] = useState('Quick Test Pipeline');
159
+ const [paramsText, setParamsText] = useState('');
160
+ const [fixtureKey, setFixtureKey] = useState('text');
161
+ const [statusText, setStatusText] = useState('Idle');
162
+ const [logs, setLogs] = useState([]);
163
+ const [runEvents, setRunEvents] = useState([]);
164
+ const [outputEvents, setOutputEvents] = useState([]);
165
+ const [jobId, setJobId] = useState(null);
166
+ const [fixtureFile, setFixtureFile] = useState(null);
167
+ const [isRunning, setIsRunning] = useState(false);
168
+ const [elapsedTime, setElapsedTime] = useState(0);
169
+ const wsRef = useRef(null);
170
+ const startTimeRef = useRef(null);
171
+
172
+ const log = (msg, type) => {
173
+ const time = new Date().toLocaleTimeString();
174
+ setLogs((prev) => [{ text: '[' + time + '] ' + msg, type: type || 'info' }, ...prev].slice(0, 200));
175
+ };
176
+
177
+ const filtered = useMemo(() => {
178
+ const q = search.trim().toLowerCase();
179
+ return metadata.filter((n) => (
180
+ !q ||
181
+ String(n.title || '').toLowerCase().includes(q) ||
182
+ String(n.node_type || '').toLowerCase().includes(q) ||
183
+ String(n.namespace || '').toLowerCase().includes(q)
184
+ ));
185
+ }, [metadata, search]);
186
+
187
+ const graphData = useMemo(() => {
188
+ if (importedGraph && Array.isArray(importedGraph.nodes) && Array.isArray(importedGraph.edges)) {
189
+ return { graph: importedGraph, warnings: ['Loaded example graph (read-only in builder mode)'] };
190
+ }
191
+ const state = { metadata, steps };
192
+ return compileGraph(state);
193
+ }, [metadata, steps, importedGraph]);
194
+
195
+ const summaryText = 'events=' + runEvents.length + ', outputs=' + outputEvents.length;
196
+ const lastOutput = outputEvents[outputEvents.length - 1];
197
+ const formatDuration = (seconds) => {
198
+ if (seconds < 1) return Math.round(seconds * 1000) + 'ms';
199
+ if (seconds < 60) return seconds.toFixed(1) + 's';
200
+ const minutes = Math.floor(seconds / 60);
201
+ const remain = Math.round(seconds % 60);
202
+ return minutes + 'm ' + remain + 's';
203
+ };
204
+
205
+ async function refreshSavedWorkflows() {
206
+ const res = await fetch('/api/workflows?run_mode=test&limit=200');
207
+ if (!res.ok) throw new Error('failed to list workflows: ' + res.status);
208
+ const body = await res.json();
209
+ setSavedWorkflows(Array.isArray(body.workflows) ? body.workflows : []);
210
+ }
211
+
212
+ async function refreshExamples() {
213
+ const res = await fetch('/api/examples');
214
+ if (!res.ok) throw new Error('failed to list examples: ' + res.status);
215
+ const body = await res.json();
216
+ setExamples(Array.isArray(body.examples) ? body.examples : []);
217
+ }
218
+
219
+ async function loadMetadata() {
220
+ const res = await fetch('/api/node/metadata');
221
+ if (!res.ok) throw new Error('metadata fetch failed: ' + res.status);
222
+ const all = await res.json();
223
+ const filteredAll = all.filter((n) => n && typeof n.node_type === 'string');
224
+ setMetadata(filteredAll);
225
+ log('Loaded ' + filteredAll.length + ' node metadata entries', 'ok');
226
+ await refreshSavedWorkflows();
227
+ await refreshExamples();
228
+ }
229
+
230
+ useEffect(() => {
231
+ loadMetadata().catch((err) => log(String(err), 'err'));
232
+ }, []);
233
+
234
+ useEffect(() => {
235
+ if (!isRunning) {
236
+ startTimeRef.current = null;
237
+ return;
238
+ }
239
+ startTimeRef.current = Date.now();
240
+ setElapsedTime(0);
241
+ const interval = setInterval(() => {
242
+ if (startTimeRef.current) {
243
+ setElapsedTime((Date.now() - startTimeRef.current) / 1000);
244
+ }
245
+ }, 100);
246
+ return () => clearInterval(interval);
247
+ }, [isRunning]);
248
+
249
+ async function connectWs() {
250
+ if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
251
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
252
+ const ws = new WebSocket(proto + '//' + location.host + '/ws');
253
+ return await new Promise((resolve, reject) => {
254
+ ws.onopen = () => {
255
+ wsRef.current = ws;
256
+ ws.send(JSON.stringify({ command: 'set_mode', data: { mode: 'text' } }));
257
+ log('WS connected', 'ok');
258
+ resolve(ws);
259
+ };
260
+ ws.onerror = (err) => reject(err);
261
+ ws.onclose = () => {
262
+ log('WS disconnected', 'err');
263
+ if (wsRef.current === ws) wsRef.current = null;
264
+ setIsRunning(false);
265
+ };
266
+ ws.onmessage = (event) => {
267
+ let data = null;
268
+ try { data = JSON.parse(event.data); } catch { return; }
269
+ setRunEvents((prev) => [...prev, data]);
270
+ if (data.type === 'output_update') {
271
+ setOutputEvents((prev) => [...prev, data]);
272
+ }
273
+ if (data.type === 'job_update') {
274
+ if (typeof data.job_id === 'string' && data.job_id) setJobId(data.job_id);
275
+ const status = String(data.status || '');
276
+ if (status === 'running') setIsRunning(true);
277
+ if (['completed', 'failed', 'cancelled'].includes(status)) {
278
+ setJobId(null);
279
+ setIsRunning(false);
280
+ }
281
+ setStatusText('Job ' + (data.job_id || '') + ': ' + data.status + (data.error ? (' (' + data.error + ')') : ''));
282
+ }
283
+ if (data.error) log('Error: ' + data.error, 'err');
284
+ else if (data.message) log(data.message, 'ok');
285
+ else if (data.type) log(data.type + (data.status ? (' -> ' + data.status) : ''), 'info');
286
+ };
287
+ });
288
+ }
289
+
290
+ async function runPipeline() {
291
+ try {
292
+ const ws = await connectWs();
293
+ setJobId(null);
294
+ setRunEvents([]);
295
+ setOutputEvents([]);
296
+ setIsRunning(true);
297
+ const raw = paramsText.trim();
298
+ const params = raw ? JSON.parse(raw) : {};
299
+ if (!steps.length) {
300
+ log('Add at least one step', 'err');
301
+ setIsRunning(false);
302
+ return;
303
+ }
304
+ ws.send(JSON.stringify({
305
+ command: 'run_job',
306
+ data: { graph: graphData.graph, params }
307
+ }));
308
+ setStatusText('Submitted run request...');
309
+ } catch (err) {
310
+ log('Run failed: ' + String(err), 'err');
311
+ setIsRunning(false);
312
+ }
313
+ }
314
+
315
+ async function cancelRun() {
316
+ const ws = wsRef.current;
317
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
318
+ if (!jobId) {
319
+ log('No active job to cancel yet', 'err');
320
+ return;
321
+ }
322
+ ws.send(JSON.stringify({ command: 'cancel_job', data: { job_id: jobId } }));
323
+ log('Cancellation requested for job ' + jobId, 'info');
324
+ }
325
+
326
+ async function saveCurrentWorkflow() {
327
+ const name = (workflowName || '').trim() || 'Quick Test Pipeline';
328
+ if (!steps.length) {
329
+ log('Cannot save empty pipeline', 'err');
330
+ return;
331
+ }
332
+ const payload = {
333
+ name,
334
+ access: 'private',
335
+ description: 'Saved from TS test UI',
336
+ run_mode: 'test',
337
+ graph: graphData.graph,
338
+ tags: ['test-ui']
339
+ };
340
+ const res = await fetch('/api/workflows', {
341
+ method: 'POST',
342
+ headers: { 'content-type': 'application/json' },
343
+ body: JSON.stringify(payload)
344
+ });
345
+ if (!res.ok) {
346
+ const detail = await res.text();
347
+ throw new Error('save failed: ' + res.status + ' ' + detail);
348
+ }
349
+ const wf = await res.json();
350
+ setSelectedWorkflowId(wf.id);
351
+ log('Saved test workflow: ' + wf.name, 'ok');
352
+ await refreshSavedWorkflows();
353
+ }
354
+
355
+ async function loadSelectedExample() {
356
+ if (!selectedExampleId) {
357
+ log('Select an example first', 'err');
358
+ return;
359
+ }
360
+ const res = await fetch('/api/examples/' + encodeURIComponent(selectedExampleId));
361
+ if (!res.ok) throw new Error('example load failed: ' + res.status);
362
+ const example = await res.json();
363
+ if (!example?.graph || !Array.isArray(example.graph.nodes) || !Array.isArray(example.graph.edges)) {
364
+ throw new Error('example graph is invalid');
365
+ }
366
+ setImportedGraph(example.graph);
367
+ setImportedGraphName(typeof example.name === 'string' ? example.name : selectedExampleId);
368
+ const nodes = Array.isArray(example.graph.nodes) ? example.graph.nodes : [];
369
+ setSteps(
370
+ nodes
371
+ .filter((n) => n && n.type !== OUTPUT_NODE_TYPE)
372
+ .map((n) => ({
373
+ type: n.type,
374
+ properties:
375
+ n && typeof n === 'object' && n.data && typeof n.data === 'object'
376
+ ? n.data
377
+ : n && typeof n === 'object' && n.properties && typeof n.properties === 'object'
378
+ ? n.properties
379
+ : {}
380
+ }))
381
+ );
382
+ log('Loaded example: ' + (example.name || selectedExampleId), 'ok');
383
+ }
384
+
385
+ async function loadSelectedWorkflow() {
386
+ if (!selectedWorkflowId) {
387
+ log('Select a saved workflow first', 'err');
388
+ return;
389
+ }
390
+ const res = await fetch('/api/workflows/' + encodeURIComponent(selectedWorkflowId));
391
+ if (!res.ok) throw new Error('load failed: ' + res.status);
392
+ const wf = await res.json();
393
+ const nodes = Array.isArray(wf.graph?.nodes) ? wf.graph.nodes : [];
394
+ setSteps(
395
+ nodes
396
+ .filter((n) => n && n.type !== OUTPUT_NODE_TYPE)
397
+ .map((n) => ({
398
+ type: n.type,
399
+ properties: n.properties && typeof n.properties === 'object' ? n.properties : {}
400
+ }))
401
+ );
402
+ setImportedGraph(null);
403
+ setImportedGraphName('');
404
+ if (wf.name) setWorkflowName(wf.name);
405
+ log('Loaded workflow: ' + (wf.name || selectedWorkflowId), 'ok');
406
+ }
407
+
408
+ async function applyFixture() {
409
+ const key = (fixtureKey || '').trim();
410
+ const file = fixtureFile;
411
+ if (!key) {
412
+ log('Fixture param key is required', 'err');
413
+ return;
414
+ }
415
+ if (!file) {
416
+ log('Choose a fixture file first', 'err');
417
+ return;
418
+ }
419
+ const params = paramsText.trim() ? JSON.parse(paramsText) : {};
420
+ if (file.type.startsWith('text/') || file.name.endsWith('.txt') || file.name.endsWith('.md') || file.name.endsWith('.json')) {
421
+ params[key] = await file.text();
422
+ } else if (file.type.startsWith('image/')) {
423
+ const dataUri = await new Promise((resolve, reject) => {
424
+ const reader = new FileReader();
425
+ reader.onerror = () => reject(new Error('Failed reading image file'));
426
+ reader.onload = () => resolve(String(reader.result || ''));
427
+ reader.readAsDataURL(file);
428
+ });
429
+ params[key] = dataUri;
430
+ } else {
431
+ log('Unsupported fixture type for quick mode; use text/image', 'err');
432
+ return;
433
+ }
434
+ setParamsText(JSON.stringify(params, null, 2));
435
+ log('Fixture applied to params key: ' + key, 'ok');
436
+ }
437
+
438
+ const theme = useMemo(() => createTheme({
439
+ palette: {
440
+ mode: 'dark',
441
+ primary: { main: '#3b82f6' },
442
+ secondary: { main: '#66bb6a' },
443
+ error: { main: '#ef5350' },
444
+ background: { default: '#0b1220', paper: '#0f172a' }
445
+ },
446
+ shape: { borderRadius: 12 },
447
+ components: {
448
+ MuiPaper: {
449
+ styleOverrides: {
450
+ root: {
451
+ border: '1px solid rgba(148,163,184,0.16)',
452
+ boxShadow: '0 12px 30px rgba(2,6,23,0.28)'
453
+ }
454
+ }
455
+ }
456
+ }
457
+ }), []);
458
+
459
+ return (
460
+ <ThemeProvider theme={theme}>
461
+ <CssBaseline />
462
+ <Box sx={{
463
+ minHeight: '100vh',
464
+ p: { xs: 1.5, md: 3 },
465
+ background: 'radial-gradient(1200px 700px at 0% -15%, rgba(59,130,246,0.22) 0%, transparent 62%), radial-gradient(900px 500px at 100% -20%, rgba(16,185,129,0.16) 0%, transparent 60%), #0b1220'
466
+ }}>
467
+ <Box sx={{ maxWidth: 1700, mx: 'auto' }}>
468
+ <Stack spacing={0.75} sx={{ mb: 2 }}>
469
+ <Typography variant="h4" sx={{ fontWeight: 300 }}>NodeTool Pipeline Tester</Typography>
470
+ <Typography variant="body2" color="text.secondary">
471
+ Compose a quick test workflow, run it over websocket, and inspect outputs.
472
+ </Typography>
473
+ </Stack>
474
+ {isRunning && (
475
+ <Paper sx={{ p: 1.25, mb: 2 }}>
476
+ <Stack direction="row" spacing={1.25} alignItems="center">
477
+ <Box sx={{ flex: 1 }}>
478
+ <LinearProgress />
479
+ </Box>
480
+ <Typography variant="caption" color="text.secondary">
481
+ Running • {formatDuration(elapsedTime)}
482
+ </Typography>
483
+ </Stack>
484
+ </Paper>
485
+ )}
486
+ <Grid container spacing={2}>
487
+ <Grid item xs={12} lg={3}>
488
+ <Paper sx={{ p: 2 }}>
489
+ <Stack spacing={1.25}>
490
+ <Typography variant="h6">Pipeline Builder</Typography>
491
+ <TextField size="small" label="Search nodes" value={search} onChange={(e) => setSearch(e.target.value)} />
492
+ <List dense sx={{ maxHeight: 260, overflow: 'auto', border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
493
+ {filtered.map((md) => (
494
+ <ListItemButton key={md.node_type} selected={selectedNodeType === md.node_type} onClick={() => setSelectedNodeType(md.node_type)}>
495
+ <ListItemText primary={md.title + ' (' + md.node_type + ')'} />
496
+ </ListItemButton>
497
+ ))}
498
+ </List>
499
+ <Button
500
+ variant="contained"
501
+ disabled={Boolean(importedGraph)}
502
+ onClick={() => selectedNodeType && setSteps((prev) => [...prev, { type: selectedNodeType, properties: {} }])}
503
+ >
504
+ Add Step
505
+ </Button>
506
+ <Divider />
507
+ <Typography variant="subtitle2">Saved Tests</Typography>
508
+ <List dense sx={{ maxHeight: 180, overflow: 'auto', border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
509
+ {savedWorkflows.map((wf) => (
510
+ <ListItemButton key={wf.id} selected={selectedWorkflowId === wf.id} onClick={() => setSelectedWorkflowId(wf.id)}>
511
+ <ListItemText primary={wf.name + ' (' + String(wf.id).slice(0, 8) + ')'} />
512
+ </ListItemButton>
513
+ ))}
514
+ </List>
515
+ <Typography variant="subtitle2">NodeTool-Base Examples</Typography>
516
+ <List dense sx={{ maxHeight: 200, overflow: 'auto', border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
517
+ {examples.map((ex) => (
518
+ <ListItemButton key={ex.id} selected={selectedExampleId === ex.id} onClick={() => setSelectedExampleId(ex.id)}>
519
+ <ListItemText
520
+ primary={ex.name || ex.id}
521
+ secondary={ex.description || ''}
522
+ secondaryTypographyProps={{ sx: { display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical', overflow: 'hidden' } }}
523
+ />
524
+ </ListItemButton>
525
+ ))}
526
+ </List>
527
+ <Button variant="outlined" onClick={() => void loadSelectedExample().catch((err) => log(String(err), 'err'))}>Load Example</Button>
528
+ {importedGraph && (
529
+ <Alert
530
+ severity="info"
531
+ action={<Button size="small" color="inherit" onClick={() => { setImportedGraph(null); setImportedGraphName(''); }}>Use Builder</Button>}
532
+ >
533
+ Using imported example graph: {importedGraphName || selectedExampleId}
534
+ </Alert>
535
+ )}
536
+ <TextField size="small" label="Workflow name" value={workflowName} onChange={(e) => setWorkflowName(e.target.value)} />
537
+ <Stack direction="row" spacing={1}>
538
+ <Button fullWidth variant="outlined" onClick={() => void saveCurrentWorkflow().catch((err) => log(String(err), 'err'))}>Save Test</Button>
539
+ <Button fullWidth variant="outlined" onClick={() => void loadSelectedWorkflow().catch((err) => log(String(err), 'err'))}>Load</Button>
540
+ </Stack>
541
+ <Typography variant="subtitle2">Run Params (JSON)</Typography>
542
+ <TextField multiline minRows={7} value={paramsText} onChange={(e) => setParamsText(e.target.value)} placeholder='{"text":"hello"}' />
543
+ <Typography variant="subtitle2">Asset Fixture</Typography>
544
+ <TextField size="small" label="Param key" value={fixtureKey} onChange={(e) => setFixtureKey(e.target.value)} />
545
+ <Button variant="outlined" component="label">
546
+ {fixtureFile ? fixtureFile.name : 'Choose fixture file'}
547
+ <input hidden type="file" onChange={(e) => setFixtureFile(e.target.files && e.target.files[0] ? e.target.files[0] : null)} />
548
+ </Button>
549
+ <Button variant="outlined" onClick={() => void applyFixture().catch((err) => log(String(err), 'err'))}>Apply Fixture To Params</Button>
550
+ <Typography variant="caption" color="text.secondary">Text files become plain strings, images become data URIs.</Typography>
551
+ <Stack direction="row" spacing={1}>
552
+ <Button fullWidth variant="contained" color={isRunning ? 'warning' : 'secondary'} onClick={isRunning ? cancelRun : runPipeline}>
553
+ {isRunning ? 'Stop' : 'Run Pipeline'}
554
+ </Button>
555
+ <Button fullWidth variant="contained" color="error" onClick={cancelRun} disabled={!isRunning}>Cancel</Button>
556
+ </Stack>
557
+ <Typography variant="body2" color="text.secondary">{statusText}</Typography>
558
+ </Stack>
559
+ </Paper>
560
+ </Grid>
561
+ <Grid item xs={12} lg={5}>
562
+ <Paper sx={{ p: 2 }}>
563
+ <Stack spacing={1.25}>
564
+ <Typography variant="h6">Steps</Typography>
565
+ {steps.length === 0 && <Typography variant="body2" color="text.secondary">No steps yet</Typography>}
566
+ {steps.map((step, idx) => {
567
+ const md = metadata.find((m) => m.node_type === step.type);
568
+ const propList = md?.properties || [];
569
+ return (
570
+ <Paper key={idx} variant="outlined" sx={{ p: 1.25 }}>
571
+ <Stack spacing={1}>
572
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
573
+ <Typography variant="subtitle2">{(idx + 1) + '. ' + (md?.title || step.type)}</Typography>
574
+ <Chip size="small" label={md?.namespace || ''} />
575
+ </Stack>
576
+ <Typography variant="caption" color="text.secondary">{step.type}</Typography>
577
+ {propList.map((prop) => {
578
+ const kind = inputEditorKind(prop);
579
+ const current = step.properties[prop.name] ?? prop.default ?? '';
580
+ const enumValues = propertyEnumValues(prop);
581
+ if (kind === 'checkbox') {
582
+ return (
583
+ <FormControlLabel
584
+ key={prop.name}
585
+ control={
586
+ <Checkbox
587
+ checked={Boolean(current)}
588
+ disabled={Boolean(importedGraph)}
589
+ onChange={(e) => {
590
+ setSteps((prev) => prev.map((s, i) => i === idx ? { ...s, properties: { ...s.properties, [prop.name]: e.target.checked } } : s));
591
+ }}
592
+ />
593
+ }
594
+ label={prop.name}
595
+ />
596
+ );
597
+ }
598
+ if (kind === 'enum') {
599
+ const value = current === null || current === undefined ? '' : String(current);
600
+ return (
601
+ <TextField
602
+ key={prop.name}
603
+ size="small"
604
+ select
605
+ label={prop.name}
606
+ value={value}
607
+ disabled={Boolean(importedGraph)}
608
+ onChange={(e) => {
609
+ setSteps((prev) => prev.map((s, i) => i === idx ? { ...s, properties: { ...s.properties, [prop.name]: e.target.value } } : s));
610
+ }}
611
+ >
612
+ {enumValues.map((opt, optIdx) => (
613
+ <MenuItem key={String(opt) + ':' + optIdx} value={String(opt)}>{String(opt)}</MenuItem>
614
+ ))}
615
+ </TextField>
616
+ );
617
+ }
618
+ if (kind === 'json') {
619
+ const value = typeof current === 'string' ? current : JSON.stringify(current, null, 2);
620
+ return (
621
+ <TextField
622
+ key={prop.name}
623
+ size="small"
624
+ multiline
625
+ minRows={3}
626
+ label={prop.name}
627
+ value={value}
628
+ disabled={Boolean(importedGraph)}
629
+ onChange={(e) => {
630
+ setSteps((prev) => prev.map((s, i) => i === idx ? { ...s, properties: { ...s.properties, [prop.name]: e.target.value } } : s));
631
+ }}
632
+ />
633
+ );
634
+ }
635
+ return (
636
+ <TextField
637
+ key={prop.name}
638
+ size="small"
639
+ type={kind === 'multiline' ? 'text' : kind}
640
+ multiline={kind === 'multiline'}
641
+ minRows={kind === 'multiline' ? 3 : undefined}
642
+ label={prop.name}
643
+ value={current === null || current === undefined ? '' : String(current)}
644
+ disabled={Boolean(importedGraph)}
645
+ onChange={(e) => {
646
+ setSteps((prev) => prev.map((s, i) => i === idx ? { ...s, properties: { ...s.properties, [prop.name]: e.target.value } } : s));
647
+ }}
648
+ />
649
+ );
650
+ })}
651
+ <Stack direction="row" spacing={1}>
652
+ <Button
653
+ fullWidth
654
+ variant="outlined"
655
+ disabled={Boolean(importedGraph)}
656
+ onClick={() => {
657
+ if (idx === 0) return;
658
+ setSteps((prev) => {
659
+ const next = [...prev];
660
+ const x = next[idx - 1];
661
+ next[idx - 1] = next[idx];
662
+ next[idx] = x;
663
+ return next;
664
+ });
665
+ }}
666
+ >Up</Button>
667
+ <Button
668
+ fullWidth
669
+ variant="outlined"
670
+ disabled={Boolean(importedGraph)}
671
+ onClick={() => {
672
+ if (idx === steps.length - 1) return;
673
+ setSteps((prev) => {
674
+ const next = [...prev];
675
+ const x = next[idx + 1];
676
+ next[idx + 1] = next[idx];
677
+ next[idx] = x;
678
+ return next;
679
+ });
680
+ }}
681
+ >Down</Button>
682
+ <Button fullWidth variant="contained" color="error" disabled={Boolean(importedGraph)} onClick={() => setSteps((prev) => prev.filter((_, i) => i !== idx))}>Remove</Button>
683
+ </Stack>
684
+ </Stack>
685
+ </Paper>
686
+ );
687
+ })}
688
+ <Typography variant="subtitle2">Connection Warnings</Typography>
689
+ {graphData.warnings.length ? (
690
+ <Stack spacing={0.75}>
691
+ {graphData.warnings.map((w, i) => <Alert key={i} severity="warning">{w}</Alert>)}
692
+ </Stack>
693
+ ) : (
694
+ <Typography variant="body2" color="text.secondary">No connection warnings</Typography>
695
+ )}
696
+ <Typography variant="subtitle2">Compiled Graph</Typography>
697
+ <Box component="pre" sx={{ m: 0, p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, backgroundColor: 'rgba(0,0,0,0.25)', maxHeight: 300, overflow: 'auto', fontSize: 12 }}>
698
+ {JSON.stringify(graphData.graph, null, 2)}
699
+ </Box>
700
+ </Stack>
701
+ </Paper>
702
+ </Grid>
703
+ <Grid item xs={12} lg={4}>
704
+ <Paper sx={{ p: 2 }}>
705
+ <Stack spacing={1.25}>
706
+ <Typography variant="h6">Run Output</Typography>
707
+ <Typography variant="caption" color="text.secondary">{summaryText || 'No run yet'}</Typography>
708
+ <Typography variant="subtitle2">Output Updates</Typography>
709
+ <Box component="pre" sx={{ m: 0, p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, backgroundColor: 'rgba(0,0,0,0.25)', maxHeight: 220, overflow: 'auto', fontSize: 12 }}>
710
+ {JSON.stringify(outputEvents, null, 2)}
711
+ </Box>
712
+ <Typography variant="subtitle2">Preview</Typography>
713
+ {!lastOutput && <Typography variant="body2" color="text.secondary">No output yet</Typography>}
714
+ {lastOutput && typeof lastOutput.value === 'string' && lastOutput.value.startsWith('data:image/') && (
715
+ <Box component="img" src={lastOutput.value} sx={{ width: '100%', borderRadius: 1, border: '1px solid', borderColor: 'divider' }} />
716
+ )}
717
+ {lastOutput && !(typeof lastOutput.value === 'string' && lastOutput.value.startsWith('data:image/')) && (
718
+ <Box component="pre" sx={{ m: 0, p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, backgroundColor: 'rgba(0,0,0,0.25)', maxHeight: 220, overflow: 'auto', fontSize: 12 }}>
719
+ {typeof lastOutput.value === 'string' ? lastOutput.value : JSON.stringify(lastOutput.value, null, 2)}
720
+ </Box>
721
+ )}
722
+ <Typography variant="subtitle2">Event Log</Typography>
723
+ <List dense sx={{ maxHeight: 320, overflow: 'auto', border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
724
+ {logs.map((entry, idx) => (
725
+ <ListItemText
726
+ key={idx}
727
+ primaryTypographyProps={{
728
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
729
+ fontSize: 12,
730
+ color: entry.type === 'err' ? 'error.main' : entry.type === 'ok' ? 'success.main' : 'text.primary',
731
+ sx: { px: 1, py: 0.4 }
732
+ }}
733
+ primary={entry.text}
734
+ />
735
+ ))}
736
+ </List>
737
+ </Stack>
738
+ </Paper>
739
+ </Grid>
740
+ </Grid>
741
+ </Box>
742
+ </Box>
743
+ </ThemeProvider>
744
+ );
745
+ }
746
+
747
+ function compileGraph(state) {
748
+ const warnings = [];
749
+ const userNodes = state.steps.map((step, i) => {
750
+ const id = 'n' + (i + 1);
751
+ const md = state.metadata.find((m) => m.node_type === step.type);
752
+ const propDefs = Array.isArray(md?.properties) ? md.properties : [];
753
+ const normalizedProps = { ...step.properties };
754
+ for (const prop of propDefs) {
755
+ if (!(prop.name in normalizedProps)) continue;
756
+ normalizedProps[prop.name] = normalizePropertyValue(prop, normalizedProps[prop.name]);
757
+ }
758
+ return {
759
+ id,
760
+ type: step.type,
761
+ name: step.type,
762
+ properties: normalizedProps
763
+ };
764
+ });
765
+ const nodes = [...userNodes];
766
+ if (userNodes.length) {
767
+ const id = 'n' + (userNodes.length + 1);
768
+ nodes.push({
769
+ id,
770
+ type: OUTPUT_NODE_TYPE,
771
+ name: OUTPUT_NODE_TYPE,
772
+ properties: {}
773
+ });
774
+ }
775
+ const edges = [];
776
+ for (let i = 0; i < nodes.length - 1; i++) {
777
+ const aMd = state.metadata.find((m) => m.node_type === nodes[i].type);
778
+ const bMd = state.metadata.find((m) => m.node_type === nodes[i + 1].type);
779
+ const isOutputTarget = nodes[i + 1].type === OUTPUT_NODE_TYPE;
780
+ const link = isOutputTarget
781
+ ? {
782
+ sourceHandle: Array.isArray(aMd?.outputs) && aMd.outputs.length ? aMd.outputs[0].name : 'output',
783
+ targetHandle: 'value',
784
+ warning:
785
+ Array.isArray(aMd?.outputs) && aMd.outputs.length
786
+ ? null
787
+ : 'Auto output wiring used fallback source handle "output"'
788
+ }
789
+ : chooseLink(aMd, bMd);
790
+ if (link.warning) {
791
+ warnings.push(
792
+ 'Step ' + (i + 1) + ' -> ' + (i + 2) + ': ' + link.warning + ' (' + nodes[i].type + ' -> ' + nodes[i + 1].type + ')'
793
+ );
794
+ }
795
+ edges.push({
796
+ id: 'e' + (i + 1),
797
+ source: nodes[i].id,
798
+ target: nodes[i + 1].id,
799
+ sourceHandle: link.sourceHandle,
800
+ targetHandle: link.targetHandle,
801
+ edge_type: 'data'
802
+ });
803
+ }
804
+ return { graph: { nodes, edges }, warnings };
805
+ }
806
+
807
+ ReactDOM.createRoot(document.getElementById('root')).render(<App />);
808
+ </script>
809
+ </body>
810
+ </html>`;
811
+ }
812
+ class WsAdapter {
813
+ socket;
814
+ clientState = "connected";
815
+ applicationState = "connected";
816
+ queue = [];
817
+ waiters = [];
818
+ constructor(socket) {
819
+ this.socket = socket;
820
+ socket.on("message", (raw, isBinary) => {
821
+ const frame = isBinary
822
+ ? {
823
+ type: "websocket.message",
824
+ bytes: raw instanceof Uint8Array ? raw : new Uint8Array(raw)
825
+ }
826
+ : { type: "websocket.message", text: raw.toString() };
827
+ const waiter = this.waiters.shift();
828
+ if (waiter)
829
+ waiter(frame);
830
+ else
831
+ this.queue.push(frame);
832
+ });
833
+ socket.on("close", () => {
834
+ this.clientState = "disconnected";
835
+ this.applicationState = "disconnected";
836
+ const waiter = this.waiters.shift();
837
+ if (waiter)
838
+ waiter({ type: "websocket.disconnect" });
839
+ });
840
+ }
841
+ async accept() { }
842
+ async receive() {
843
+ const next = this.queue.shift();
844
+ if (next)
845
+ return next;
846
+ return await new Promise((resolve) => this.waiters.push(resolve));
847
+ }
848
+ async sendBytes(data) {
849
+ this.socket.send(data);
850
+ }
851
+ async sendText(data) {
852
+ this.socket.send(data);
853
+ }
854
+ async close(code, reason) {
855
+ this.clientState = "disconnected";
856
+ this.applicationState = "disconnected";
857
+ this.socket.close(code, reason);
858
+ }
859
+ }
860
+ function detectMetadataRootsFromPip() {
861
+ const script = `
862
+ import json
863
+ import pathlib
864
+ import subprocess
865
+ import sys
866
+
867
+ packages = ["nodetool-core", "nodetool-base"]
868
+ roots = set()
869
+
870
+ try:
871
+ proc = subprocess.run(
872
+ [sys.executable, "-m", "pip", "show", "-f", *packages],
873
+ capture_output=True,
874
+ text=True,
875
+ check=False,
876
+ )
877
+ output = proc.stdout or ""
878
+ except Exception:
879
+ output = ""
880
+
881
+ location = None
882
+ in_files = False
883
+ for raw in output.splitlines():
884
+ line = raw.rstrip("\\n")
885
+ if line.startswith("Name: "):
886
+ location = None
887
+ in_files = False
888
+ continue
889
+ if line.startswith("Location: "):
890
+ location = line.split(":", 1)[1].strip()
891
+ continue
892
+ if line.startswith("Editable project location: "):
893
+ editable = line.split(":", 1)[1].strip()
894
+ if editable:
895
+ roots.add(editable)
896
+ continue
897
+ if line.startswith("Files:"):
898
+ in_files = True
899
+ continue
900
+ if line.startswith("---"):
901
+ location = None
902
+ in_files = False
903
+ continue
904
+
905
+ if not in_files or not location or not line.startswith(" "):
906
+ continue
907
+
908
+ rel = line.strip().replace("\\\\", "/")
909
+ if "package_metadata" not in rel:
910
+ continue
911
+ abs_path = (pathlib.Path(location) / rel).resolve()
912
+ metadata_dir = abs_path if abs_path.is_dir() else abs_path.parent
913
+ roots.add(str(metadata_dir))
914
+
915
+ print(json.dumps(sorted(roots)))
916
+ `;
917
+ for (const python of ["python3", "python"]) {
918
+ const proc = spawnSync(python, ["-c", script], {
919
+ encoding: "utf8",
920
+ stdio: ["ignore", "pipe", "ignore"]
921
+ });
922
+ if (proc.status !== 0 || !proc.stdout)
923
+ continue;
924
+ try {
925
+ const roots = JSON.parse(proc.stdout.trim());
926
+ if (!Array.isArray(roots))
927
+ continue;
928
+ return roots.filter((p) => typeof p === "string" && p.length > 0 && existsSync(p));
929
+ }
930
+ catch {
931
+ // try next python executable
932
+ }
933
+ }
934
+ return [];
935
+ }
936
+ function resolveMetadataRoots(options) {
937
+ if (options.metadataRoots && options.metadataRoots.length > 0) {
938
+ return options.metadataRoots;
939
+ }
940
+ if (process.env.METADATA_ROOTS) {
941
+ return process.env.METADATA_ROOTS.split(":").filter(Boolean);
942
+ }
943
+ const detected = detectMetadataRootsFromPip();
944
+ if (detected.length > 0)
945
+ return detected;
946
+ const nearby = detectNearbyMetadataRoots(process.cwd());
947
+ if (nearby.length > 0)
948
+ return nearby;
949
+ return [process.cwd()];
950
+ }
951
+ function hasMetadataLayout(root) {
952
+ return (existsSync(path.join(root, "src", "nodetool", "package_metadata")) ||
953
+ existsSync(path.join(root, "nodetool", "package_metadata")));
954
+ }
955
+ function detectNearbyMetadataRoots(cwd) {
956
+ const candidates = new Set();
957
+ let cur = path.resolve(cwd);
958
+ for (let i = 0; i < 8; i++) {
959
+ candidates.add(cur);
960
+ const parent = path.dirname(cur);
961
+ if (parent === cur)
962
+ break;
963
+ cur = parent;
964
+ }
965
+ // Also inspect siblings in the parent workspace (e.g. nodetool-* repos).
966
+ const workspaceRoot = path.resolve(cwd, "..", "..", "..", "..");
967
+ if (existsSync(workspaceRoot)) {
968
+ try {
969
+ for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {
970
+ if (!entry.isDirectory())
971
+ continue;
972
+ const name = entry.name.toLowerCase();
973
+ if (!name.startsWith("nodetool"))
974
+ continue;
975
+ candidates.add(path.join(workspaceRoot, entry.name));
976
+ }
977
+ }
978
+ catch {
979
+ // ignore discovery failures
980
+ }
981
+ }
982
+ return [...candidates].filter(hasMetadataLayout);
983
+ }
984
+ function hasNodetoolBaseExamplesLayout(root) {
985
+ // Installed/editable package layout: src/nodetool/examples/nodetool-base
986
+ return (existsSync(path.join(root, "src", "nodetool", "examples", "nodetool-base")) ||
987
+ // Monorepo layout: nodetool/examples/nodetool-base (e.g. packages/base-nodes/)
988
+ existsSync(path.join(root, "nodetool", "examples", "nodetool-base")));
989
+ }
990
+ function detectNearbyNodetoolBaseRoot(cwd) {
991
+ const candidates = new Set();
992
+ let cur = path.resolve(cwd);
993
+ for (let i = 0; i < 8; i++) {
994
+ candidates.add(cur);
995
+ const parent = path.dirname(cur);
996
+ if (parent === cur)
997
+ break;
998
+ cur = parent;
999
+ }
1000
+ // Also check packages/base-nodes relative to the monorepo root
1001
+ for (const candidate of [...candidates]) {
1002
+ const baseNodesCandidate = path.join(candidate, "packages", "base-nodes");
1003
+ if (existsSync(baseNodesCandidate)) {
1004
+ candidates.add(baseNodesCandidate);
1005
+ }
1006
+ }
1007
+ const workspaceRoot = path.resolve(cwd, "..", "..", "..", "..");
1008
+ if (existsSync(workspaceRoot)) {
1009
+ try {
1010
+ for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {
1011
+ if (!entry.isDirectory())
1012
+ continue;
1013
+ if (entry.name.toLowerCase() !== "nodetool-base")
1014
+ continue;
1015
+ candidates.add(path.join(workspaceRoot, entry.name));
1016
+ }
1017
+ }
1018
+ catch {
1019
+ // ignore discovery failures
1020
+ }
1021
+ }
1022
+ for (const candidate of candidates) {
1023
+ if (hasNodetoolBaseExamplesLayout(candidate))
1024
+ return candidate;
1025
+ }
1026
+ return null;
1027
+ }
1028
+ function resolveExamplesDir(options) {
1029
+ if (options.examplesDir && existsSync(options.examplesDir))
1030
+ return options.examplesDir;
1031
+ if (process.env.NODETOOL_BASE_EXAMPLES_DIR &&
1032
+ existsSync(process.env.NODETOOL_BASE_EXAMPLES_DIR)) {
1033
+ return process.env.NODETOOL_BASE_EXAMPLES_DIR;
1034
+ }
1035
+ const baseRoot = detectNearbyNodetoolBaseRoot(process.cwd());
1036
+ if (!baseRoot)
1037
+ return null;
1038
+ // Try monorepo layout first (packages/base-nodes/nodetool/examples/nodetool-base)
1039
+ const monoDir = path.join(baseRoot, "nodetool", "examples", "nodetool-base");
1040
+ if (existsSync(monoDir))
1041
+ return monoDir;
1042
+ // Fall back to installed/editable layout (src/nodetool/examples/nodetool-base)
1043
+ const srcDir = path.join(baseRoot, "src", "nodetool", "examples", "nodetool-base");
1044
+ return existsSync(srcDir) ? srcDir : null;
1045
+ }
1046
+ function listExampleWorkflows(examplesDir) {
1047
+ const files = readdirSync(examplesDir, { withFileTypes: true })
1048
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".json"))
1049
+ .map((entry) => entry.name)
1050
+ .sort((a, b) => a.localeCompare(b));
1051
+ const items = [];
1052
+ for (const file of files) {
1053
+ try {
1054
+ const raw = readFileSync(path.join(examplesDir, file), "utf8");
1055
+ const parsed = JSON.parse(raw);
1056
+ items.push({
1057
+ id: file,
1058
+ name: typeof parsed.name === "string"
1059
+ ? parsed.name
1060
+ : file.replace(/\.json$/i, ""),
1061
+ description: typeof parsed.description === "string" ? parsed.description : "",
1062
+ tags: Array.isArray(parsed.tags)
1063
+ ? parsed.tags.filter((t) => typeof t === "string")
1064
+ : []
1065
+ });
1066
+ }
1067
+ catch {
1068
+ // skip invalid files
1069
+ }
1070
+ }
1071
+ return items;
1072
+ }
1073
+ function readExampleWorkflow(examplesDir, id) {
1074
+ const safeId = path.basename(id);
1075
+ if (!safeId.toLowerCase().endsWith(".json"))
1076
+ return null;
1077
+ const full = path.join(examplesDir, safeId);
1078
+ if (!existsSync(full))
1079
+ return null;
1080
+ const raw = readFileSync(full, "utf8");
1081
+ const parsed = JSON.parse(raw);
1082
+ const graph = parsed.graph;
1083
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges))
1084
+ return null;
1085
+ return {
1086
+ id: safeId,
1087
+ name: typeof parsed.name === "string"
1088
+ ? parsed.name
1089
+ : safeId.replace(/\.json$/i, ""),
1090
+ description: typeof parsed.description === "string" ? parsed.description : "",
1091
+ tags: Array.isArray(parsed.tags)
1092
+ ? parsed.tags.filter((t) => typeof t === "string")
1093
+ : [],
1094
+ graph
1095
+ };
1096
+ }
1097
+ export function createTestUiServer(options = {}) {
1098
+ const host = options.host ?? "127.0.0.1";
1099
+ const port = options.port ?? Number(process.env.PORT ?? 7777);
1100
+ const examplesDir = resolveExamplesDir(options);
1101
+ const metadataRoots = resolveMetadataRoots(options);
1102
+ const registry = new NodeRegistry();
1103
+ registry.loadPythonMetadata({
1104
+ roots: metadataRoots,
1105
+ maxDepth: options.metadataMaxDepth ?? 8
1106
+ });
1107
+ registerBaseNodes(registry);
1108
+ registerElevenLabsNodes(registry);
1109
+ registerTransformersJsNodes(registry);
1110
+ const resolvedApiOptions = {
1111
+ ...options,
1112
+ metadataRoots,
1113
+ registry,
1114
+ // Pass the resolved examples directory so handleWorkflowExamples can serve
1115
+ // examples directly from the filesystem without requiring Python metadata.
1116
+ ...(examplesDir ? { examplesDir } : {})
1117
+ };
1118
+ const graphNodeTypeResolver = createGraphNodeTypeResolver(registry);
1119
+ const server = createServer((req, res) => {
1120
+ const url = new URL(req.url ?? "/", `http://${host}:${port}`);
1121
+ if (url.pathname === "/" || url.pathname === "/test-ui") {
1122
+ res.statusCode = 200;
1123
+ res.setHeader("content-type", "text/html; charset=utf-8");
1124
+ res.end(htmlPage());
1125
+ return;
1126
+ }
1127
+ if (url.pathname === "/api/examples") {
1128
+ if (!examplesDir) {
1129
+ res.statusCode = 200;
1130
+ res.setHeader("content-type", "application/json");
1131
+ res.end(JSON.stringify({ examples: [] }));
1132
+ return;
1133
+ }
1134
+ const examples = listExampleWorkflows(examplesDir);
1135
+ res.statusCode = 200;
1136
+ res.setHeader("content-type", "application/json");
1137
+ res.end(JSON.stringify({ examples }));
1138
+ return;
1139
+ }
1140
+ if (url.pathname.startsWith("/api/examples/")) {
1141
+ if (!examplesDir) {
1142
+ res.statusCode = 404;
1143
+ res.setHeader("content-type", "application/json");
1144
+ res.end(JSON.stringify({ detail: "Examples directory not configured" }));
1145
+ return;
1146
+ }
1147
+ const id = decodeURIComponent(url.pathname.slice("/api/examples/".length));
1148
+ const example = readExampleWorkflow(examplesDir, id);
1149
+ if (!example) {
1150
+ res.statusCode = 404;
1151
+ res.setHeader("content-type", "application/json");
1152
+ res.end(JSON.stringify({ detail: "Example not found" }));
1153
+ return;
1154
+ }
1155
+ res.statusCode = 200;
1156
+ res.setHeader("content-type", "application/json");
1157
+ res.end(JSON.stringify(example));
1158
+ return;
1159
+ }
1160
+ if (url.pathname.startsWith("/api/") ||
1161
+ url.pathname.startsWith("/v1/") ||
1162
+ url.pathname.startsWith("/admin/")) {
1163
+ void handleNodeHttpRequest(req, res, resolvedApiOptions);
1164
+ return;
1165
+ }
1166
+ res.statusCode = 404;
1167
+ res.setHeader("content-type", "application/json");
1168
+ res.end(JSON.stringify({ detail: "Not found" }));
1169
+ });
1170
+ const wss = new WebSocketServer({ noServer: true });
1171
+ wss.on("error", (error) => {
1172
+ log.error("WebSocketServer error", error);
1173
+ });
1174
+ server.on("upgrade", (request, socket, head) => {
1175
+ const url = new URL(request.url ?? "/", `http://${host}:${port}`);
1176
+ if (url.pathname !== "/ws") {
1177
+ socket.destroy();
1178
+ return;
1179
+ }
1180
+ wss.handleUpgrade(request, socket, head, (ws) => {
1181
+ ws.on("error", (error) => {
1182
+ log.error("WebSocket client error", error);
1183
+ });
1184
+ const runner = new UnifiedWebSocketRunner({
1185
+ resolveExecutor: (node) => registry.resolve(node),
1186
+ resolveNodeType: graphNodeTypeResolver,
1187
+ resolveProvider: async (_providerId) => new ScriptedProvider([
1188
+ autoScript({
1189
+ plan: {
1190
+ title: "Agent task",
1191
+ steps: [
1192
+ {
1193
+ id: "s1",
1194
+ instructions: "Complete the objective",
1195
+ depends_on: []
1196
+ }
1197
+ ]
1198
+ },
1199
+ text: "fake agent response from server"
1200
+ })
1201
+ ]),
1202
+ getNodeMetadata: (nodeType) => registry.getMetadata(nodeType)
1203
+ });
1204
+ log.info("WebSocket client connected");
1205
+ void runner.run(new WsAdapter(ws)).catch((error) => {
1206
+ log.error("Runner crashed", error instanceof Error ? error : new Error(String(error)));
1207
+ });
1208
+ });
1209
+ });
1210
+ return {
1211
+ server,
1212
+ listen: () => new Promise((resolve) => {
1213
+ server.listen(port, host, () => resolve());
1214
+ }),
1215
+ close: () => new Promise((resolve, reject) => {
1216
+ wss.close((err) => {
1217
+ if (err)
1218
+ reject(err);
1219
+ else {
1220
+ server.close((closeErr) => {
1221
+ if (closeErr)
1222
+ reject(closeErr);
1223
+ else
1224
+ resolve();
1225
+ });
1226
+ }
1227
+ });
1228
+ }),
1229
+ info: {
1230
+ host,
1231
+ port,
1232
+ metadataRoots,
1233
+ metadataCount: registry.listMetadata().length
1234
+ }
1235
+ };
1236
+ }
1237
+ if (import.meta.url === `file://${process.argv[1]}`) {
1238
+ // Initialize SQLite adapter pointing at the same DB as the Python side
1239
+ const dbPath = getDefaultDbPath();
1240
+ try {
1241
+ initDb(dbPath);
1242
+ }
1243
+ catch {
1244
+ // DB unavailable — secrets will appear unconfigured
1245
+ }
1246
+ const srv = createTestUiServer();
1247
+ void srv.listen().then(() => {
1248
+ log.info("Server listening", {
1249
+ host: srv.info.host,
1250
+ port: srv.info.port,
1251
+ metadataRoots: srv.info.metadataRoots,
1252
+ nodes: srv.info.metadataCount
1253
+ });
1254
+ });
1255
+ }
1256
+ //# sourceMappingURL=test-ui-server.js.map