vision-navigator 1.0.0

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.
@@ -0,0 +1,567 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Vision Navigator Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --primary: #0066cc;
10
+ --primary-hover: #0052a3;
11
+ --bg-color: #f5f7fa;
12
+ --panel-bg: #ffffff;
13
+ --text-main: #333333;
14
+ --text-muted: #666666;
15
+ --border-color: #e1e4e8;
16
+ --success: #28a745;
17
+ --error: #dc3545;
18
+ --warning: #ffaa00;
19
+ --info-bg: #e6f7ff;
20
+ --info-border: #1890ff;
21
+ }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
25
+ margin: 0;
26
+ padding: 0;
27
+ background-color: var(--bg-color);
28
+ color: var(--text-main);
29
+ }
30
+
31
+ .header-bar {
32
+ background-color: white;
33
+ padding: 15px 30px;
34
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
35
+ margin-bottom: 30px;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ }
40
+
41
+ .header-bar h1 {
42
+ margin: 0;
43
+ font-size: 1.5rem;
44
+ color: var(--primary);
45
+ }
46
+
47
+ .container {
48
+ max-width: 1400px;
49
+ margin: 0 auto;
50
+ padding: 0 20px;
51
+ display: grid;
52
+ grid-template-columns: 350px 1fr;
53
+ gap: 30px;
54
+ }
55
+
56
+ .panel {
57
+ background: var(--panel-bg);
58
+ padding: 25px;
59
+ border-radius: 10px;
60
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
61
+ border: 1px solid var(--border-color);
62
+ }
63
+
64
+ .panel h2 {
65
+ margin-top: 0;
66
+ font-size: 1.2rem;
67
+ border-bottom: 2px solid var(--bg-color);
68
+ padding-bottom: 10px;
69
+ margin-bottom: 20px;
70
+ }
71
+
72
+ .input-group {
73
+ margin-bottom: 20px;
74
+ }
75
+
76
+ .input-group label {
77
+ display: block;
78
+ font-weight: 600;
79
+ margin-bottom: 8px;
80
+ color: var(--text-main);
81
+ }
82
+
83
+ textarea {
84
+ width: 100%;
85
+ height: 400px;
86
+ font-family: 'Monaco', 'Consolas', monospace;
87
+ font-size: 14px;
88
+ padding: 15px;
89
+ border: 1px solid var(--border-color);
90
+ border-radius: 6px;
91
+ resize: vertical;
92
+ box-sizing: border-box;
93
+ background-color: #fafbfc;
94
+ line-height: 1.5;
95
+ }
96
+
97
+ textarea:focus {
98
+ outline: none;
99
+ border-color: var(--primary);
100
+ box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
101
+ }
102
+
103
+ button {
104
+ background-color: var(--primary);
105
+ color: white;
106
+ border: none;
107
+ padding: 12px 24px;
108
+ font-size: 16px;
109
+ font-weight: 600;
110
+ border-radius: 6px;
111
+ cursor: pointer;
112
+ width: 100%;
113
+ transition: background-color 0.2s, transform 0.1s;
114
+ }
115
+
116
+ button:hover:not(:disabled) {
117
+ background-color: var(--primary-hover);
118
+ transform: translateY(-1px);
119
+ }
120
+
121
+ button:disabled {
122
+ background-color: #cccccc;
123
+ cursor: not-allowed;
124
+ opacity: 0.7;
125
+ }
126
+
127
+ .results {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 25px;
131
+ }
132
+
133
+ .step {
134
+ border: 1px solid var(--border-color);
135
+ border-radius: 8px;
136
+ background: var(--panel-bg);
137
+ overflow: hidden;
138
+ box-shadow: 0 2px 4px rgba(0,0,0,0.02);
139
+ }
140
+
141
+ .step-header {
142
+ font-weight: 600;
143
+ font-size: 1.1em;
144
+ padding: 15px 20px;
145
+ background-color: #f8f9fa;
146
+ border-bottom: 1px solid var(--border-color);
147
+ display: flex;
148
+ justify-content: space-between;
149
+ align-items: center;
150
+ }
151
+
152
+ .step-body {
153
+ padding: 20px;
154
+ }
155
+
156
+ .status-badge {
157
+ padding: 4px 12px;
158
+ border-radius: 20px;
159
+ font-size: 0.85em;
160
+ font-weight: 600;
161
+ text-transform: uppercase;
162
+ }
163
+
164
+ .status-executed {
165
+ background-color: rgba(40, 167, 69, 0.1);
166
+ color: var(--success);
167
+ }
168
+
169
+ .status-failed {
170
+ background-color: rgba(220, 53, 69, 0.1);
171
+ color: var(--error);
172
+ }
173
+
174
+ .section-title {
175
+ font-size: 0.9em;
176
+ font-weight: 700;
177
+ color: var(--text-muted);
178
+ text-transform: uppercase;
179
+ letter-spacing: 0.5px;
180
+ margin: 15px 0 8px 0;
181
+ }
182
+
183
+ .action-box {
184
+ background: #f8f9fa;
185
+ padding: 12px;
186
+ border-radius: 6px;
187
+ border-left: 4px solid var(--text-muted);
188
+ font-family: monospace;
189
+ margin-bottom: 15px;
190
+ }
191
+
192
+ .ai-suggestions {
193
+ background: var(--info-bg);
194
+ border-left: 4px solid var(--info-border);
195
+ padding: 15px;
196
+ border-radius: 0 6px 6px 0;
197
+ margin-bottom: 15px;
198
+ }
199
+
200
+ .ai-suggestions p {
201
+ margin: 0;
202
+ line-height: 1.5;
203
+ white-space: pre-wrap;
204
+ }
205
+
206
+ .logs-container {
207
+ display: grid;
208
+ grid-template-columns: 1fr 1fr;
209
+ gap: 15px;
210
+ margin-bottom: 15px;
211
+ }
212
+
213
+ .logs {
214
+ background: #2b2b2b;
215
+ color: #a9b7c6;
216
+ font-family: 'Monaco', monospace;
217
+ padding: 12px;
218
+ border-radius: 6px;
219
+ font-size: 0.85em;
220
+ max-height: 200px;
221
+ overflow-y: auto;
222
+ line-height: 1.4;
223
+ }
224
+
225
+ .logs.network {
226
+ color: #ffc66d;
227
+ }
228
+
229
+ .images {
230
+ display: grid;
231
+ grid-template-columns: 1fr 1fr;
232
+ gap: 20px;
233
+ margin-top: 20px;
234
+ border-top: 1px solid var(--border-color);
235
+ padding-top: 20px;
236
+ }
237
+
238
+ .image-container {
239
+ background: #f8f9fa;
240
+ padding: 10px;
241
+ border-radius: 8px;
242
+ text-align: center;
243
+ }
244
+
245
+ .image-container p {
246
+ margin: 0 0 10px 0;
247
+ font-weight: 600;
248
+ color: var(--text-muted);
249
+ }
250
+
251
+ img {
252
+ max-width: 100%;
253
+ height: auto;
254
+ border: 1px solid var(--border-color);
255
+ border-radius: 4px;
256
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
257
+ }
258
+
259
+ .loader-container {
260
+ display: none;
261
+ text-align: center;
262
+ margin-top: 20px;
263
+ padding: 20px;
264
+ background: #f8f9fa;
265
+ border-radius: 8px;
266
+ }
267
+
268
+ .spinner {
269
+ border: 4px solid rgba(0, 0, 0, 0.1);
270
+ width: 36px;
271
+ height: 36px;
272
+ border-radius: 50%;
273
+ border-left-color: var(--primary);
274
+ animation: spin 1s linear infinite;
275
+ margin: 0 auto 15px auto;
276
+ }
277
+
278
+ @keyframes spin {
279
+ 0% { transform: rotate(0deg); }
280
+ 100% { transform: rotate(360deg); }
281
+ }
282
+
283
+ .loader-text {
284
+ font-weight: 600;
285
+ color: var(--primary);
286
+ }
287
+
288
+ .empty-state {
289
+ text-align: center;
290
+ padding: 60px 20px;
291
+ color: var(--text-muted);
292
+ }
293
+
294
+ .empty-state svg {
295
+ width: 64px;
296
+ height: 64px;
297
+ margin-bottom: 20px;
298
+ opacity: 0.2;
299
+ }
300
+ </style>
301
+ </head>
302
+ <body>
303
+ <div class="header-bar">
304
+ <h1>👁️ Vision Navigator</h1>
305
+ <div style="font-size: 0.9em; color: var(--text-muted);">AI-Powered Browser Automation</div>
306
+ </div>
307
+
308
+ <div class="container">
309
+ <div class="sidebar">
310
+ <div class="panel">
311
+ <h2>Workflow Configuration</h2>
312
+ <div class="input-group">
313
+ <label for="yaml-input">YAML Script</label>
314
+ <textarea id="yaml-input" spellcheck="false">steps:
315
+ - instruction: "Navigate to http://quotes.toscrape.com/"
316
+ - instruction: "Click the 'Login' link near the top right"
317
+ - instruction: "Type 'admin' into the username field"
318
+ - instruction: "Type 'password123' into the password field"
319
+ - instruction: "Click the Login button"</textarea>
320
+ </div>
321
+ <button id="run-btn" onclick="runWorkflow()">▶ Run Workflow</button>
322
+
323
+ <div id="loader" class="loader-container">
324
+ <div class="spinner"></div>
325
+ <div class="loader-text" id="loader-text">Executing workflow...</div>
326
+ <div style="font-size: 0.85em; color: var(--text-muted); margin-top: 5px;">This may take a few minutes.</div>
327
+ </div>
328
+ </div>
329
+
330
+ <div class="panel" style="margin-top: 20px; max-height: 400px; overflow-y: auto;">
331
+ <h2>Past Runs</h2>
332
+ <div id="past-runs-container">
333
+ <div style="text-align: center; color: var(--text-muted); padding: 20px 0;">Loading...</div>
334
+ </div>
335
+ </div>
336
+ </div>
337
+
338
+ <div class="panel" style="overflow-y: auto; max-height: calc(100vh - 120px);">
339
+ <h2>Execution Results</h2>
340
+ <div id="results-container" class="results">
341
+ <div class="empty-state">
342
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
343
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
344
+ </svg>
345
+ <p>No results yet. Run a workflow to see output here.</p>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+
351
+ <script>
352
+ document.addEventListener('DOMContentLoaded', loadPastRuns);
353
+
354
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname : window.location.pathname + '/';
355
+ const urlFor = (p) => basePath + String(p || '').replace(/^\//, '');
356
+
357
+ async function loadPastRuns() {
358
+ const container = document.getElementById('past-runs-container');
359
+ try {
360
+ const response = await fetch(urlFor('runs'));
361
+ const runs = await response.json();
362
+
363
+ if (runs.length === 0) {
364
+ container.innerHTML = '<div style="text-align: center; color: var(--text-muted); padding: 20px 0;">No past runs found.</div>';
365
+ return;
366
+ }
367
+
368
+ container.innerHTML = runs.map(run => `
369
+ <div style="border: 1px solid var(--border-color); border-radius: 6px; padding: 10px; margin-bottom: 10px; background: #fafbfc;">
370
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
371
+ <strong style="font-size: 0.9em;">${new Date(run.timestamp * 1000).toLocaleString()}</strong>
372
+ <span class="status-badge ${run.status === 'completed' ? 'status-executed' : 'status-failed'}" style="font-size: 0.7em;">${run.status}</span>
373
+ </div>
374
+ <div style="font-size: 0.85em; color: var(--text-muted);">
375
+ Steps: ${run.steps} | ID: ${run.id}
376
+ </div>
377
+ </div>
378
+ `).join('');
379
+ } catch (e) {
380
+ container.innerHTML = '<div style="text-align: center; color: var(--error); padding: 20px 0;">Failed to load runs.</div>';
381
+ }
382
+ }
383
+
384
+ async function runWorkflow() {
385
+ const yamlInput = document.getElementById('yaml-input').value;
386
+ const btn = document.getElementById('run-btn');
387
+ const loader = document.getElementById('loader');
388
+ const loaderText = document.getElementById('loader-text');
389
+ const resultsContainer = document.getElementById('results-container');
390
+
391
+ btn.disabled = true;
392
+ btn.innerHTML = 'Executing...';
393
+ loader.style.display = 'block';
394
+ loaderText.innerText = 'Starting workflow...';
395
+ resultsContainer.innerHTML = '';
396
+
397
+ let currentResults = [];
398
+
399
+ try {
400
+ const response = await fetch(urlFor('workflow/stream'), {
401
+ method: 'POST',
402
+ headers: {
403
+ 'Content-Type': 'text/yaml'
404
+ },
405
+ body: yamlInput
406
+ });
407
+
408
+ if (!response.ok) {
409
+ throw new Error('Server returned ' + response.status);
410
+ }
411
+
412
+ const reader = response.body.getReader();
413
+ const decoder = new TextDecoder();
414
+ let buffer = '';
415
+
416
+ while (true) {
417
+ const { done, value } = await reader.read();
418
+ if (done) break;
419
+
420
+ buffer += decoder.decode(value, { stream: true });
421
+ const lines = buffer.split('\n\n');
422
+ buffer = lines.pop() || ''; // Keep the last incomplete chunk
423
+
424
+ for (const line of lines) {
425
+ if (line.startsWith('data: ')) {
426
+ const dataStr = line.substring(6);
427
+ try {
428
+ const data = JSON.parse(dataStr);
429
+ if (data.error) {
430
+ throw new Error(data.error);
431
+ } else if (data.step) {
432
+ // Update real-time step
433
+ loaderText.innerText = `Executing: ${data.step.step}...`;
434
+
435
+ // Find if we already have this step
436
+ const existingIndex = currentResults.findIndex(r => r.step === data.step.step && r.status === 'pending');
437
+ if (existingIndex >= 0) {
438
+ currentResults[existingIndex] = data.step;
439
+ } else {
440
+ currentResults.push(data.step);
441
+ }
442
+ displayResults(currentResults);
443
+ } else if (data.done) {
444
+ currentResults = data.run?.results || data.results || [];
445
+ displayResults(currentResults);
446
+ loadPastRuns();
447
+ }
448
+ } catch (e) {
449
+ console.error("Parse error:", e, dataStr);
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ } catch (error) {
456
+ resultsContainer.innerHTML = `
457
+ <div class="step" style="border-left: 4px solid var(--error);">
458
+ <div class="step-body">
459
+ <h3 style="color: var(--error); margin-top: 0;">Execution Failed</h3>
460
+ <p>${error.message}</p>
461
+ </div>
462
+ </div>`;
463
+ } finally {
464
+ btn.disabled = false;
465
+ btn.innerHTML = '▶ Run Workflow';
466
+ loader.style.display = 'none';
467
+ }
468
+ }
469
+
470
+ function displayResults(results) {
471
+ const container = document.getElementById('results-container');
472
+ container.innerHTML = '';
473
+
474
+ if (!results || results.length === 0) {
475
+ container.innerHTML = '<p>Workflow finished but returned no results.</p>';
476
+ return;
477
+ }
478
+
479
+ results.forEach((step, index) => {
480
+ const stepEl = document.createElement('div');
481
+ stepEl.className = 'step';
482
+
483
+ const isSuccess = step.status === 'executed' || step.status === 'completed';
484
+ const statusClass = isSuccess ? 'status-executed' : 'status-failed';
485
+ const statusIcon = isSuccess ? '✅' : '❌';
486
+
487
+ let html = `
488
+ <div class="step-header">
489
+ <span>${index + 1}. ${step.step}</span>
490
+ <span class="status-badge ${statusClass}">${statusIcon} ${step.status}</span>
491
+ </div>
492
+ <div class="step-body">
493
+ `;
494
+
495
+ if (step.action) {
496
+ html += `
497
+ <div class="section-title">Model Action</div>
498
+ <div class="action-box">${step.action}</div>
499
+ `;
500
+ }
501
+
502
+ if (step.ai_suggestions) {
503
+ html += `
504
+ <div class="ai-suggestions">
505
+ <strong>💡 AI Performance & Usability Insights</strong>
506
+ <p>${step.ai_suggestions}</p>
507
+ </div>
508
+ `;
509
+ }
510
+
511
+ if ((step.logs && step.logs.length > 0) || (step.network_issues && step.network_issues.length > 0)) {
512
+ html += `<div class="logs-container">`;
513
+
514
+ if (step.logs && step.logs.length > 0) {
515
+ html += `
516
+ <div>
517
+ <div class="section-title">Console Logs</div>
518
+ <div class="logs">${step.logs.join('<br>')}</div>
519
+ </div>
520
+ `;
521
+ }
522
+
523
+ if (step.network_issues && step.network_issues.length > 0) {
524
+ html += `
525
+ <div>
526
+ <div class="section-title">Network Issues</div>
527
+ <div class="logs network">${step.network_issues.join('<br>')}</div>
528
+ </div>
529
+ `;
530
+ }
531
+
532
+ html += `</div>`;
533
+ }
534
+
535
+ if (step.performance_metrics) {
536
+ html += `
537
+ <div class="section-title">Performance Metrics</div>
538
+ <div class="logs" style="max-height: 100px;">${JSON.stringify(step.performance_metrics, null, 2)}</div>
539
+ `;
540
+ }
541
+
542
+ html += `<div class="images">`;
543
+ if (step.screenshots?.before) {
544
+ html += `
545
+ <div class="image-container">
546
+ <p>Before Action</p>
547
+ <img src="${urlFor(step.screenshots.before)}?t=${Date.now()}" alt="Before step">
548
+ </div>
549
+ `;
550
+ }
551
+ if (step.screenshots?.after) {
552
+ html += `
553
+ <div class="image-container">
554
+ <p>After Action</p>
555
+ <img src="${urlFor(step.screenshots.after)}?t=${Date.now()}" alt="After step">
556
+ </div>
557
+ `;
558
+ }
559
+ html += `</div></div>`; // Close images and step-body
560
+
561
+ stepEl.innerHTML = html;
562
+ container.appendChild(stepEl);
563
+ });
564
+ }
565
+ </script>
566
+ </body>
567
+ </html>