elasticdash-test 0.1.9 → 0.1.11-alpha-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/cli.js +48 -2
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-server.d.ts +33 -0
- package/dist/dashboard-server.d.ts.map +1 -0
- package/dist/dashboard-server.js +814 -0
- package/dist/dashboard-server.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy/llm-capture.d.ts +14 -0
- package/dist/proxy/llm-capture.d.ts.map +1 -0
- package/dist/proxy/llm-capture.js +264 -0
- package/dist/proxy/llm-capture.js.map +1 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +37 -3
- package/dist/runner.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Scan for ed_tools.ts or ed_tools.js and extract exported functions
|
|
8
|
+
*/
|
|
9
|
+
async function scanTools(cwd) {
|
|
10
|
+
const tools = [];
|
|
11
|
+
const candidates = [
|
|
12
|
+
path.join(cwd, 'ed_tools.ts'),
|
|
13
|
+
path.join(cwd, 'ed_tools.js'),
|
|
14
|
+
];
|
|
15
|
+
for (const toolsPath of candidates) {
|
|
16
|
+
if (!existsSync(toolsPath)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const moduleUrl = pathToFileURL(toolsPath).href;
|
|
21
|
+
const module = await import(moduleUrl);
|
|
22
|
+
for (const key of Object.keys(module)) {
|
|
23
|
+
if (key === 'default')
|
|
24
|
+
continue;
|
|
25
|
+
const value = module[key];
|
|
26
|
+
if (typeof value === 'function') {
|
|
27
|
+
const funcStr = value.toString();
|
|
28
|
+
let signature = '()';
|
|
29
|
+
const match = funcStr.match(/^(?:async\s+)?function\s*\w*\s*(\([^)]*\))|^(?:async\s+)?(\([^)]*\))\s*=>|^(?:async\s+)?\w+\s*(\([^)]*\))\s*{/);
|
|
30
|
+
if (match) {
|
|
31
|
+
signature = match[1] || match[2] || match[3] || '()';
|
|
32
|
+
}
|
|
33
|
+
tools.push({
|
|
34
|
+
name: key,
|
|
35
|
+
isAsync: funcStr.trimStart().startsWith('async'),
|
|
36
|
+
signature,
|
|
37
|
+
filePath: toolsPath,
|
|
38
|
+
sourceCode: funcStr.length < 2000 ? funcStr : funcStr.substring(0, 2000) + '...\n// (truncated)',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.warn(`Warning: Failed to scan ${toolsPath}:`, error);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tools;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Scan for ed_workflow.ts or ed_workflow.js and extract exported functions
|
|
53
|
+
*/
|
|
54
|
+
async function scanWorkflows(cwd) {
|
|
55
|
+
const workflows = [];
|
|
56
|
+
// Check for ed_workflow.ts first, then .js
|
|
57
|
+
const candidates = [
|
|
58
|
+
path.join(cwd, 'ed_workflow.ts'),
|
|
59
|
+
path.join(cwd, 'ed_workflow.js'),
|
|
60
|
+
];
|
|
61
|
+
for (const workflowPath of candidates) {
|
|
62
|
+
if (!existsSync(workflowPath)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const moduleUrl = pathToFileURL(workflowPath).href;
|
|
67
|
+
const module = await import(moduleUrl);
|
|
68
|
+
// Extract all exported functions
|
|
69
|
+
for (const key of Object.keys(module)) {
|
|
70
|
+
if (key === 'default')
|
|
71
|
+
continue;
|
|
72
|
+
const value = module[key];
|
|
73
|
+
if (typeof value === 'function') {
|
|
74
|
+
// Try to get function signature
|
|
75
|
+
const funcStr = value.toString();
|
|
76
|
+
let signature = '()';
|
|
77
|
+
// Extract parameters from function string
|
|
78
|
+
const match = funcStr.match(/^(?:async\s+)?function\s*\w*\s*(\([^)]*\))|^(?:async\s+)?(\([^)]*\))\s*=>|^(?:async\s+)?\w+\s*(\([^)]*\))\s*{/);
|
|
79
|
+
if (match) {
|
|
80
|
+
signature = match[1] || match[2] || match[3] || '()';
|
|
81
|
+
}
|
|
82
|
+
// Get source information
|
|
83
|
+
let sourceFile;
|
|
84
|
+
let sourceModule;
|
|
85
|
+
try {
|
|
86
|
+
// Try to extract module information from function name/toString
|
|
87
|
+
if (value.name && value.name !== key) {
|
|
88
|
+
sourceModule = value.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Ignore errors
|
|
93
|
+
}
|
|
94
|
+
workflows.push({
|
|
95
|
+
name: key,
|
|
96
|
+
isAsync: funcStr.trimStart().startsWith('async'),
|
|
97
|
+
signature,
|
|
98
|
+
filePath: workflowPath,
|
|
99
|
+
sourceFile,
|
|
100
|
+
sourceModule,
|
|
101
|
+
sourceCode: funcStr.length < 2000 ? funcStr : funcStr.substring(0, 2000) + '...\n// (truncated)',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Successfully scanned this file, no need to try others
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.warn(`Warning: Failed to scan ${workflowPath}:`, error);
|
|
110
|
+
// Continue to try the next candidate
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return workflows;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Open URL in default browser (platform-aware)
|
|
118
|
+
*/
|
|
119
|
+
function openBrowser(url) {
|
|
120
|
+
const platform = process.platform;
|
|
121
|
+
if (platform === 'darwin') {
|
|
122
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' });
|
|
123
|
+
}
|
|
124
|
+
else if (platform === 'linux') {
|
|
125
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
|
|
126
|
+
}
|
|
127
|
+
else if (platform === 'win32') {
|
|
128
|
+
spawn('cmd', ['/c', 'start', url], { detached: true, stdio: 'ignore', shell: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get the dashboard HTML page
|
|
133
|
+
*/
|
|
134
|
+
function getDashboardHtml() {
|
|
135
|
+
// Use function to avoid template literal issues in TypeScript
|
|
136
|
+
return `<!DOCTYPE html>
|
|
137
|
+
<html lang="en">
|
|
138
|
+
<head>
|
|
139
|
+
<meta charset="UTF-8">
|
|
140
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
141
|
+
<title>ElasticDash Dashboard</title>
|
|
142
|
+
<style>
|
|
143
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
144
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
|
145
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
146
|
+
header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
147
|
+
h1 { font-size: 28px; margin-bottom: 8px; color: #1a1a1a; }
|
|
148
|
+
.subtitle { font-size: 14px; color: #666; margin-bottom: 16px; }
|
|
149
|
+
.search-box { display: flex; gap: 10px; }
|
|
150
|
+
input[type="text"] { flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
|
151
|
+
input[type="text"]:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); }
|
|
152
|
+
.result-count { padding: 10px 12px; background: #f0f0f0; border-radius: 6px; font-size: 14px; color: #666; }
|
|
153
|
+
.workflows-list { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; max-height: 65vh; display: flex; flex-direction: column; }
|
|
154
|
+
.workflows-table { width: 100%; border-collapse: collapse; }
|
|
155
|
+
.workflows-table thead { background: #f5f5f5; position: sticky; top: 0; z-index: 10; }
|
|
156
|
+
.workflows-table th { padding: 12px 16px; text-align: left; font-weight: 600; font-size: 13px; color: #333; border-bottom: 2px solid #ddd; }
|
|
157
|
+
.workflows-table td { padding: 12px 16px; border-bottom: 1px solid #eee; }
|
|
158
|
+
.workflows-table tbody tr { cursor: pointer; transition: background-color 0.2s; }
|
|
159
|
+
.workflows-table tbody tr:hover { background-color: #f9f9f9; }
|
|
160
|
+
.workflow-name-cell { font-family: Monaco, monospace; font-weight: 600; color: #0066cc; }
|
|
161
|
+
.workflow-path-cell { font-family: Monaco, monospace; font-size: 12px; color: #666; }
|
|
162
|
+
.async-badge { display: inline-block; background: #e8f3ff; color: #0066cc; padding: 2px 8px; border-radius: 4px; font-size: 11px; margin-left: 8px; }
|
|
163
|
+
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; align-items: center; justify-content: center; }
|
|
164
|
+
.modal.open { display: flex; }
|
|
165
|
+
.modal-content { background: white; border-radius: 12px; width: 92%; max-width: 1100px; max-height: 90vh; overflow-y: auto; padding: 30px; }
|
|
166
|
+
.modal-header { display: flex; justify-content: space-between; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
|
|
167
|
+
.modal-title { font-size: 20px; font-weight: 600; }
|
|
168
|
+
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #999; }
|
|
169
|
+
.upload-area { border: 2px dashed #ddd; border-radius: 8px; padding: 30px; text-align: center; cursor: pointer; background: #fafafa; }
|
|
170
|
+
.upload-area:hover { border-color: #0066cc; background: #f0f7ff; }
|
|
171
|
+
.upload-icon { font-size: 32px; margin-bottom: 12px; }
|
|
172
|
+
input[type="file"] { display: none; }
|
|
173
|
+
.upload-status { margin-top: 20px; padding: 12px; border-radius: 6px; display: none; }
|
|
174
|
+
.upload-status.success { display: block; background: #e8f5e9; color: #2e7d32; }
|
|
175
|
+
.upload-status.error { display: block; background: #ffebee; color: #c62828; }
|
|
176
|
+
.hidden { display: none !important; }
|
|
177
|
+
.trace-viewer { display: none; margin-top: 20px; }
|
|
178
|
+
.trace-viewer.visible { display: block; }
|
|
179
|
+
.trace-layout { display: grid; grid-template-columns: 40% calc(60% - 16px); gap: 16px; min-height: 420px; }
|
|
180
|
+
.trace-layout.step-5 { display: grid; grid-template-columns: calc(30% - 16px) calc(30% - 16px) 40%; gap: 16px; min-height: 420px; }
|
|
181
|
+
.trace-left, .trace-right { background: #f9f9f9; border-radius: 8px; padding: 14px; border: 1px solid #eee; }
|
|
182
|
+
.trace-section-title { font-size: 14px; font-weight: 600; margin-bottom: 10px; }
|
|
183
|
+
.observation-table-wrap { max-height: 460px; overflow: auto; background: white; border-radius: 6px; border: 1px solid #eee; }
|
|
184
|
+
.observation-table { width: 100%; border-collapse: collapse; }
|
|
185
|
+
.observation-table thead { background: #f5f5f5; position: sticky; top: 0; z-index: 1; }
|
|
186
|
+
.observation-table th { text-align: left; font-size: 12px; font-weight: 600; color: #555; padding: 10px 12px; border-bottom: 1px solid #e8e8e8; }
|
|
187
|
+
.observation-table td { font-size: 13px; padding: 10px 12px; border-bottom: 1px solid #f0f0f0; }
|
|
188
|
+
.observation-table tbody tr { cursor: pointer; }
|
|
189
|
+
.observation-table tbody tr:hover { background: #f7fbff; }
|
|
190
|
+
.observation-table tbody tr.selected { background: #e8f3ff; }
|
|
191
|
+
.obs-type { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; background: #e6e6e6; color: #333; }
|
|
192
|
+
.obs-type.tool { background: #e8f7ef; color: #1f7a44; }
|
|
193
|
+
.obs-type.ai { background: #e8f1ff; color: #1f5fbf; }
|
|
194
|
+
.detail-sections { display: flex; flex-direction: column; gap: 12px; height: 486.5px; overflow-y: auto; }
|
|
195
|
+
.detail-section { background: white; border: 1px solid #eee; border-radius: 6px; padding: 10px; }
|
|
196
|
+
.detail-title { font-size: 12px; font-weight: 600; margin-bottom: 8px; color: #555; text-transform: uppercase; letter-spacing: 0.02em; }
|
|
197
|
+
.detail-pre { margin: 0; font-family: Monaco, monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; background: #fafafa; border-radius: 4px; padding: 10px; border: 1px solid #f0f0f0; min-height: 56px; max-height: 340px; overflow-y: auto; }
|
|
198
|
+
.modal-footer { display: none; margin-top: 24px; padding-top: 20px; border-top: 1px solid #eee; gap: 12px; justify-content: space-between; }
|
|
199
|
+
.modal-footer.visible { display: flex; }
|
|
200
|
+
.btn { padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: none; }
|
|
201
|
+
.btn-secondary { background: #f0f0f0; color: #333; }
|
|
202
|
+
.btn-secondary:hover { background: #e0e0e0; }
|
|
203
|
+
.btn-primary { background: #0066cc; color: white; }
|
|
204
|
+
.btn-primary:hover { background: #0052a3; }
|
|
205
|
+
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
206
|
+
.btn-primary:disabled:hover { background: #0066cc; }
|
|
207
|
+
.btn-secondary:disabled:hover { background: #f0f0f0; }
|
|
208
|
+
.obs-checkbox { width: 18px; height: 18px; cursor: pointer; }
|
|
209
|
+
@media (max-width: 900px) {
|
|
210
|
+
.trace-layout { grid-template-columns: 1fr; }
|
|
211
|
+
.observation-table-wrap { max-height: 260px; }
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
214
|
+
</head>
|
|
215
|
+
<body>
|
|
216
|
+
<div class="container">
|
|
217
|
+
<header>
|
|
218
|
+
<h1>Workflow Functions</h1>
|
|
219
|
+
<div class="subtitle">Select a workflow to debug with trace analysis</div>
|
|
220
|
+
<div class="search-box">
|
|
221
|
+
<input type="text" id="searchInput" placeholder="Search by name or path..." autocomplete="off">
|
|
222
|
+
<div class="result-count"><span id="resultCount">0</span> workflows</div>
|
|
223
|
+
</div>
|
|
224
|
+
</header>
|
|
225
|
+
<div class="workflows-list">
|
|
226
|
+
<table class="workflows-table">
|
|
227
|
+
<thead><tr><th style="width: 35%">Function Name</th><th>File Path</th></tr></thead>
|
|
228
|
+
<tbody id="workflowsTableBody"><tr><td colspan="2" style="text-align: center; padding: 40px;">Loading...</td></tr></tbody>
|
|
229
|
+
</table>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div id="traceModal" class="modal">
|
|
233
|
+
<div class="modal-content">
|
|
234
|
+
<div class="modal-header">
|
|
235
|
+
<h2 class="modal-title">Import Trace for Analysis</h2>
|
|
236
|
+
<button class="modal-close" id="closeModal">×</button>
|
|
237
|
+
</div>
|
|
238
|
+
<div id="uploadArea" class="upload-area">
|
|
239
|
+
<div class="upload-icon">📤</div>
|
|
240
|
+
<div>Drop trace file here or click to select</div>
|
|
241
|
+
<input type="file" id="traceFile" accept=".json" />
|
|
242
|
+
</div>
|
|
243
|
+
<div id="uploadStatus" class="upload-status"></div>
|
|
244
|
+
<div id="traceViewer" class="trace-viewer">
|
|
245
|
+
<div class="trace-layout">
|
|
246
|
+
<div class="trace-left">
|
|
247
|
+
<div class="trace-section-title">Observations</div>
|
|
248
|
+
<div class="observation-table-wrap">
|
|
249
|
+
<table class="observation-table">
|
|
250
|
+
<thead id="observationTableHead"><tr><th style="width: 65%;">Name</th><th>Type</th><th>Action</th></tr></thead>
|
|
251
|
+
<tbody id="observationTableBody"></tbody>
|
|
252
|
+
</table>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="trace-right">
|
|
256
|
+
<div id="observationDetail"></div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div id="modalFooter" class="modal-footer">
|
|
261
|
+
<button class="btn btn-secondary" id="changeTraceBtn">Change Trace File</button>
|
|
262
|
+
<button class="btn btn-primary" id="nextBtn">Next</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<script>
|
|
267
|
+
console.log("[Dashboard] Script starting...");
|
|
268
|
+
let allWorkflows = [], codeIndex = {workflows: [], tools: []}, selectedWorkflow = null;
|
|
269
|
+
let currentObservations = [], selectedObservationIndex = -1;
|
|
270
|
+
const tbody = document.getElementById("workflowsTableBody");
|
|
271
|
+
const countEl = document.getElementById("resultCount");
|
|
272
|
+
const modal = document.getElementById("traceModal");
|
|
273
|
+
const uploadArea = document.getElementById("uploadArea");
|
|
274
|
+
const fileInput = document.getElementById("traceFile");
|
|
275
|
+
const modalFooter = document.getElementById("modalFooter");
|
|
276
|
+
const uploadStatus = document.getElementById("uploadStatus");
|
|
277
|
+
const traceViewer = document.getElementById("traceViewer");
|
|
278
|
+
let observationTableBody = document.getElementById("observationTableBody");
|
|
279
|
+
let observationDetail = document.getElementById("observationDetail");
|
|
280
|
+
const modalTitle = document.querySelector(".modal-title");
|
|
281
|
+
console.log("[Dashboard] DOM elements loaded, tbody:", tbody);
|
|
282
|
+
|
|
283
|
+
let currentStep = 0; // 0=upload, 3=mark, 4=verify, 5=validate
|
|
284
|
+
let checkedObservations = new Set();
|
|
285
|
+
|
|
286
|
+
document.getElementById("closeModal").onclick = () => {
|
|
287
|
+
modal.classList.remove("open");
|
|
288
|
+
resetTraceModal();
|
|
289
|
+
};
|
|
290
|
+
modal.onclick = (e) => {
|
|
291
|
+
if (e.target === modal) {
|
|
292
|
+
modal.classList.remove("open");
|
|
293
|
+
resetTraceModal();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
document.getElementById("changeTraceBtn").onclick = () => {
|
|
298
|
+
if (currentStep === 3) {
|
|
299
|
+
resetTraceModal();
|
|
300
|
+
} else if (currentStep === 4) {
|
|
301
|
+
// Go back to Step 3
|
|
302
|
+
currentStep = 3;
|
|
303
|
+
updateModalTitle();
|
|
304
|
+
updateFooterButtons();
|
|
305
|
+
renderObservationTable();
|
|
306
|
+
} else if (currentStep === 5) {
|
|
307
|
+
currentStep = 3;
|
|
308
|
+
updateModalTitle();
|
|
309
|
+
updateFooterButtons();
|
|
310
|
+
renderObservationTable();
|
|
311
|
+
customFooter.remove();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
document.getElementById("nextBtn").onclick = () => {
|
|
316
|
+
if (currentStep === 3) {
|
|
317
|
+
// Validate that at least one checkbox is checked
|
|
318
|
+
if (checkedObservations.size === 0) {
|
|
319
|
+
alert("Please select at least one step to mark as broken");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Move to Step 4
|
|
323
|
+
currentStep = 4;
|
|
324
|
+
updateModalTitle();
|
|
325
|
+
updateFooterButtons();
|
|
326
|
+
renderObservationTable();
|
|
327
|
+
} else if (currentStep === 4) {
|
|
328
|
+
// Move to Step 5
|
|
329
|
+
// Show validation dialog before moving to Step 5
|
|
330
|
+
if (!window.liveValidationDialog) {
|
|
331
|
+
window.liveValidationDialog = document.createElement('div');
|
|
332
|
+
window.liveValidationDialog.id = 'liveValidationDialog';
|
|
333
|
+
window.liveValidationDialog.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
334
|
+
window.liveValidationDialog.innerHTML = \`
|
|
335
|
+
<div style="background:white;padding:32px 28px;border-radius:12px;box-shadow:0 2px 24px #0002;min-width:340px;max-width:90vw;">
|
|
336
|
+
<h3 style="margin-top:0;margin-bottom:18px;font-size:20px;">Validate Updated Flow with Live Data</h3>
|
|
337
|
+
<label style="font-size:15px;display:block;margin-bottom:8px;">How many times do you want to run the flow with live data?</label>
|
|
338
|
+
<input id="liveValidationCount" type="number" min="1" value="1" style="width:80px;font-size:16px;padding:6px 10px;margin-bottom:18px;" />
|
|
339
|
+
<div style="display:flex;gap:12px;justify-content:flex-end;">
|
|
340
|
+
<button id="cancelLiveValidation" class="btn btn-secondary">Cancel</button>
|
|
341
|
+
<button id="submitLiveValidation" class="btn btn-primary">Validate</button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
\`;
|
|
345
|
+
document.body.appendChild(window.liveValidationDialog);
|
|
346
|
+
document.getElementById('cancelLiveValidation').onclick = function() {
|
|
347
|
+
window.liveValidationDialog.remove();
|
|
348
|
+
window.liveValidationDialog = null;
|
|
349
|
+
};
|
|
350
|
+
document.getElementById('submitLiveValidation').onclick = function() {
|
|
351
|
+
const count = parseInt(document.getElementById('liveValidationCount').value, 10);
|
|
352
|
+
if (count >= 1) {
|
|
353
|
+
window.liveValidationDialog.remove();
|
|
354
|
+
window.liveValidationDialog = null;
|
|
355
|
+
window.liveValidationCount = count;
|
|
356
|
+
window.step5SelectedTrace = 0;
|
|
357
|
+
window.step5SelectedObservation = 0;
|
|
358
|
+
currentStep = 5;
|
|
359
|
+
updateModalTitle();
|
|
360
|
+
updateFooterButtons();
|
|
361
|
+
renderObservationTable();
|
|
362
|
+
console.log("[Dashboard] Moving to Step 5: Validate updated flow");
|
|
363
|
+
} else {
|
|
364
|
+
document.getElementById('liveValidationCount').style.borderColor = 'red';
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
} else if (currentStep === 5) {
|
|
370
|
+
modal.classList.remove("open");
|
|
371
|
+
resetTraceModal();
|
|
372
|
+
customFooter.remove();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
uploadArea.onclick = () => fileInput.click();
|
|
377
|
+
|
|
378
|
+
fileInput.onchange = (e) => {
|
|
379
|
+
if (!e.target.files[0]) return;
|
|
380
|
+
const file = e.target.files[0];
|
|
381
|
+
// Always clear file input so same file can be uploaded again
|
|
382
|
+
fileInput.value = "";
|
|
383
|
+
// Clear observations before loading new trace
|
|
384
|
+
resetTraceModal();
|
|
385
|
+
const reader = new FileReader();
|
|
386
|
+
reader.onload = (e) => {
|
|
387
|
+
try {
|
|
388
|
+
const data = JSON.parse(e.target.result);
|
|
389
|
+
uploadStatus.className = "upload-status";
|
|
390
|
+
uploadStatus.textContent = "";
|
|
391
|
+
displayTrace(data);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
uploadArea.classList.remove("hidden");
|
|
394
|
+
traceViewer.classList.remove("visible");
|
|
395
|
+
uploadStatus.className = "upload-status error";
|
|
396
|
+
uploadStatus.textContent = "Invalid JSON";
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
reader.readAsText(file);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
function displayTrace(data) {
|
|
403
|
+
let obs = [];
|
|
404
|
+
if (Array.isArray(data)) {
|
|
405
|
+
obs = data.filter(o =>
|
|
406
|
+
(o.type === "GENERATION" || o.type === "TOOL" || o.type === "SPAN") &&
|
|
407
|
+
(o.input !== null && o.input !== undefined) &&
|
|
408
|
+
(o.output !== null && o.output !== undefined)
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
const trace = data.trace || data;
|
|
412
|
+
obs = (data.observations || trace.observations || []).filter(o =>
|
|
413
|
+
(o.type === "GENERATION" || o.type === "TOOL" || o.type === "SPAN") &&
|
|
414
|
+
(o.input !== null && o.input !== undefined) &&
|
|
415
|
+
(o.output !== null && o.output !== undefined)
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
// Sort by timestamp ascending
|
|
419
|
+
obs = obs.sort((a, b) => {
|
|
420
|
+
const timeA = new Date(a.startTime || a.createdAt || 0).getTime();
|
|
421
|
+
const timeB = new Date(b.startTime || b.createdAt || 0).getTime();
|
|
422
|
+
return timeA - timeB;
|
|
423
|
+
});
|
|
424
|
+
currentObservations = obs;
|
|
425
|
+
selectedObservationIndex = -1;
|
|
426
|
+
checkedObservations.clear();
|
|
427
|
+
observationDetail.innerHTML = "";
|
|
428
|
+
uploadArea.classList.add("hidden");
|
|
429
|
+
traceViewer.classList.add("visible");
|
|
430
|
+
modalFooter.classList.add("visible");
|
|
431
|
+
currentStep = 3;
|
|
432
|
+
updateModalTitle();
|
|
433
|
+
updateFooterButtons();
|
|
434
|
+
renderObservationTable();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function renderObservationTable() {
|
|
438
|
+
if (currentStep === 5) {
|
|
439
|
+
// Step 5: Validate updated flow with live data
|
|
440
|
+
// Render traceTable before observationsTable
|
|
441
|
+
const n = window.liveValidationCount || 1;
|
|
442
|
+
const traceCount = n + 1;
|
|
443
|
+
document.getElementsByClassName("trace-layout")[0].classList.add("step-5");
|
|
444
|
+
let traceTable = \`<div class="trace-section-title">Traces</div>
|
|
445
|
+
<div class="observation-table-wrap">
|
|
446
|
+
<table class="observation-table">
|
|
447
|
+
<thead><tr><th>Trace #</th></tr></thead>
|
|
448
|
+
<tbody>\`;
|
|
449
|
+
for (let i = 0; i < traceCount; i++) {
|
|
450
|
+
const isSelected = i === window.step5SelectedTrace;
|
|
451
|
+
const label = i === 0 ? "Original Trace" : \`Trace \${i}\`;
|
|
452
|
+
traceTable += \`<tr class="\${isSelected ? "selected" : ""}" onclick="window.step5SelectedTrace=\${i};window.step5SelectedObservation=0;renderObservationTable();"><td>\${label}</td></tr>\`;
|
|
453
|
+
}
|
|
454
|
+
traceTable += \`</tbody></table></div>\`;
|
|
455
|
+
|
|
456
|
+
// Observations table for selected trace
|
|
457
|
+
let observationsTable = "";
|
|
458
|
+
let detailsSection = "";
|
|
459
|
+
const traces = Array.from(checkedObservations).map(idx => currentObservations[idx]);
|
|
460
|
+
|
|
461
|
+
if (window.step5SelectedTrace === 0) {
|
|
462
|
+
// Original Trace: show all currentObservations (same as step 3)
|
|
463
|
+
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
464
|
+
<div class="observation-table-wrap">
|
|
465
|
+
<table class="observation-table">
|
|
466
|
+
<thead><tr><th>Name</th><th>Type</th></tr></thead>
|
|
467
|
+
<tbody>\`;
|
|
468
|
+
observationsTable += currentObservations.map((obs, j) => {
|
|
469
|
+
const isSelected = j === window.step5SelectedObservation;
|
|
470
|
+
const name = obs.name || obs.id || ("Observation " + (j + 1));
|
|
471
|
+
const type = obs.type || "UNKNOWN";
|
|
472
|
+
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
473
|
+
return \`<tr class="\${isSelected ? "selected" : ""}" onclick="window.step5SelectedObservation=\${j};renderObservationTable();"><td>\${esc(name)}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td></tr>\`;
|
|
474
|
+
}).join("");
|
|
475
|
+
observationsTable += \`</tbody></table></div>\`;
|
|
476
|
+
// Details for selected observation
|
|
477
|
+
const selObs = currentObservations[window.step5SelectedObservation];
|
|
478
|
+
if (selObs) {
|
|
479
|
+
const inputText = toDisplayText(selObs.input, selObs.type);
|
|
480
|
+
const outputText = toDisplayText(selObs.output, selObs.type);
|
|
481
|
+
detailsSection = \`<div class="detail-sections">
|
|
482
|
+
<div class="detail-section">
|
|
483
|
+
<div class="detail-title">File Path</div>
|
|
484
|
+
<pre class="detail-pre">/mock/path/relevant-function.ts</pre>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="detail-section">
|
|
487
|
+
<div class="detail-title">Input</div>
|
|
488
|
+
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="detail-section">
|
|
491
|
+
<div class="detail-title">Output</div>
|
|
492
|
+
<pre class="detail-pre">\${esc(outputText)}</pre>
|
|
493
|
+
</div>
|
|
494
|
+
</div>\`;
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
// Live traces: index 1 → traces[0], index 2 → traces[1], …
|
|
498
|
+
const liveTrace = traces[window.step5SelectedTrace - 1];
|
|
499
|
+
if (liveTrace) {
|
|
500
|
+
const actions = liveTrace.actions || [];
|
|
501
|
+
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
502
|
+
<div class="observation-table-wrap">
|
|
503
|
+
<table class="observation-table">
|
|
504
|
+
<thead><tr><th>Name</th><th>Type</th></tr></thead>
|
|
505
|
+
<tbody>\`;
|
|
506
|
+
observationsTable += actions.map((action, j) => {
|
|
507
|
+
const isSelected = j === window.step5SelectedObservation;
|
|
508
|
+
const name = action.name || action.id || ("Observation " + (j + 1));
|
|
509
|
+
const type = action.type || "UNKNOWN";
|
|
510
|
+
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
511
|
+
return \`<tr class="\${isSelected ? "selected" : ""}" onclick="window.step5SelectedObservation=\${j};renderObservationTable();"><td>\${esc(name)}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td></tr>\`;
|
|
512
|
+
}).join("");
|
|
513
|
+
observationsTable += \`</tbody></table></div>\`;
|
|
514
|
+
// Details for selected observation
|
|
515
|
+
if (actions[window.step5SelectedObservation]) {
|
|
516
|
+
const obs = actions[window.step5SelectedObservation];
|
|
517
|
+
const inputText = toDisplayText(obs.input, obs.type);
|
|
518
|
+
const outputText = toDisplayText(obs.output, obs.type);
|
|
519
|
+
const mockFilePath = liveTrace.filePath || "/mock/path/relevant-function.ts";
|
|
520
|
+
detailsSection = \`<div class="detail-sections">
|
|
521
|
+
<div class="detail-section">
|
|
522
|
+
<div class="detail-title">File Path</div>
|
|
523
|
+
<pre class="detail-pre">\${esc(mockFilePath)}</pre>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="detail-section">
|
|
526
|
+
<div class="detail-title">Input</div>
|
|
527
|
+
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="detail-section">
|
|
530
|
+
<div class="detail-title">Output</div>
|
|
531
|
+
<pre class="detail-pre">\${esc(outputText)}</pre>
|
|
532
|
+
</div>
|
|
533
|
+
</div>\`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Render 3 sibling columns inside the CSS grid
|
|
539
|
+
const traceLayout = document.getElementsByClassName("trace-layout")[0];
|
|
540
|
+
traceLayout.innerHTML = \`
|
|
541
|
+
<div class="trace-left">\${traceTable}</div>
|
|
542
|
+
<div class="trace-left">\${observationsTable || '<div class="trace-section-title">Observations</div>'}</div>
|
|
543
|
+
<div class="trace-right">\${detailsSection}</div>
|
|
544
|
+
\`;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const traceLayoutEl = document.getElementsByClassName("trace-layout")[0];
|
|
549
|
+
if (traceLayoutEl.classList.contains("step-5")) {
|
|
550
|
+
traceLayoutEl.classList.remove("step-5");
|
|
551
|
+
traceLayoutEl.innerHTML = \`
|
|
552
|
+
<div class="trace-left">
|
|
553
|
+
<div class="trace-section-title">Observations</div>
|
|
554
|
+
<div class="observation-table-wrap">
|
|
555
|
+
<table class="observation-table">
|
|
556
|
+
<thead id="observationTableHead"><tr><th style="width: 65%;">Name</th><th>Type</th><th>Action</th></tr></thead>
|
|
557
|
+
<tbody id="observationTableBody"></tbody>
|
|
558
|
+
</table>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="trace-right">
|
|
562
|
+
<div id="observationDetail"></div>
|
|
563
|
+
</div>
|
|
564
|
+
\`;
|
|
565
|
+
observationTableBody = document.getElementById("observationTableBody");
|
|
566
|
+
observationDetail = document.getElementById("observationDetail");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const obsToRender = currentStep === 4 ? Array.from(checkedObservations).map(idx => currentObservations[idx]) : currentObservations;
|
|
570
|
+
const indices = currentStep === 4 ? Array.from(checkedObservations) : currentObservations.map((_, i) => i);
|
|
571
|
+
|
|
572
|
+
if (!obsToRender.length) {
|
|
573
|
+
observationTableBody.innerHTML = '<tr><td colspan="3" style="padding: 16px; color: #777;">No observations found.</td></tr>';
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
observationTableBody.innerHTML = obsToRender.map((obs, displayIndex) => {
|
|
578
|
+
const actualIndex = indices[displayIndex];
|
|
579
|
+
const isSelected = actualIndex === selectedObservationIndex;
|
|
580
|
+
const isChecked = checkedObservations.has(actualIndex);
|
|
581
|
+
const name = obs.name || obs.id || ("Observation " + (displayIndex + 1));
|
|
582
|
+
const type = obs.type || "UNKNOWN";
|
|
583
|
+
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
584
|
+
|
|
585
|
+
if (currentStep === 3) {
|
|
586
|
+
// Step 3: Mark broken - show checkboxes
|
|
587
|
+
return \`<tr class="\${isSelected ? "selected" : ""}">
|
|
588
|
+
<td style="width: 40px;"><input type="checkbox" class="obs-checkbox" value="\${actualIndex}" \${isChecked ? "checked" : ""}></td>
|
|
589
|
+
<td onclick="selectObservation(\${actualIndex})">\${esc(name)}</td>
|
|
590
|
+
<td><span class="obs-type \${typeClass}">\${esc(type)}</span></td>
|
|
591
|
+
</tr>\`;
|
|
592
|
+
} else if (currentStep === 4) {
|
|
593
|
+
// Step 4: Verify - show rerun button in Action column
|
|
594
|
+
return \`<tr class="\${isSelected ? "selected" : ""}">
|
|
595
|
+
<td onclick="selectObservation(\${actualIndex})">\${esc(name)}</td>
|
|
596
|
+
<td><span class="obs-type \${typeClass}">\${esc(type)}</span></td>
|
|
597
|
+
<td><button class="btn btn-primary rerun-btn" data-index="\${actualIndex}">Rerun</button><span class="rerun-spinner" style="display:none;margin-left:8px;"><span class="spinner"></span></span></td>
|
|
598
|
+
</tr>\`;
|
|
599
|
+
}
|
|
600
|
+
}).join("");
|
|
601
|
+
|
|
602
|
+
// Add checkbox event listeners for Step 3
|
|
603
|
+
if (currentStep === 3) {
|
|
604
|
+
document.querySelectorAll(".obs-checkbox").forEach(checkbox => {
|
|
605
|
+
checkbox.onchange = (e) => {
|
|
606
|
+
const idx = parseInt(e.target.value);
|
|
607
|
+
if (e.target.checked) {
|
|
608
|
+
checkedObservations.add(idx);
|
|
609
|
+
} else {
|
|
610
|
+
checkedObservations.delete(idx);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function selectObservation(index) {
|
|
618
|
+
selectedObservationIndex = index;
|
|
619
|
+
renderObservationTable();
|
|
620
|
+
const obs = currentObservations[index];
|
|
621
|
+
const inputText = toDisplayText(obs.input, obs.type);
|
|
622
|
+
const outputText = toDisplayText(obs.output, obs.type);
|
|
623
|
+
const mockFilePath = "/mock/path/relevant-function.ts";
|
|
624
|
+
|
|
625
|
+
observationDetail.innerHTML = \`<div class="detail-sections">
|
|
626
|
+
<div class="detail-section">
|
|
627
|
+
<div class="detail-title">File Path</div>
|
|
628
|
+
<pre class="detail-pre">\${esc(mockFilePath)}</pre>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="detail-section">
|
|
631
|
+
<div class="detail-title">Input</div>
|
|
632
|
+
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="detail-section">
|
|
635
|
+
<div class="detail-title">Output</div>
|
|
636
|
+
<pre class="detail-pre">\${esc(outputText)}</pre>
|
|
637
|
+
</div>
|
|
638
|
+
</div>\`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function toDisplayText(value, type) {
|
|
642
|
+
if (value === null || value === undefined || value === "") {
|
|
643
|
+
return "No data";
|
|
644
|
+
}
|
|
645
|
+
if (typeof value === "string") {
|
|
646
|
+
return value;
|
|
647
|
+
}
|
|
648
|
+
if (type === "GENERATION" && value.messages) {
|
|
649
|
+
return JSON.stringify(value.messages, null, 2);
|
|
650
|
+
}
|
|
651
|
+
if (type === "GENERATION" && value.role && value.content) {
|
|
652
|
+
return value.content || "No content";
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
return JSON.stringify(value, null, 2);
|
|
656
|
+
} catch {
|
|
657
|
+
return String(value);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function resetTraceModal() {
|
|
662
|
+
uploadArea.classList.remove("hidden");
|
|
663
|
+
traceViewer.classList.remove("visible");
|
|
664
|
+
modalFooter.classList.remove("visible");
|
|
665
|
+
let customFooter = document.getElementById("step5FooterBtns");
|
|
666
|
+
if (customFooter) customFooter.remove();
|
|
667
|
+
uploadStatus.className = "upload-status";
|
|
668
|
+
uploadStatus.textContent = "";
|
|
669
|
+
fileInput.value = "";
|
|
670
|
+
currentObservations = [];
|
|
671
|
+
selectedObservationIndex = -1;
|
|
672
|
+
checkedObservations.clear();
|
|
673
|
+
observationTableBody.innerHTML = "";
|
|
674
|
+
observationDetail.innerHTML = "";
|
|
675
|
+
currentStep = 0;
|
|
676
|
+
updateModalTitle();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function updateModalTitle() {
|
|
680
|
+
const titles = {
|
|
681
|
+
0: "Import Trace for Analysis",
|
|
682
|
+
3: "Step 3: Mark broken step",
|
|
683
|
+
4: "Step 4: Verify your fix",
|
|
684
|
+
5: "Step 5: Validate updated flow with live data"
|
|
685
|
+
};
|
|
686
|
+
modalTitle.textContent = titles[currentStep] || "Import Trace for Analysis";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function updateFooterButtons() {
|
|
690
|
+
const changeBtn = document.getElementById("changeTraceBtn");
|
|
691
|
+
const nextBtn = document.getElementById("nextBtn");
|
|
692
|
+
|
|
693
|
+
if (currentStep === 3) {
|
|
694
|
+
changeBtn.textContent = "Change Trace File";
|
|
695
|
+
nextBtn.textContent = "Next";
|
|
696
|
+
nextBtn.disabled = false;
|
|
697
|
+
} else if (currentStep === 4) {
|
|
698
|
+
changeBtn.textContent = "Select Different Steps";
|
|
699
|
+
nextBtn.textContent = "Fix Works as Expected";
|
|
700
|
+
nextBtn.disabled = false;
|
|
701
|
+
} else if (currentStep === 5) {
|
|
702
|
+
// Step 5 - show custom buttons
|
|
703
|
+
changeBtn.textContent = "Still Failing";
|
|
704
|
+
nextBtn.textContent = "Done";
|
|
705
|
+
} else {
|
|
706
|
+
let customFooter = document.getElementById("step5FooterBtns");
|
|
707
|
+
if (customFooter) customFooter.remove();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
fetch("/api/workflows").then(r => r.json()).then(d => {
|
|
712
|
+
console.log("[Dashboard] Workflows fetched:", d);
|
|
713
|
+
allWorkflows = d.workflows || [];
|
|
714
|
+
console.log("[Dashboard] Calling render with", allWorkflows.length, "workflows");
|
|
715
|
+
render();
|
|
716
|
+
}).catch(err => {
|
|
717
|
+
console.error("Failed to fetch workflows:", err);
|
|
718
|
+
tbody.innerHTML = '<tr><td colspan="2" style="text-align: center; padding: 40px; color: #c62828;">Error loading workflows</td></tr>';
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
fetch("/api/code-index").then(r => r.json()).then(d => {
|
|
722
|
+
codeIndex = d;
|
|
723
|
+
console.log("Code index:", d);
|
|
724
|
+
}).catch(err => {
|
|
725
|
+
console.error("Failed to fetch code index:", err);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
function render(search = "") {
|
|
729
|
+
console.log("[Dashboard] render() called with search:", search, "workflows:", allWorkflows.length);
|
|
730
|
+
const filtered = search ? allWorkflows.filter(w =>
|
|
731
|
+
w.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
732
|
+
w.filePath.toLowerCase().includes(search.toLowerCase())
|
|
733
|
+
) : allWorkflows;
|
|
734
|
+
countEl.textContent = filtered.length;
|
|
735
|
+
console.log("[Dashboard] Rendering", filtered.length, "workflows");
|
|
736
|
+
tbody.innerHTML = filtered.length ? filtered.map((w, i) => \`<tr onclick="showModal(\${i},'\${search}')">
|
|
737
|
+
<td><div class="workflow-name-cell">\${esc(w.name)}\${w.isAsync ? '<span class="async-badge">async</span>' : ""}</div></td>
|
|
738
|
+
<td><div class="workflow-path-cell">\${esc(w.filePath)}</div></td>
|
|
739
|
+
</tr>\`).join("") : \`<tr><td colspan="2" style="text-align: center; padding: 40px; color: #999;">No workflows found</td></tr>\`;
|
|
740
|
+
console.log("[Dashboard] tbody updated");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function showModal(index, search) {
|
|
744
|
+
const filtered = search ? allWorkflows.filter(w =>
|
|
745
|
+
w.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
746
|
+
w.filePath.toLowerCase().includes(search.toLowerCase())
|
|
747
|
+
) : allWorkflows;
|
|
748
|
+
selectedWorkflow = filtered[index];
|
|
749
|
+
modal.classList.add("open");
|
|
750
|
+
resetTraceModal();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
window.showModal = showModal;
|
|
754
|
+
window.selectObservation = selectObservation;
|
|
755
|
+
function esc(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; }
|
|
756
|
+
window.esc = esc;
|
|
757
|
+
|
|
758
|
+
document.getElementById("searchInput").oninput = (e) => render(e.target.value);
|
|
759
|
+
</script>
|
|
760
|
+
</body>
|
|
761
|
+
</html>`;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Start the dashboard server
|
|
765
|
+
*/
|
|
766
|
+
export async function startDashboardServer(cwd, options = {}) {
|
|
767
|
+
const port = options.port ?? 4573;
|
|
768
|
+
const autoOpen = options.autoOpen ?? true;
|
|
769
|
+
// Scan workflows and tools once at startup
|
|
770
|
+
const workflows = await scanWorkflows(cwd);
|
|
771
|
+
const tools = await scanTools(cwd);
|
|
772
|
+
const codeIndex = { workflows, tools };
|
|
773
|
+
console.log(`[elasticdash] Scanned: ${workflows.length} workflows, ${tools.length} tools`);
|
|
774
|
+
// Create HTTP server
|
|
775
|
+
const server = http.createServer((req, res) => {
|
|
776
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
777
|
+
if (url.pathname === '/api/workflows') {
|
|
778
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
779
|
+
res.end(JSON.stringify({ workflows }));
|
|
780
|
+
}
|
|
781
|
+
else if (url.pathname === '/api/code-index') {
|
|
782
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
783
|
+
res.end(JSON.stringify(codeIndex));
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
787
|
+
res.end(getDashboardHtml());
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
// Start listening
|
|
791
|
+
await new Promise((resolve, reject) => {
|
|
792
|
+
server.listen(port, () => resolve());
|
|
793
|
+
server.on('error', reject);
|
|
794
|
+
});
|
|
795
|
+
const url = `http://localhost:${port}`;
|
|
796
|
+
// Auto-open browser
|
|
797
|
+
if (autoOpen) {
|
|
798
|
+
openBrowser(url);
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
url,
|
|
802
|
+
async close() {
|
|
803
|
+
return new Promise((resolve, reject) => {
|
|
804
|
+
server.close((err) => {
|
|
805
|
+
if (err)
|
|
806
|
+
reject(err);
|
|
807
|
+
else
|
|
808
|
+
resolve();
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=dashboard-server.js.map
|