voyageai-cli 1.26.1 → 1.28.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.
- package/package.json +1 -1
- package/src/commands/config.js +33 -0
- package/src/commands/doctor.js +157 -14
- package/src/commands/mcp-server.js +4 -1
- package/src/commands/playground.js +212 -4
- package/src/commands/workflow.js +45 -0
- package/src/lib/api.js +40 -2
- package/src/lib/workflow.js +98 -2
- package/src/mcp/server.js +15 -2
- package/src/mcp/sse-transport.js +112 -0
- package/src/playground/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/index.html +3377 -168
|
@@ -573,7 +573,7 @@ body:not(.is-electron) .sidebar-drag-region {
|
|
|
573
573
|
background-repeat: no-repeat;
|
|
574
574
|
opacity: 0.05;
|
|
575
575
|
pointer-events: none;
|
|
576
|
-
z-index:
|
|
576
|
+
z-index: -1;
|
|
577
577
|
filter: invert(1);
|
|
578
578
|
}
|
|
579
579
|
|
|
@@ -593,7 +593,7 @@ body:not(.is-electron) .content-drag-region { display: none; }
|
|
|
593
593
|
body:not(.is-electron) .update-banner { display: none !important; }
|
|
594
594
|
|
|
595
595
|
/* Main */
|
|
596
|
-
.main { padding: 24px; max-width: 1200px; margin: 0 auto; width: 100%; }
|
|
596
|
+
.main { padding: 24px; max-width: 1200px; margin: 0 auto; width: 100%; flex: 1; overflow-y: auto; min-height: 0; }
|
|
597
597
|
|
|
598
598
|
/* Legacy compat — hide old horizontal bar (replaced by sidebar) */
|
|
599
599
|
.nav { display: none; }
|
|
@@ -992,8 +992,9 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
992
992
|
}
|
|
993
993
|
|
|
994
994
|
.explore-card-icon {
|
|
995
|
-
font-size: 28px;
|
|
996
995
|
margin-bottom: 8px;
|
|
996
|
+
color: var(--accent, #00D4AA);
|
|
997
|
+
opacity: 0.85;
|
|
997
998
|
}
|
|
998
999
|
.explore-card-title {
|
|
999
1000
|
font-size: 16px;
|
|
@@ -1056,7 +1057,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
1056
1057
|
padding: 24px 28px 16px;
|
|
1057
1058
|
border-bottom: 1px solid var(--border);
|
|
1058
1059
|
}
|
|
1059
|
-
.explore-modal-icon {
|
|
1060
|
+
.explore-modal-icon { color: var(--accent, #00D4AA); }
|
|
1060
1061
|
.explore-modal-title {
|
|
1061
1062
|
font-size: 18px;
|
|
1062
1063
|
font-weight: 600;
|
|
@@ -1825,6 +1826,34 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
1825
1826
|
line-height: 1.5;
|
|
1826
1827
|
}
|
|
1827
1828
|
|
|
1829
|
+
/* ── Contextual docs link ── */
|
|
1830
|
+
.page-header { position: relative; }
|
|
1831
|
+
.page-header-docs {
|
|
1832
|
+
position: absolute; top: 0; right: 0;
|
|
1833
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
1834
|
+
font-size: 12px; font-weight: 500;
|
|
1835
|
+
color: var(--text-muted); text-decoration: none;
|
|
1836
|
+
padding: 4px 10px; border-radius: 6px;
|
|
1837
|
+
border: 1px solid var(--border);
|
|
1838
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
1839
|
+
}
|
|
1840
|
+
.page-header-docs:hover {
|
|
1841
|
+
color: var(--accent, #6c63ff); border-color: var(--accent, #6c63ff);
|
|
1842
|
+
background: rgba(108,99,255,0.06);
|
|
1843
|
+
}
|
|
1844
|
+
.page-header-docs svg { opacity: 0.6; }
|
|
1845
|
+
.page-header-docs:hover svg { opacity: 1; }
|
|
1846
|
+
.sidebar-docs-link {
|
|
1847
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1848
|
+
font-size: 10px; color: var(--text-muted); text-decoration: none;
|
|
1849
|
+
padding: 2px 6px; border-radius: 4px;
|
|
1850
|
+
transition: color 0.15s, background 0.15s;
|
|
1851
|
+
}
|
|
1852
|
+
.sidebar-docs-link:hover {
|
|
1853
|
+
color: var(--accent, #6c63ff); background: rgba(108,99,255,0.08);
|
|
1854
|
+
}
|
|
1855
|
+
.sidebar-docs-link svg { width: 12px; height: 12px; }
|
|
1856
|
+
|
|
1828
1857
|
/* ── Settings page ── */
|
|
1829
1858
|
.settings-container { max-width: 640px; margin: 0 auto; }
|
|
1830
1859
|
.settings-section {
|
|
@@ -2052,6 +2081,34 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2052
2081
|
transition: opacity 0.3s;
|
|
2053
2082
|
}
|
|
2054
2083
|
.settings-saved.show { opacity: 1; }
|
|
2084
|
+
.doctor-check {
|
|
2085
|
+
display: flex;
|
|
2086
|
+
align-items: flex-start;
|
|
2087
|
+
gap: 10px;
|
|
2088
|
+
padding: 10px 14px;
|
|
2089
|
+
border-radius: var(--radius);
|
|
2090
|
+
background: var(--bg-input);
|
|
2091
|
+
border: 1px solid var(--border);
|
|
2092
|
+
}
|
|
2093
|
+
.doctor-check-icon { font-size: 15px; flex-shrink: 0; line-height: 1.4; }
|
|
2094
|
+
.doctor-check-body { flex: 1; min-width: 0; }
|
|
2095
|
+
.doctor-check-name { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
2096
|
+
.doctor-check-msg { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
|
|
2097
|
+
.doctor-check-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
|
|
2098
|
+
.doctor-check-badge {
|
|
2099
|
+
font-size: 10px;
|
|
2100
|
+
padding: 2px 8px;
|
|
2101
|
+
border-radius: 10px;
|
|
2102
|
+
font-weight: 600;
|
|
2103
|
+
text-transform: uppercase;
|
|
2104
|
+
letter-spacing: 0.5px;
|
|
2105
|
+
flex-shrink: 0;
|
|
2106
|
+
align-self: center;
|
|
2107
|
+
}
|
|
2108
|
+
.doctor-check-badge.pass { background: rgba(0,212,170,0.12); color: var(--success); }
|
|
2109
|
+
.doctor-check-badge.fail { background: rgba(255,105,96,0.12); color: var(--error); }
|
|
2110
|
+
.doctor-check-badge.warn { background: rgba(255,192,16,0.12); color: var(--warning); }
|
|
2111
|
+
.doctor-check-badge.optional { background: rgba(136,147,151,0.12); color: var(--text-muted); }
|
|
2055
2112
|
.settings-reset-btn {
|
|
2056
2113
|
background: transparent;
|
|
2057
2114
|
border: 1px solid var(--border);
|
|
@@ -2316,6 +2373,25 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2316
2373
|
background: var(--bg-card);
|
|
2317
2374
|
border: 1px solid var(--border);
|
|
2318
2375
|
border-bottom-left-radius: 4px;
|
|
2376
|
+
position: relative;
|
|
2377
|
+
}
|
|
2378
|
+
.chat-copy-btn {
|
|
2379
|
+
position: absolute; top: 6px; right: 6px;
|
|
2380
|
+
width: 28px; height: 28px; border-radius: 6px;
|
|
2381
|
+
border: 1px solid var(--border); background: var(--bg);
|
|
2382
|
+
color: var(--text-muted); cursor: pointer;
|
|
2383
|
+
display: flex; align-items: center; justify-content: center;
|
|
2384
|
+
opacity: 0; transition: opacity 0.15s, background 0.15s, color 0.15s;
|
|
2385
|
+
pointer-events: none; z-index: 1;
|
|
2386
|
+
}
|
|
2387
|
+
.chat-message.assistant:hover .chat-copy-btn {
|
|
2388
|
+
opacity: 1; pointer-events: auto;
|
|
2389
|
+
}
|
|
2390
|
+
.chat-copy-btn:hover {
|
|
2391
|
+
background: var(--bg-card); color: var(--text); border-color: var(--text-muted);
|
|
2392
|
+
}
|
|
2393
|
+
.chat-copy-btn.copied {
|
|
2394
|
+
color: #69F0AE; border-color: #69F0AE;
|
|
2319
2395
|
}
|
|
2320
2396
|
.chat-message.system-msg {
|
|
2321
2397
|
align-self: center;
|
|
@@ -2421,21 +2497,160 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2421
2497
|
margin: 12px 0;
|
|
2422
2498
|
}
|
|
2423
2499
|
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2500
|
+
/* Agent thinking panel */
|
|
2501
|
+
.chat-thinking {
|
|
2502
|
+
align-self: flex-start;
|
|
2503
|
+
max-width: 85%;
|
|
2504
|
+
margin-bottom: 2px;
|
|
2505
|
+
font-size: 13px;
|
|
2427
2506
|
}
|
|
2428
|
-
.chat-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2507
|
+
.chat-thinking summary {
|
|
2508
|
+
cursor: pointer;
|
|
2509
|
+
list-style: none;
|
|
2510
|
+
display: flex;
|
|
2511
|
+
align-items: center;
|
|
2512
|
+
gap: 6px;
|
|
2513
|
+
padding: 8px 14px;
|
|
2514
|
+
border-radius: 10px;
|
|
2515
|
+
background: var(--bg-card);
|
|
2516
|
+
border: 1px solid var(--border);
|
|
2517
|
+
color: var(--text-muted);
|
|
2518
|
+
font-size: 13px;
|
|
2519
|
+
transition: background 0.15s, border-color 0.15s;
|
|
2520
|
+
user-select: none;
|
|
2521
|
+
}
|
|
2522
|
+
.chat-thinking summary::-webkit-details-marker { display: none; }
|
|
2523
|
+
.chat-thinking summary:hover {
|
|
2524
|
+
background: var(--bg-surface);
|
|
2525
|
+
border-color: var(--accent-dim, #009E80);
|
|
2526
|
+
}
|
|
2527
|
+
.chat-thinking[open] summary {
|
|
2528
|
+
border-radius: 10px 10px 0 0;
|
|
2529
|
+
border-bottom-color: transparent;
|
|
2530
|
+
}
|
|
2531
|
+
.chat-thinking .thinking-icon {
|
|
2532
|
+
font-size: 14px;
|
|
2533
|
+
line-height: 1;
|
|
2534
|
+
}
|
|
2535
|
+
.chat-thinking .thinking-label {
|
|
2536
|
+
font-weight: 500;
|
|
2537
|
+
}
|
|
2538
|
+
.chat-thinking .thinking-chevron {
|
|
2539
|
+
margin-left: auto;
|
|
2540
|
+
font-size: 10px;
|
|
2541
|
+
opacity: 0.5;
|
|
2542
|
+
transition: transform 0.15s;
|
|
2543
|
+
}
|
|
2544
|
+
.chat-thinking[open] .thinking-chevron {
|
|
2545
|
+
transform: rotate(90deg);
|
|
2546
|
+
}
|
|
2547
|
+
.chat-thinking .thinking-count {
|
|
2548
|
+
background: var(--bg-input, #112733);
|
|
2549
|
+
color: var(--text-muted);
|
|
2550
|
+
font-size: 11px;
|
|
2551
|
+
padding: 1px 7px;
|
|
2552
|
+
border-radius: 10px;
|
|
2553
|
+
font-weight: 600;
|
|
2554
|
+
}
|
|
2555
|
+
.chat-thinking .thinking-elapsed {
|
|
2556
|
+
font-size: 11px;
|
|
2557
|
+
opacity: 0.45;
|
|
2558
|
+
}
|
|
2559
|
+
.chat-thinking .thinking-timeline {
|
|
2560
|
+
border: 1px solid var(--border);
|
|
2561
|
+
border-top: none;
|
|
2562
|
+
border-radius: 0 0 10px 10px;
|
|
2563
|
+
padding: 10px 14px;
|
|
2564
|
+
background: var(--bg-card);
|
|
2565
|
+
}
|
|
2566
|
+
.thinking-step {
|
|
2567
|
+
display: flex;
|
|
2568
|
+
gap: 10px;
|
|
2569
|
+
padding: 7px 0;
|
|
2570
|
+
position: relative;
|
|
2571
|
+
animation: thinkingSlideIn 0.25s ease-out;
|
|
2572
|
+
}
|
|
2573
|
+
@keyframes thinkingSlideIn {
|
|
2574
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
2575
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2576
|
+
}
|
|
2577
|
+
.thinking-step + .thinking-step {
|
|
2578
|
+
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
|
2579
|
+
}
|
|
2580
|
+
.thinking-step-icon {
|
|
2581
|
+
width: 28px;
|
|
2582
|
+
height: 28px;
|
|
2583
|
+
border-radius: 50%;
|
|
2584
|
+
display: flex;
|
|
2585
|
+
align-items: center;
|
|
2586
|
+
justify-content: center;
|
|
2587
|
+
font-size: 14px;
|
|
2588
|
+
flex-shrink: 0;
|
|
2589
|
+
background: var(--bg-input, #112733);
|
|
2590
|
+
border: 1px solid var(--border);
|
|
2591
|
+
}
|
|
2592
|
+
.thinking-step.active .thinking-step-icon {
|
|
2593
|
+
border-color: var(--accent);
|
|
2594
|
+
animation: thinkingPulse 1.2s ease-in-out infinite;
|
|
2595
|
+
}
|
|
2596
|
+
@keyframes thinkingPulse {
|
|
2597
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0.25); }
|
|
2598
|
+
50% { box-shadow: 0 0 0 6px rgba(0, 212, 170, 0); }
|
|
2599
|
+
}
|
|
2600
|
+
.thinking-step.error .thinking-step-icon {
|
|
2601
|
+
border-color: #e74c3c;
|
|
2602
|
+
background: rgba(231, 76, 60, 0.08);
|
|
2603
|
+
}
|
|
2604
|
+
.thinking-step.done .thinking-step-icon {
|
|
2605
|
+
border-color: var(--accent-dim, #009E80);
|
|
2606
|
+
opacity: 0.7;
|
|
2607
|
+
}
|
|
2608
|
+
.thinking-step-body {
|
|
2609
|
+
flex: 1;
|
|
2610
|
+
min-width: 0;
|
|
2611
|
+
display: flex;
|
|
2612
|
+
flex-direction: column;
|
|
2613
|
+
justify-content: center;
|
|
2614
|
+
gap: 2px;
|
|
2615
|
+
}
|
|
2616
|
+
.thinking-step-header {
|
|
2617
|
+
display: flex;
|
|
2618
|
+
align-items: center;
|
|
2619
|
+
gap: 6px;
|
|
2620
|
+
}
|
|
2621
|
+
.thinking-step-name {
|
|
2622
|
+
font-weight: 600;
|
|
2623
|
+
color: var(--text);
|
|
2624
|
+
font-size: 12.5px;
|
|
2625
|
+
}
|
|
2626
|
+
.thinking-step-desc {
|
|
2627
|
+
font-size: 12px;
|
|
2628
|
+
color: var(--text-muted);
|
|
2629
|
+
opacity: 0.7;
|
|
2630
|
+
}
|
|
2631
|
+
.thinking-step-time {
|
|
2632
|
+
font-size: 11px;
|
|
2633
|
+
opacity: 0.45;
|
|
2634
|
+
margin-left: auto;
|
|
2635
|
+
white-space: nowrap;
|
|
2636
|
+
}
|
|
2637
|
+
.thinking-step-detail {
|
|
2638
|
+
font-size: 11.5px;
|
|
2639
|
+
color: var(--text-muted);
|
|
2640
|
+
margin-top: 2px;
|
|
2641
|
+
line-height: 1.4;
|
|
2642
|
+
overflow: hidden;
|
|
2643
|
+
text-overflow: ellipsis;
|
|
2644
|
+
display: -webkit-box;
|
|
2645
|
+
-webkit-line-clamp: 2;
|
|
2646
|
+
-webkit-box-orient: vertical;
|
|
2647
|
+
}
|
|
2648
|
+
.thinking-step-detail code {
|
|
2649
|
+
font-size: 11px;
|
|
2650
|
+
background: var(--bg-input, #112733);
|
|
2651
|
+
padding: 1px 4px;
|
|
2652
|
+
border-radius: 3px;
|
|
2433
2653
|
}
|
|
2434
|
-
.chat-tool-call .tool-icon { opacity: 0.5; font-size: 11px; }
|
|
2435
|
-
.chat-tool-call .tool-name { font-weight: 600; }
|
|
2436
|
-
.chat-tool-call .tool-time { opacity: 0.5; }
|
|
2437
|
-
.chat-tool-call.error { border-color: #e74c3c; }
|
|
2438
|
-
.chat-tool-call .tool-error { color: #e74c3c; font-size: 11px; }
|
|
2439
2654
|
.chat-input-area {
|
|
2440
2655
|
padding: 12px 16px;
|
|
2441
2656
|
border-top: 1px solid var(--border);
|
|
@@ -2750,56 +2965,654 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2750
2965
|
.settings-row { flex-direction: column; align-items: flex-start; gap: 8px; }
|
|
2751
2966
|
.settings-select, .settings-input { min-width: 100%; }
|
|
2752
2967
|
}
|
|
2968
|
+
|
|
2969
|
+
/* ========== WORKFLOW VISUALIZER ========== */
|
|
2970
|
+
/* Expand .main when workflows tab is active to fill viewport */
|
|
2971
|
+
.main:has(#tab-workflows.active) {
|
|
2972
|
+
max-width: none; padding: 0; overflow: hidden;
|
|
2973
|
+
}
|
|
2974
|
+
#tab-workflows.tab-panel.active {
|
|
2975
|
+
display: flex; flex-direction: column;
|
|
2976
|
+
height: 100%;
|
|
2977
|
+
/* Fill entire .main which is flex:1 inside the 100vh app-shell */
|
|
2978
|
+
}
|
|
2979
|
+
.wf-container {
|
|
2980
|
+
display: flex; flex: 1; gap: 0; overflow: hidden;
|
|
2981
|
+
border-radius: 0;
|
|
2982
|
+
background: var(--bg-card);
|
|
2983
|
+
min-height: 0;
|
|
2984
|
+
}
|
|
2985
|
+
.wf-library {
|
|
2986
|
+
width: 220px; min-width: 180px; flex-shrink: 0;
|
|
2987
|
+
border-right: 1px solid var(--border);
|
|
2988
|
+
display: flex; flex-direction: column;
|
|
2989
|
+
background: var(--bg);
|
|
2990
|
+
}
|
|
2991
|
+
.wf-library-header {
|
|
2992
|
+
padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
|
|
2993
|
+
color: var(--text); border-bottom: 1px solid var(--border);
|
|
2994
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
2995
|
+
}
|
|
2996
|
+
.wf-library-list {
|
|
2997
|
+
flex: 1; overflow-y: auto; padding: 8px;
|
|
2998
|
+
}
|
|
2999
|
+
.wf-library-footer {
|
|
3000
|
+
padding: 8px; border-top: 1px solid var(--border);
|
|
3001
|
+
}
|
|
3002
|
+
.wf-load-file-btn {
|
|
3003
|
+
width: 100%; padding: 7px 12px; border-radius: 6px;
|
|
3004
|
+
border: 1px dashed var(--border); background: transparent;
|
|
3005
|
+
color: var(--text-muted); cursor: pointer; font-size: 12px;
|
|
3006
|
+
display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
3007
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
3008
|
+
}
|
|
3009
|
+
.wf-load-file-btn:hover {
|
|
3010
|
+
color: var(--text); border-color: var(--text-muted); background: var(--bg-card);
|
|
3011
|
+
}
|
|
3012
|
+
.wf-library-item {
|
|
3013
|
+
padding: 10px 12px; border-radius: 8px; cursor: pointer;
|
|
3014
|
+
margin-bottom: 4px; transition: background 0.15s;
|
|
3015
|
+
border: 1px solid transparent;
|
|
3016
|
+
}
|
|
3017
|
+
.wf-library-item:hover { background: var(--bg-card); }
|
|
3018
|
+
.wf-library-item.active {
|
|
3019
|
+
background: var(--bg-card); border-color: var(--accent, #6c63ff);
|
|
3020
|
+
}
|
|
3021
|
+
.wf-library-item-name {
|
|
3022
|
+
font-weight: 600; font-size: 13px; color: var(--text);
|
|
3023
|
+
margin-bottom: 2px;
|
|
3024
|
+
}
|
|
3025
|
+
.wf-library-item-desc {
|
|
3026
|
+
font-size: 11px; color: var(--text-muted);
|
|
3027
|
+
line-height: 1.3; display: -webkit-box;
|
|
3028
|
+
-webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
3029
|
+
}
|
|
3030
|
+
.wf-library-item-meta {
|
|
3031
|
+
font-size: 10px; color: var(--text-muted); margin-top: 4px;
|
|
3032
|
+
display: flex; gap: 8px;
|
|
3033
|
+
}
|
|
3034
|
+
/* Collapsible examples section */
|
|
3035
|
+
.wf-library-section { padding: 0; margin-top: 4px; }
|
|
3036
|
+
.wf-library-section-toggle {
|
|
3037
|
+
display: flex; align-items: center; gap: 6px;
|
|
3038
|
+
width: 100%; padding: 8px 12px; border: none; background: none;
|
|
3039
|
+
color: var(--text-muted); font-size: 11px; font-weight: 600;
|
|
3040
|
+
text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer;
|
|
3041
|
+
transition: color 0.15s;
|
|
3042
|
+
}
|
|
3043
|
+
.wf-library-section-toggle:hover { color: var(--text); }
|
|
3044
|
+
.wf-library-section-toggle .arrow {
|
|
3045
|
+
font-size: 8px; transition: transform 0.2s; display: inline-block;
|
|
3046
|
+
}
|
|
3047
|
+
.wf-library-section-toggle.open .arrow { transform: rotate(90deg); }
|
|
3048
|
+
.wf-library-category {
|
|
3049
|
+
font-size: 10px; font-weight: 600; color: var(--text-muted);
|
|
3050
|
+
padding: 8px 12px 2px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
3051
|
+
}
|
|
3052
|
+
.wf-canvas-area {
|
|
3053
|
+
flex: 1; position: relative; overflow: hidden;
|
|
3054
|
+
background: var(--bg);
|
|
3055
|
+
background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
|
|
3056
|
+
background-size: 20px 20px;
|
|
3057
|
+
}
|
|
3058
|
+
.wf-canvas-toolbar {
|
|
3059
|
+
position: absolute; top: 12px; right: 12px; z-index: 10;
|
|
3060
|
+
display: flex; gap: 4px;
|
|
3061
|
+
}
|
|
3062
|
+
.wf-canvas-toolbar button {
|
|
3063
|
+
width: 32px; height: 32px; border-radius: 8px;
|
|
3064
|
+
border: 1px solid var(--border); background: var(--bg-card);
|
|
3065
|
+
color: var(--text); cursor: pointer; font-size: 14px;
|
|
3066
|
+
display: flex; align-items: center; justify-content: center;
|
|
3067
|
+
transition: background 0.15s;
|
|
3068
|
+
}
|
|
3069
|
+
.wf-canvas-toolbar button:hover { background: var(--bg); border-color: var(--text-muted); }
|
|
3070
|
+
.wf-canvas-toolbar .wf-plan-btn {
|
|
3071
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3072
|
+
font-weight: 600; font-size: 12px;
|
|
3073
|
+
}
|
|
3074
|
+
.wf-canvas-toolbar .wf-plan-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3075
|
+
.wf-canvas-toolbar .wf-run-btn {
|
|
3076
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3077
|
+
background: var(--accent, #6c63ff); color: #fff; border-color: transparent;
|
|
3078
|
+
font-weight: 600; font-size: 12px;
|
|
3079
|
+
}
|
|
3080
|
+
.wf-canvas-toolbar .wf-run-btn:hover { opacity: 0.9; }
|
|
3081
|
+
.wf-canvas-toolbar .wf-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3082
|
+
.wf-canvas-toolbar .wf-new-btn {
|
|
3083
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3084
|
+
font-weight: 600; font-size: 12px;
|
|
3085
|
+
background: var(--bg-card); border: 1px solid var(--accent);
|
|
3086
|
+
color: var(--accent);
|
|
3087
|
+
}
|
|
3088
|
+
.wf-canvas-toolbar .wf-new-btn:hover { background: var(--accent); color: #fff; }
|
|
3089
|
+
.wf-canvas-toolbar .wf-edit-btn {
|
|
3090
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3091
|
+
font-weight: 600; font-size: 12px;
|
|
3092
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3093
|
+
color: var(--text-muted);
|
|
3094
|
+
}
|
|
3095
|
+
.wf-canvas-toolbar .wf-edit-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
3096
|
+
.wf-canvas-toolbar .wf-edit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3097
|
+
.wf-canvas-toolbar .wf-edit-btn:disabled:hover { border-color: var(--border); color: var(--text-muted); }
|
|
3098
|
+
.wf-toolbar-sep {
|
|
3099
|
+
width: 1px; height: 20px; background: var(--border); margin: 0 2px;
|
|
3100
|
+
}
|
|
3101
|
+
/* Library / Palette tabs */
|
|
3102
|
+
.wf-library-tabs {
|
|
3103
|
+
display: flex; gap: 2px; width: 100%;
|
|
3104
|
+
background: var(--bg-input); border-radius: 6px; padding: 2px;
|
|
3105
|
+
}
|
|
3106
|
+
.wf-lib-tab {
|
|
3107
|
+
flex: 1; padding: 5px 0; border: none; border-radius: 4px;
|
|
3108
|
+
background: transparent; color: var(--text-muted); cursor: pointer;
|
|
3109
|
+
font-size: 11px; font-weight: 600; font-family: var(--font);
|
|
3110
|
+
transition: background 0.15s, color 0.15s; text-align: center;
|
|
3111
|
+
}
|
|
3112
|
+
.wf-lib-tab.active { background: var(--accent); color: #fff; }
|
|
3113
|
+
.wf-lib-tab:hover:not(.active) { color: var(--text); }
|
|
3114
|
+
/* Palette items */
|
|
3115
|
+
.wf-palette-category { margin-bottom: 8px; }
|
|
3116
|
+
.wf-palette-category-title {
|
|
3117
|
+
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
|
3118
|
+
letter-spacing: 0.5px; color: var(--text-muted);
|
|
3119
|
+
padding: 4px 12px; margin-bottom: 2px;
|
|
3120
|
+
}
|
|
3121
|
+
.wf-palette-item {
|
|
3122
|
+
display: flex; align-items: center; gap: 8px;
|
|
3123
|
+
padding: 7px 12px; border-radius: 6px; cursor: grab;
|
|
3124
|
+
font-size: 12px; color: var(--text); transition: background 0.15s;
|
|
3125
|
+
}
|
|
3126
|
+
.wf-palette-item:hover { background: var(--bg-card); }
|
|
3127
|
+
.wf-palette-item:active { cursor: grabbing; }
|
|
3128
|
+
.wf-palette-icon { width: 20px; text-align: center; display: flex; align-items: center; justify-content: center; }
|
|
3129
|
+
.wf-palette-label { font-weight: 500; }
|
|
3130
|
+
/* Builder port styles */
|
|
3131
|
+
.wf-port-builder { cursor: crosshair; transition: r 0.15s; pointer-events: all !important; }
|
|
3132
|
+
.wf-port-builder:hover { r: 9; filter: brightness(1.4); }
|
|
3133
|
+
.wf-inspector-delete-btn {
|
|
3134
|
+
width: 100%; padding: 8px 0; border: none; border-radius: 6px;
|
|
3135
|
+
background: var(--error); color: #fff; cursor: pointer;
|
|
3136
|
+
font-size: 12px; font-weight: 600; margin-top: 12px;
|
|
3137
|
+
transition: opacity 0.15s;
|
|
3138
|
+
}
|
|
3139
|
+
.wf-inspector-delete-btn:hover { opacity: 0.85; }
|
|
3140
|
+
.wf-validation-errors {
|
|
3141
|
+
background: rgba(255,105,96,0.08); border: 1px solid var(--error);
|
|
3142
|
+
border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;
|
|
3143
|
+
font-size: 11px; color: var(--error);
|
|
3144
|
+
}
|
|
3145
|
+
.wf-validation-errors ul { margin: 4px 0 0 16px; }
|
|
3146
|
+
/* Dry-run plan overlay */
|
|
3147
|
+
.wf-dryrun-overlay {
|
|
3148
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
3149
|
+
z-index: 8; background: rgba(0,0,0,0.5);
|
|
3150
|
+
display: flex; align-items: center; justify-content: center;
|
|
3151
|
+
animation: wf-modal-fade-in 0.15s ease;
|
|
3152
|
+
}
|
|
3153
|
+
.wf-dryrun-panel {
|
|
3154
|
+
width: 90%; max-width: 520px; max-height: 70vh;
|
|
3155
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
3156
|
+
border-radius: 12px; display: flex; flex-direction: column;
|
|
3157
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
3158
|
+
animation: wf-modal-slide-up 0.2s ease;
|
|
3159
|
+
}
|
|
3160
|
+
.wf-dryrun-header {
|
|
3161
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3162
|
+
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
|
3163
|
+
}
|
|
3164
|
+
.wf-dryrun-title { font-weight: 700; font-size: 14px; color: var(--text); }
|
|
3165
|
+
.wf-dryrun-close {
|
|
3166
|
+
border: none; background: none; color: var(--text-muted);
|
|
3167
|
+
cursor: pointer; font-size: 20px; padding: 2px 8px;
|
|
3168
|
+
}
|
|
3169
|
+
.wf-dryrun-close:hover { color: var(--text); }
|
|
3170
|
+
.wf-dryrun-body {
|
|
3171
|
+
flex: 1; overflow-y: auto; padding: 16px 20px;
|
|
3172
|
+
}
|
|
3173
|
+
.wf-dryrun-layer {
|
|
3174
|
+
margin-bottom: 16px;
|
|
3175
|
+
}
|
|
3176
|
+
.wf-dryrun-layer-title {
|
|
3177
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
3178
|
+
letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 8px;
|
|
3179
|
+
display: flex; align-items: center; gap: 8px;
|
|
3180
|
+
}
|
|
3181
|
+
.wf-dryrun-layer-badge {
|
|
3182
|
+
font-size: 10px; padding: 1px 6px; border-radius: 10px;
|
|
3183
|
+
background: var(--accent); color: #fff; font-weight: 600;
|
|
3184
|
+
}
|
|
3185
|
+
.wf-dryrun-step {
|
|
3186
|
+
padding: 8px 12px; border-radius: 6px; margin-bottom: 4px;
|
|
3187
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3188
|
+
display: flex; align-items: center; gap: 10px; font-size: 12px;
|
|
3189
|
+
}
|
|
3190
|
+
.wf-dryrun-step-icon { flex-shrink: 0; display: flex; align-items: center; }
|
|
3191
|
+
.wf-dryrun-step-info { flex: 1; min-width: 0; }
|
|
3192
|
+
.wf-dryrun-step-name { font-weight: 600; color: var(--text); }
|
|
3193
|
+
.wf-dryrun-step-tool { color: var(--text-muted); font-size: 11px; }
|
|
3194
|
+
.wf-dryrun-step-cond {
|
|
3195
|
+
font-size: 10px; color: #FFD54F; margin-top: 2px;
|
|
3196
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
3197
|
+
}
|
|
3198
|
+
.wf-dryrun-summary {
|
|
3199
|
+
padding: 12px 16px; border-radius: 8px; margin-top: 8px;
|
|
3200
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3201
|
+
font-size: 12px; color: var(--text-muted);
|
|
3202
|
+
display: flex; gap: 16px;
|
|
3203
|
+
}
|
|
3204
|
+
.wf-dryrun-stat { text-align: center; }
|
|
3205
|
+
.wf-dryrun-stat-value { font-size: 20px; font-weight: 700; color: var(--text); }
|
|
3206
|
+
.wf-dryrun-stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
3207
|
+
[data-theme="light"] .wf-dryrun-panel { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
|
|
3208
|
+
[data-theme="light"] .wf-dryrun-step-cond { color: #944F01; }
|
|
3209
|
+
.wf-canvas-toolbar .wf-stop-btn {
|
|
3210
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3211
|
+
background: #e74c3c; color: #fff; border-color: transparent;
|
|
3212
|
+
font-weight: 600; font-size: 12px;
|
|
3213
|
+
}
|
|
3214
|
+
.wf-canvas-toolbar .wf-stop-btn:hover { opacity: 0.9; background: #c0392b; }
|
|
3215
|
+
/* Execution status bar */
|
|
3216
|
+
.wf-exec-status {
|
|
3217
|
+
position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
|
|
3218
|
+
z-index: 10; display: flex; align-items: center; gap: 8px;
|
|
3219
|
+
padding: 6px 16px; border-radius: 20px;
|
|
3220
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3221
|
+
font-size: 12px; color: var(--text); box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
3222
|
+
}
|
|
3223
|
+
.wf-exec-status-dot {
|
|
3224
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
3225
|
+
background: #69F0AE; animation: wf-status-blink 1s ease-in-out infinite;
|
|
3226
|
+
}
|
|
3227
|
+
.wf-exec-status.error .wf-exec-status-dot { background: #e74c3c; animation: none; }
|
|
3228
|
+
.wf-exec-status.stopped .wf-exec-status-dot { background: #FFB74D; animation: none; }
|
|
3229
|
+
.wf-exec-status.done .wf-exec-status-dot { background: #69F0AE; animation: none; }
|
|
3230
|
+
@keyframes wf-status-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
3231
|
+
.wf-exec-status-time { color: var(--text-muted); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; }
|
|
3232
|
+
/* Output modal */
|
|
3233
|
+
.wf-output-modal-backdrop {
|
|
3234
|
+
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
3235
|
+
background: rgba(0,0,0,0.6); z-index: 9999;
|
|
3236
|
+
display: flex; align-items: center; justify-content: center;
|
|
3237
|
+
animation: wf-modal-fade-in 0.15s ease;
|
|
3238
|
+
}
|
|
3239
|
+
@keyframes wf-modal-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
3240
|
+
.wf-output-modal {
|
|
3241
|
+
width: 90%; max-width: 800px; max-height: 80vh;
|
|
3242
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
3243
|
+
border-radius: 12px; display: flex; flex-direction: column;
|
|
3244
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
3245
|
+
animation: wf-modal-slide-up 0.2s ease;
|
|
3246
|
+
}
|
|
3247
|
+
@keyframes wf-modal-slide-up { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
3248
|
+
.wf-output-modal-header {
|
|
3249
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3250
|
+
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
|
3251
|
+
}
|
|
3252
|
+
.wf-output-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
|
|
3253
|
+
.wf-output-modal-actions { display: flex; align-items: center; gap: 6px; }
|
|
3254
|
+
.wf-output-modal-btn {
|
|
3255
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3256
|
+
padding: 5px 10px; border-radius: 6px;
|
|
3257
|
+
border: 1px solid var(--border); background: var(--bg-card);
|
|
3258
|
+
color: var(--text); cursor: pointer; font-size: 12px;
|
|
3259
|
+
transition: background 0.15s, border-color 0.15s;
|
|
3260
|
+
}
|
|
3261
|
+
.wf-output-modal-btn:hover { background: var(--bg); border-color: var(--text-muted); }
|
|
3262
|
+
.wf-output-modal-btn.close { border: none; font-size: 20px; padding: 2px 8px; }
|
|
3263
|
+
.wf-output-modal-body {
|
|
3264
|
+
flex: 1; overflow: auto; padding: 16px 20px;
|
|
3265
|
+
margin: 0; font-family: 'SF Mono', 'Fira Code', monospace;
|
|
3266
|
+
font-size: 12px; line-height: 1.5; color: var(--text);
|
|
3267
|
+
white-space: pre-wrap; word-break: break-word;
|
|
3268
|
+
}
|
|
3269
|
+
/* Expand button for output sections */
|
|
3270
|
+
.wf-output-expand-btn {
|
|
3271
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3272
|
+
padding: 3px 8px; border-radius: 4px; margin-top: 6px;
|
|
3273
|
+
border: 1px solid var(--border); background: transparent;
|
|
3274
|
+
color: var(--text-muted); cursor: pointer; font-size: 10px;
|
|
3275
|
+
transition: color 0.15s, border-color 0.15s;
|
|
3276
|
+
}
|
|
3277
|
+
.wf-output-expand-btn:hover { color: var(--text); border-color: var(--text-muted); }
|
|
3278
|
+
/* Input modal (pre-execution) */
|
|
3279
|
+
.wf-input-modal-backdrop {
|
|
3280
|
+
position: fixed; inset: 0; z-index: 9999;
|
|
3281
|
+
background: rgba(0,0,0,0.55); display: flex;
|
|
3282
|
+
align-items: center; justify-content: center;
|
|
3283
|
+
}
|
|
3284
|
+
.wf-input-modal {
|
|
3285
|
+
background: var(--bg-surface, var(--bg)); border: 1px solid var(--border);
|
|
3286
|
+
border-radius: 10px; width: 440px; max-width: 90vw;
|
|
3287
|
+
max-height: 80vh; display: flex; flex-direction: column;
|
|
3288
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
3289
|
+
}
|
|
3290
|
+
.wf-input-modal-header {
|
|
3291
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3292
|
+
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
|
3293
|
+
}
|
|
3294
|
+
.wf-input-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
|
|
3295
|
+
.wf-input-modal-body {
|
|
3296
|
+
flex: 1; overflow-y: auto; padding: 16px 20px;
|
|
3297
|
+
}
|
|
3298
|
+
.wf-input-modal-field { margin-bottom: 14px; }
|
|
3299
|
+
.wf-input-modal-label {
|
|
3300
|
+
font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 4px;
|
|
3301
|
+
}
|
|
3302
|
+
.wf-input-modal-desc {
|
|
3303
|
+
font-size: 11px; color: var(--text-muted); margin-bottom: 4px; line-height: 1.3;
|
|
3304
|
+
}
|
|
3305
|
+
.wf-input-modal-input {
|
|
3306
|
+
width: 100%; box-sizing: border-box; padding: 6px 10px;
|
|
3307
|
+
border: 1px solid var(--border); border-radius: 6px;
|
|
3308
|
+
background: var(--bg); color: var(--text); font-size: 13px;
|
|
3309
|
+
font-family: inherit; outline: none;
|
|
3310
|
+
transition: border-color 0.15s;
|
|
3311
|
+
}
|
|
3312
|
+
.wf-input-modal-input:focus { border-color: var(--accent, #6c63ff); }
|
|
3313
|
+
.wf-input-modal-input.error { border-color: #e74c3c; }
|
|
3314
|
+
.wf-input-modal-error { font-size: 11px; color: #e74c3c; margin-top: 3px; display: none; }
|
|
3315
|
+
.wf-input-modal-footer {
|
|
3316
|
+
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
|
|
3317
|
+
padding: 12px 20px; border-top: 1px solid var(--border);
|
|
3318
|
+
}
|
|
3319
|
+
.wf-input-modal-cancel {
|
|
3320
|
+
padding: 6px 16px; border: 1px solid var(--border); border-radius: 6px;
|
|
3321
|
+
background: transparent; color: var(--text); font-size: 12px; cursor: pointer;
|
|
3322
|
+
transition: background 0.15s, border-color 0.15s;
|
|
3323
|
+
}
|
|
3324
|
+
.wf-input-modal-cancel:hover { background: var(--bg); border-color: var(--text-muted); }
|
|
3325
|
+
.wf-input-modal-run {
|
|
3326
|
+
padding: 6px 16px; border: none; border-radius: 6px;
|
|
3327
|
+
background: var(--accent, #6c63ff); color: #fff; font-size: 12px;
|
|
3328
|
+
font-weight: 600; cursor: pointer; transition: filter 0.15s;
|
|
3329
|
+
}
|
|
3330
|
+
.wf-input-modal-run:hover { filter: brightness(1.15); }
|
|
3331
|
+
#wf-canvas {
|
|
3332
|
+
width: 100%; height: 100%; display: block; position: relative; z-index: 1;
|
|
3333
|
+
}
|
|
3334
|
+
/* SVG node styles */
|
|
3335
|
+
.wf-node { cursor: pointer; }
|
|
3336
|
+
.wf-node rect {
|
|
3337
|
+
rx: 10; ry: 10; stroke-width: 2;
|
|
3338
|
+
transition: stroke 0.2s, filter 0.2s;
|
|
3339
|
+
}
|
|
3340
|
+
.wf-node:hover rect { filter: brightness(1.1); }
|
|
3341
|
+
.wf-node.selected rect { stroke-width: 3; stroke: var(--accent, #6c63ff); }
|
|
3342
|
+
.wf-node-label {
|
|
3343
|
+
font-size: 12px; font-weight: 600; fill: #fff;
|
|
3344
|
+
pointer-events: none; text-anchor: middle; dominant-baseline: central;
|
|
3345
|
+
}
|
|
3346
|
+
.wf-node-icon {
|
|
3347
|
+
pointer-events: none;
|
|
3348
|
+
color: #fff;
|
|
3349
|
+
}
|
|
3350
|
+
.wf-node-badge {
|
|
3351
|
+
font-size: 10px; fill: rgba(255,255,255,0.7);
|
|
3352
|
+
pointer-events: none; text-anchor: middle; dominant-baseline: central;
|
|
3353
|
+
}
|
|
3354
|
+
.wf-node-condition {
|
|
3355
|
+
font-size: 10px; fill: #FFD54F;
|
|
3356
|
+
pointer-events: none; text-anchor: end;
|
|
3357
|
+
}
|
|
3358
|
+
/* Execution states */
|
|
3359
|
+
.wf-node--pending rect { opacity: 0.5; }
|
|
3360
|
+
.wf-node--running rect {
|
|
3361
|
+
animation: wf-pulse 1.2s ease-in-out infinite;
|
|
3362
|
+
}
|
|
3363
|
+
@keyframes wf-pulse {
|
|
3364
|
+
0%, 100% { filter: drop-shadow(0 0 4px rgba(108,99,255,0.4)); }
|
|
3365
|
+
50% { filter: drop-shadow(0 0 12px rgba(108,99,255,0.8)); }
|
|
3366
|
+
}
|
|
3367
|
+
.wf-node--completed rect { opacity: 1; }
|
|
3368
|
+
.wf-node--skipped rect { opacity: 0.3; }
|
|
3369
|
+
.wf-node--error rect { stroke: #e74c3c !important; stroke-width: 3; }
|
|
3370
|
+
.wf-node-status {
|
|
3371
|
+
font-size: 12px; pointer-events: none; fill: #fff;
|
|
3372
|
+
text-anchor: middle; dominant-baseline: central;
|
|
3373
|
+
}
|
|
3374
|
+
.wf-node--error .wf-node-status { fill: #e74c3c; }
|
|
3375
|
+
.wf-node--skipped .wf-node-status { fill: rgba(255,255,255,0.5); }
|
|
3376
|
+
.wf-node-time {
|
|
3377
|
+
font-size: 9px; fill: rgba(255,255,255,0.6);
|
|
3378
|
+
pointer-events: none; text-anchor: middle;
|
|
3379
|
+
}
|
|
3380
|
+
/* Port styles */
|
|
3381
|
+
.wf-port { pointer-events: none; transition: fill 0.2s, stroke 0.2s; }
|
|
3382
|
+
.wf-port-in { fill: var(--bg-card); }
|
|
3383
|
+
.wf-port-out { fill: currentColor; stroke: rgba(255,255,255,0.8); }
|
|
3384
|
+
.wf-node:hover .wf-port-out { filter: brightness(1.3); }
|
|
3385
|
+
.wf-node--completed .wf-port-out { fill: #69F0AE; stroke: #fff; }
|
|
3386
|
+
.wf-node--error .wf-port-in { fill: #e74c3c; }
|
|
3387
|
+
/* Edge styles */
|
|
3388
|
+
.wf-edge {
|
|
3389
|
+
fill: none; stroke: #6b7b8d; stroke-width: 2;
|
|
3390
|
+
opacity: 0.5; transition: opacity 0.3s, stroke 0.3s;
|
|
3391
|
+
stroke-linecap: round;
|
|
3392
|
+
}
|
|
3393
|
+
.wf-edge--backward {
|
|
3394
|
+
stroke-dasharray: 6 4; opacity: 0.35;
|
|
3395
|
+
}
|
|
3396
|
+
.wf-edge--active {
|
|
3397
|
+
stroke: var(--accent, #6c63ff); opacity: 0.85;
|
|
3398
|
+
stroke-dasharray: 8 4;
|
|
3399
|
+
animation: wf-flow 0.6s linear infinite;
|
|
3400
|
+
}
|
|
3401
|
+
@keyframes wf-flow { to { stroke-dashoffset: -12; } }
|
|
3402
|
+
.wf-edge--complete { stroke: var(--accent, #6c63ff); opacity: 0.6; stroke-dasharray: none; }
|
|
3403
|
+
/* Inspector */
|
|
3404
|
+
.wf-inspector {
|
|
3405
|
+
flex-shrink: 0; position: relative;
|
|
3406
|
+
display: flex; flex-direction: row;
|
|
3407
|
+
background: var(--bg);
|
|
3408
|
+
transition: width 0.25s ease;
|
|
3409
|
+
width: 300px;
|
|
3410
|
+
overflow: hidden;
|
|
3411
|
+
align-self: stretch;
|
|
3412
|
+
}
|
|
3413
|
+
.wf-inspector.collapsed {
|
|
3414
|
+
width: 28px;
|
|
3415
|
+
}
|
|
3416
|
+
.wf-inspector-toggle {
|
|
3417
|
+
width: 28px; flex-shrink: 0; align-self: stretch;
|
|
3418
|
+
border: none; border-left: 1px solid var(--border);
|
|
3419
|
+
background: var(--bg); color: var(--text-muted);
|
|
3420
|
+
cursor: pointer; font-size: 14px; padding: 0;
|
|
3421
|
+
display: flex; align-items: center; justify-content: center;
|
|
3422
|
+
transition: color 0.15s, background 0.15s;
|
|
3423
|
+
}
|
|
3424
|
+
.wf-inspector-toggle:hover {
|
|
3425
|
+
color: var(--text); background: var(--bg-card);
|
|
3426
|
+
}
|
|
3427
|
+
.wf-inspector.collapsed .wf-inspector-toggle {
|
|
3428
|
+
border-left: 1px solid var(--border);
|
|
3429
|
+
}
|
|
3430
|
+
.wf-inspector.collapsed .wf-inspector-content {
|
|
3431
|
+
display: none;
|
|
3432
|
+
}
|
|
3433
|
+
.wf-inspector-content {
|
|
3434
|
+
flex: 1; display: flex; flex-direction: column;
|
|
3435
|
+
border-left: 1px solid var(--border);
|
|
3436
|
+
overflow-y: auto; min-width: 0; height: 100%;
|
|
3437
|
+
}
|
|
3438
|
+
.wf-inspector-header {
|
|
3439
|
+
padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
|
|
3440
|
+
color: var(--text); border-bottom: 1px solid var(--border);
|
|
3441
|
+
}
|
|
3442
|
+
.wf-inspector-body {
|
|
3443
|
+
padding: 16px; flex: 1; overflow-y: auto;
|
|
3444
|
+
}
|
|
3445
|
+
.wf-inspector-empty {
|
|
3446
|
+
color: var(--text-muted); font-size: 13px; text-align: center;
|
|
3447
|
+
padding: 40px 16px;
|
|
3448
|
+
}
|
|
3449
|
+
.wf-inspector-section {
|
|
3450
|
+
margin-bottom: 16px;
|
|
3451
|
+
}
|
|
3452
|
+
.wf-inspector-section-title {
|
|
3453
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
3454
|
+
letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 6px;
|
|
3455
|
+
}
|
|
3456
|
+
.wf-inspector-field {
|
|
3457
|
+
font-size: 12px; color: var(--text); margin-bottom: 4px;
|
|
3458
|
+
display: flex; gap: 6px;
|
|
3459
|
+
}
|
|
3460
|
+
.wf-inspector-field-label {
|
|
3461
|
+
font-weight: 600; min-width: 60px; color: var(--text-muted);
|
|
3462
|
+
}
|
|
3463
|
+
.wf-inspector-field-value {
|
|
3464
|
+
word-break: break-all;
|
|
3465
|
+
}
|
|
3466
|
+
.wf-inspector-code {
|
|
3467
|
+
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
|
|
3468
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3469
|
+
border-radius: 6px; padding: 8px; overflow-x: auto;
|
|
3470
|
+
white-space: pre-wrap; color: var(--text); margin-top: 4px;
|
|
3471
|
+
}
|
|
3472
|
+
.wf-tool-badge {
|
|
3473
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3474
|
+
padding: 2px 8px; border-radius: 10px; font-size: 11px;
|
|
3475
|
+
font-weight: 600; color: #fff;
|
|
3476
|
+
}
|
|
3477
|
+
.wf-inspector-input {
|
|
3478
|
+
width: 100%; padding: 6px 10px; border-radius: 6px;
|
|
3479
|
+
border: 1px solid var(--border); background: var(--bg-card);
|
|
3480
|
+
color: var(--text); font-size: 12px; margin-top: 4px;
|
|
3481
|
+
}
|
|
3482
|
+
.wf-inspector-input:focus {
|
|
3483
|
+
outline: none; border-color: var(--accent, #6c63ff);
|
|
3484
|
+
}
|
|
3485
|
+
.wf-inspector-btn {
|
|
3486
|
+
width: 100%; padding: 8px 16px; border-radius: 8px;
|
|
3487
|
+
border: none; background: var(--accent, #6c63ff);
|
|
3488
|
+
color: #fff; font-weight: 600; font-size: 13px;
|
|
3489
|
+
cursor: pointer; margin-top: 12px;
|
|
3490
|
+
}
|
|
3491
|
+
.wf-inspector-btn:hover { opacity: 0.9; }
|
|
3492
|
+
.wf-inspector-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3493
|
+
.wf-inspector-result {
|
|
3494
|
+
margin-top: 8px; padding: 8px; border-radius: 6px;
|
|
3495
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3496
|
+
font-size: 11px; max-height: 200px; overflow-y: auto;
|
|
3497
|
+
}
|
|
3498
|
+
.wf-inspector-result.success { border-color: #69F0AE; }
|
|
3499
|
+
.wf-inspector-result.error { border-color: #e74c3c; }
|
|
3500
|
+
/* Responsive */
|
|
3501
|
+
@media (max-width: 900px) {
|
|
3502
|
+
.wf-library { display: none; }
|
|
3503
|
+
.wf-inspector:not(.collapsed) { width: 240px; }
|
|
3504
|
+
}
|
|
3505
|
+
@media (max-width: 600px) {
|
|
3506
|
+
.wf-inspector { display: none; }
|
|
3507
|
+
}
|
|
3508
|
+
/* Workflow visualizer light mode */
|
|
3509
|
+
[data-theme="light"] .wf-node-label { fill: #001E2B; }
|
|
3510
|
+
[data-theme="light"] .wf-node-icon { color: #001E2B; }
|
|
3511
|
+
[data-theme="light"] .wf-node-badge { fill: rgba(0,30,43,0.55); }
|
|
3512
|
+
[data-theme="light"] .wf-node-time { fill: rgba(0,30,43,0.5); }
|
|
3513
|
+
[data-theme="light"] .wf-node-condition { fill: #944F01; }
|
|
3514
|
+
[data-theme="light"] .wf-node-status { fill: #001E2B; }
|
|
3515
|
+
[data-theme="light"] .wf-node--skipped .wf-node-status { fill: rgba(0,30,43,0.4); }
|
|
3516
|
+
[data-theme="light"] .wf-node--error .wf-node-status { fill: #DB3030; }
|
|
3517
|
+
[data-theme="light"] .wf-node rect { stroke-opacity: 0.7; }
|
|
3518
|
+
[data-theme="light"] .wf-node.selected rect { stroke: var(--accent); }
|
|
3519
|
+
[data-theme="light"] .wf-edge { stroke: #889397; opacity: 0.45; }
|
|
3520
|
+
[data-theme="light"] .wf-edge--backward { opacity: 0.3; }
|
|
3521
|
+
[data-theme="light"] .wf-edge--active { stroke: var(--accent); opacity: 0.7; }
|
|
3522
|
+
[data-theme="light"] .wf-edge--complete { stroke: var(--accent); opacity: 0.5; }
|
|
3523
|
+
[data-theme="light"] .wf-port-in { fill: var(--bg); }
|
|
3524
|
+
[data-theme="light"] .wf-port-out { stroke: rgba(0,30,43,0.3); }
|
|
3525
|
+
[data-theme="light"] .wf-node--completed .wf-port-out { fill: #009E80; stroke: var(--bg); }
|
|
3526
|
+
[data-theme="light"] .wf-canvas-area { background: var(--bg-surface); }
|
|
3527
|
+
[data-theme="light"] .wf-inspector-result.success { border-color: #009E80; }
|
|
3528
|
+
[data-theme="light"] .wf-inspector-result.error { border-color: #DB3030; }
|
|
3529
|
+
[data-theme="light"] .wf-exec-status { box-shadow: 0 2px 8px rgba(0,30,43,0.08); }
|
|
3530
|
+
[data-theme="light"] .wf-run-btn { background: var(--accent); }
|
|
3531
|
+
[data-theme="light"] .wf-output-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
|
|
3532
|
+
[data-theme="light"] .wf-input-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
|
|
3533
|
+
[data-theme="light"] .wf-input-modal-backdrop { background: rgba(0,0,0,0.3); }
|
|
3534
|
+
@keyframes wf-pulse-light {
|
|
3535
|
+
0%, 100% { filter: drop-shadow(0 0 4px rgba(0,158,128,0.3)); }
|
|
3536
|
+
50% { filter: drop-shadow(0 0 12px rgba(0,158,128,0.6)); }
|
|
3537
|
+
}
|
|
3538
|
+
[data-theme="light"] .wf-node--running rect { animation-name: wf-pulse-light; }
|
|
3539
|
+
/* Empty canvas state */
|
|
3540
|
+
.wf-canvas-empty {
|
|
3541
|
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
3542
|
+
text-align: center; color: var(--text-muted); pointer-events: none;
|
|
3543
|
+
}
|
|
3544
|
+
.wf-canvas-empty-icon { margin-bottom: 16px; opacity: 0.15; }
|
|
3545
|
+
.wf-canvas-empty-icon img { width: 200px; height: 200px; filter: invert(1); }
|
|
3546
|
+
[data-theme="light"] .wf-canvas-empty-icon img { filter: none; opacity: 0.1; }
|
|
3547
|
+
.wf-canvas-empty-text { font-size: 14px; }
|
|
3548
|
+
/* Persistent watermark behind workflows */
|
|
3549
|
+
.wf-canvas-watermark {
|
|
3550
|
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
3551
|
+
pointer-events: none; z-index: 0; opacity: 0.04;
|
|
3552
|
+
}
|
|
3553
|
+
.wf-canvas-watermark img { width: 300px; height: 300px; filter: invert(1); }
|
|
3554
|
+
[data-theme="light"] .wf-canvas-watermark img { filter: none; }
|
|
2753
3555
|
</style>
|
|
2754
3556
|
</head>
|
|
2755
3557
|
<body>
|
|
2756
3558
|
|
|
2757
|
-
<!--
|
|
3559
|
+
<!-- Lucide Icons (lucide.dev) — stroke-based, 16×16 -->
|
|
2758
3560
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none;">
|
|
2759
|
-
|
|
2760
|
-
|
|
3561
|
+
<!-- Zap (Embed) -->
|
|
3562
|
+
<symbol id="lg-lightning" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3563
|
+
<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
2761
3564
|
</symbol>
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
<path d="
|
|
2765
|
-
<path d="M11 7.42721V2.57279C11 2.04351 11.6076 1.79846 11.9279 2.19856L13.871 4.62577C14.043 4.84058 14.043 5.15942 13.871 5.37423L11.9279 7.80144C11.6076 8.20154 11 7.95649 11 7.42721Z" fill="currentColor"/>
|
|
2766
|
-
<path d="M3 4.5C3 4.22386 3.22386 4 3.5 4H11V6H3.5C3.22386 6 3 5.77614 3 5.5V4.5Z" fill="currentColor"/>
|
|
3565
|
+
<!-- Arrow Left Right (Compare) -->
|
|
3566
|
+
<symbol id="lg-arrows" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3567
|
+
<path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>
|
|
2767
3568
|
</symbol>
|
|
2768
|
-
|
|
2769
|
-
|
|
3569
|
+
<!-- Search -->
|
|
3570
|
+
<symbol id="lg-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3571
|
+
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
|
|
2770
3572
|
</symbol>
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
<path d="
|
|
3573
|
+
<!-- Gauge (Benchmark) -->
|
|
3574
|
+
<symbol id="lg-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3575
|
+
<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>
|
|
2774
3576
|
</symbol>
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
<path d="
|
|
2778
|
-
<path d="M10 14V13H6V14C6 14.5523 6.44772 15 7 15H9C9.55228 15 10 14.5523 10 14Z" fill="currentColor"/>
|
|
3577
|
+
<!-- Lightbulb (Explore) -->
|
|
3578
|
+
<symbol id="lg-bulb" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3579
|
+
<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>
|
|
2779
3580
|
</symbol>
|
|
2780
|
-
|
|
2781
|
-
|
|
3581
|
+
<!-- Info (About) -->
|
|
3582
|
+
<symbol id="lg-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3583
|
+
<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>
|
|
2782
3584
|
</symbol>
|
|
2783
|
-
|
|
2784
|
-
|
|
3585
|
+
<!-- Image (Multimodal) -->
|
|
3586
|
+
<symbol id="lg-image" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3587
|
+
<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
|
|
2785
3588
|
</symbol>
|
|
2786
|
-
|
|
2787
|
-
|
|
3589
|
+
<!-- Settings (Config) -->
|
|
3590
|
+
<symbol id="lg-config" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3591
|
+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>
|
|
2788
3592
|
</symbol>
|
|
2789
|
-
|
|
2790
|
-
|
|
3593
|
+
<!-- Code (Generate) -->
|
|
3594
|
+
<symbol id="lg-code" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3595
|
+
<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
|
|
2791
3596
|
</symbol>
|
|
2792
|
-
|
|
2793
|
-
|
|
3597
|
+
<!-- Palette (Theme) -->
|
|
3598
|
+
<symbol id="lg-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3599
|
+
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/>
|
|
2794
3600
|
</symbol>
|
|
2795
|
-
|
|
2796
|
-
|
|
3601
|
+
<!-- Box (Cube) -->
|
|
3602
|
+
<symbol id="lg-cube" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3603
|
+
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>
|
|
2797
3604
|
</symbol>
|
|
2798
|
-
|
|
2799
|
-
|
|
3605
|
+
<!-- Message Square (Chat) -->
|
|
3606
|
+
<symbol id="lg-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3607
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
2800
3608
|
</symbol>
|
|
2801
|
-
|
|
2802
|
-
|
|
3609
|
+
<!-- Shield -->
|
|
3610
|
+
<symbol id="lg-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3611
|
+
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6z"/>
|
|
3612
|
+
</symbol>
|
|
3613
|
+
<!-- Activity (Pulse) -->
|
|
3614
|
+
<symbol id="lg-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
|
3615
|
+
<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2"/>
|
|
2803
3616
|
</symbol>
|
|
2804
3617
|
</svg>
|
|
2805
3618
|
|
|
@@ -2810,7 +3623,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2810
3623
|
<div class="sidebar-drag-region">
|
|
2811
3624
|
<img class="sidebar-logo" id="sidebarLogo" src="/icons/dark/64.png" alt="Vai">
|
|
2812
3625
|
<span class="sidebar-title">Vai</span>
|
|
2813
|
-
<button class="sidebar-settings-btn" data-tab="settings" title="Settings"><svg width="16" height="16" viewBox="0 0
|
|
3626
|
+
<button class="sidebar-settings-btn" data-tab="settings" title="Settings"><svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg></button>
|
|
2814
3627
|
</div>
|
|
2815
3628
|
<nav class="sidebar-nav">
|
|
2816
3629
|
<div class="sidebar-nav-group" role="tablist" aria-label="Tools">
|
|
@@ -2820,7 +3633,8 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2820
3633
|
<button class="tab-btn" data-tab="search" role="tab" aria-selected="false" aria-controls="tab-search" id="tab-btn-search"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
|
|
2821
3634
|
<button class="tab-btn" data-tab="multimodal" role="tab" aria-selected="false" aria-controls="tab-multimodal" id="tab-btn-multimodal"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-image"/></svg></span><span>Multimodal</span></button>
|
|
2822
3635
|
<button class="tab-btn" data-tab="generate" role="tab" aria-selected="false" aria-controls="tab-generate" id="tab-btn-generate"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-code"/></svg></span><span>Generate</span></button>
|
|
2823
|
-
<button class="tab-btn" data-tab="chat" role="tab" aria-selected="false" aria-controls="tab-chat" id="tab-btn-chat"><span class="tab-btn-icon" aria-hidden="true"><svg
|
|
3636
|
+
<button class="tab-btn" data-tab="chat" role="tab" aria-selected="false" aria-controls="tab-chat" id="tab-btn-chat"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-chat"/></svg></span><span>Chat</span></button>
|
|
3637
|
+
<button class="tab-btn" data-tab="workflows" role="tab" aria-selected="false" aria-controls="tab-workflows" id="tab-btn-workflows"><span class="tab-btn-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="3"/><circle cx="19" cy="6" r="3"/><circle cx="19" cy="18" r="3"/><line x1="7.7" y1="10.7" x2="16.3" y2="7.3"/><line x1="7.7" y1="13.3" x2="16.3" y2="16.7"/></svg></span><span>Workflows</span></button>
|
|
2824
3638
|
</div>
|
|
2825
3639
|
<div class="sidebar-nav-divider"></div>
|
|
2826
3640
|
<div class="sidebar-nav-group" role="tablist" aria-label="Learn">
|
|
@@ -2840,10 +3654,16 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2840
3654
|
</div>
|
|
2841
3655
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
2842
3656
|
<div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);"></div>
|
|
2843
|
-
<
|
|
2844
|
-
<
|
|
2845
|
-
|
|
2846
|
-
|
|
3657
|
+
<div style="display:flex;align-items:center;gap:6px;">
|
|
3658
|
+
<a class="sidebar-docs-link" href="https://docs.vaicli.com" target="_blank" rel="noopener" title="Documentation (F1)">
|
|
3659
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/><path d="M6 8h4M6 10.5h4"/></svg>
|
|
3660
|
+
<span>Docs</span>
|
|
3661
|
+
</a>
|
|
3662
|
+
<button class="sidebar-bug-link" id="bugButton" title="Report a Bug">
|
|
3663
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="9" r="4.5"/><path d="M5.5 5.5L3 3M10.5 5.5L13 3M3 9H1M15 9h-2M5.5 12.5L4 15M10.5 12.5L12 15"/></svg>
|
|
3664
|
+
<span class="sidebar-bug-label">Bug</span>
|
|
3665
|
+
</button>
|
|
3666
|
+
</div>
|
|
2847
3667
|
</div>
|
|
2848
3668
|
</div>
|
|
2849
3669
|
</aside>
|
|
@@ -2873,6 +3693,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2873
3693
|
<h2 class="page-header-title">Embed</h2>
|
|
2874
3694
|
<p class="page-header-subtitle">Generate vector embeddings for text</p>
|
|
2875
3695
|
<p class="page-header-hint">Paste or type text below, choose a model, and hit Embed to see the raw vectors and token usage.</p>
|
|
3696
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Embed documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
2876
3697
|
</div>
|
|
2877
3698
|
<div class="card">
|
|
2878
3699
|
<div class="card-title">Input Text</div>
|
|
@@ -2912,7 +3733,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2912
3733
|
<option value="ubinary">ubinary (32× smaller)</option>
|
|
2913
3734
|
</select>
|
|
2914
3735
|
</div>
|
|
2915
|
-
<button class="btn" id="embedBtn" onclick="doEmbed()"
|
|
3736
|
+
<button class="btn" id="embedBtn" onclick="doEmbed()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Embed</button>
|
|
2916
3737
|
</div>
|
|
2917
3738
|
|
|
2918
3739
|
<div class="error-msg" id="embedError"></div>
|
|
@@ -2923,7 +3744,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2923
3744
|
<div id="embedStats"></div>
|
|
2924
3745
|
<div class="vector-preview" id="embedVector"></div>
|
|
2925
3746
|
<div style="margin-top:8px;">
|
|
2926
|
-
<button class="btn btn-secondary btn-small" onclick="copyVector()"
|
|
3747
|
+
<button class="btn btn-secondary btn-small" onclick="copyVector()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z"/></svg>Copy Full Vector</button>
|
|
2927
3748
|
</div>
|
|
2928
3749
|
<div class="card-title" style="margin-top:16px;">Vector Heatmap</div>
|
|
2929
3750
|
<div class="heatmap" id="embedHeatmap"></div>
|
|
@@ -2936,7 +3757,8 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2936
3757
|
<div class="page-header">
|
|
2937
3758
|
<h2 class="page-header-title">Compare</h2>
|
|
2938
3759
|
<p class="page-header-subtitle">Visualize similarity between text pairs</p>
|
|
2939
|
-
<p class="page-header-hint">Enter two texts and compare their embeddings
|
|
3760
|
+
<p class="page-header-hint">Enter two texts and compare their embeddings: see cosine similarity, a heatmap of vector dimensions, and a visual diff.</p>
|
|
3761
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/similarity" target="_blank" rel="noopener" title="Similarity documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
2940
3762
|
</div>
|
|
2941
3763
|
<div class="compare-grid">
|
|
2942
3764
|
<div class="card">
|
|
@@ -2964,7 +3786,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2964
3786
|
<option value="2048">2048</option>
|
|
2965
3787
|
</select>
|
|
2966
3788
|
</div>
|
|
2967
|
-
<button class="btn" id="compareBtn" onclick="doCompare()"
|
|
3789
|
+
<button class="btn" id="compareBtn" onclick="doCompare()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>Compare</button>
|
|
2968
3790
|
</div>
|
|
2969
3791
|
|
|
2970
3792
|
<div class="error-msg" id="compareError"></div>
|
|
@@ -2990,7 +3812,8 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2990
3812
|
<div class="page-header">
|
|
2991
3813
|
<h2 class="page-header-title">Rerank</h2>
|
|
2992
3814
|
<p class="page-header-subtitle">Re-order documents by relevance to a query</p>
|
|
2993
|
-
<p class="page-header-hint">Enter a search query and a set of documents
|
|
3815
|
+
<p class="page-header-hint">Enter a search query and a set of documents: the reranker scores and sorts them by semantic relevance.</p>
|
|
3816
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/rerank" target="_blank" rel="noopener" title="Rerank documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
2994
3817
|
</div>
|
|
2995
3818
|
<div class="card">
|
|
2996
3819
|
<div class="card-title">Query</div>
|
|
@@ -3025,8 +3848,8 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3025
3848
|
<option value="10">10</option>
|
|
3026
3849
|
</select>
|
|
3027
3850
|
</div>
|
|
3028
|
-
<button class="btn" id="searchBtn" onclick="doSearch(false)"
|
|
3029
|
-
<button class="btn btn-secondary" id="searchRerankBtn" onclick="doSearch(true)"
|
|
3851
|
+
<button class="btn" id="searchBtn" onclick="doSearch(false)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search</button>
|
|
3852
|
+
<button class="btn btn-secondary" id="searchRerankBtn" onclick="doSearch(true)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M3 6h18M7 12h10M10 18h4"/></svg>Rerank</button>
|
|
3030
3853
|
</div>
|
|
3031
3854
|
|
|
3032
3855
|
<div class="error-msg" id="searchError"></div>
|
|
@@ -3041,7 +3864,8 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3041
3864
|
<div class="page-header">
|
|
3042
3865
|
<h2 class="page-header-title">Multimodal</h2>
|
|
3043
3866
|
<p class="page-header-subtitle">Compare images and text in the same vector space</p>
|
|
3044
|
-
<p class="page-header-hint">Voyage AI's multimodal models embed images and text into a unified vector space
|
|
3867
|
+
<p class="page-header-hint">Voyage AI's multimodal models embed images and text into a unified vector space, so you can compare them directly with cosine similarity.</p>
|
|
3868
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/embeddings/embed" target="_blank" rel="noopener" title="Multimodal embedding documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
3045
3869
|
</div>
|
|
3046
3870
|
|
|
3047
3871
|
<!-- Section A: Image ↔ Text Similarity -->
|
|
@@ -3051,7 +3875,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3051
3875
|
<div class="mm-drop-zone" id="mmDropZone">
|
|
3052
3876
|
<div class="mm-drop-icon">🖼️</div>
|
|
3053
3877
|
<div class="mm-drop-text">Drop an image here or click to browse</div>
|
|
3054
|
-
<div class="mm-drop-hint">PNG, JPEG, WebP, GIF
|
|
3878
|
+
<div class="mm-drop-hint">PNG, JPEG, WebP, GIF, max 20 MB. Paste from clipboard (⌘V)</div>
|
|
3055
3879
|
</div>
|
|
3056
3880
|
<input type="file" id="mmFileInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
|
|
3057
3881
|
<div class="mm-preview" id="mmPreview">
|
|
@@ -3084,7 +3908,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3084
3908
|
<option value="2048">2048</option>
|
|
3085
3909
|
</select>
|
|
3086
3910
|
</div>
|
|
3087
|
-
<button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()"
|
|
3911
|
+
<button class="btn" id="mmCompareBtn" onclick="doMultimodalCompare()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4"/></svg>Compare</button>
|
|
3088
3912
|
</div>
|
|
3089
3913
|
|
|
3090
3914
|
<div class="error-msg" id="mmError"></div>
|
|
@@ -3122,20 +3946,20 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3122
3946
|
<div class="card" style="margin-top:12px;">
|
|
3123
3947
|
<div class="card-title">Search Query</div>
|
|
3124
3948
|
<div class="mm-search-mode" id="mmSearchMode">
|
|
3125
|
-
<button class="active" data-mode="text" onclick="setMmSearchMode('text')"
|
|
3126
|
-
<button data-mode="image" onclick="setMmSearchMode('image')"
|
|
3949
|
+
<button class="active" data-mode="text" onclick="setMmSearchMode('text')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M17 6.1H3M21 12.1H3M15.1 18H3"/></svg>Text Query</button>
|
|
3950
|
+
<button data-mode="image" onclick="setMmSearchMode('image')"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>Image Query</button>
|
|
3127
3951
|
</div>
|
|
3128
3952
|
<div id="mmSearchTextWrap">
|
|
3129
3953
|
<div class="mm-search-row">
|
|
3130
3954
|
<input type="text" id="mmSearchQuery" placeholder="Enter a search query...">
|
|
3131
|
-
<button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()"
|
|
3955
|
+
<button class="btn" id="mmSearchBtn" onclick="doMultimodalSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search Corpus</button>
|
|
3132
3956
|
</div>
|
|
3133
3957
|
</div>
|
|
3134
3958
|
<div id="mmSearchImageWrap" style="display:none;">
|
|
3135
3959
|
<p style="font-size:13px;color:var(--text-dim);margin-bottom:8px;">Click an image in the corpus above to use it as the search query, then:</p>
|
|
3136
3960
|
<div style="display:flex;gap:8px;align-items:center;">
|
|
3137
3961
|
<span id="mmSearchImageLabel" style="font-size:13px;color:var(--text-muted);">No image selected</span>
|
|
3138
|
-
<button class="btn" id="mmSearchImgBtn" onclick="doMultimodalSearch()"
|
|
3962
|
+
<button class="btn" id="mmSearchImgBtn" onclick="doMultimodalSearch()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/></svg>Search Corpus</button>
|
|
3139
3963
|
</div>
|
|
3140
3964
|
</div>
|
|
3141
3965
|
</div>
|
|
@@ -3157,16 +3981,17 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3157
3981
|
<h2 class="page-header-title">Benchmark</h2>
|
|
3158
3982
|
<p class="page-header-subtitle">Compare model speed, cost, and quality</p>
|
|
3159
3983
|
<p class="page-header-hint">Run latency tests, compare ranking accuracy, analyze quantization trade-offs, and estimate costs across models.</p>
|
|
3984
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/evaluation/benchmark" target="_blank" rel="noopener" title="Benchmark documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
3160
3985
|
</div>
|
|
3161
3986
|
|
|
3162
3987
|
<!-- Sub-panel switcher -->
|
|
3163
3988
|
<div class="bench-panels">
|
|
3164
|
-
<button class="bench-panel-btn active" data-bench="latency"
|
|
3165
|
-
<button class="bench-panel-btn" data-bench="ranking"
|
|
3166
|
-
<button class="bench-panel-btn" data-bench="competitors"
|
|
3167
|
-
<button class="bench-panel-btn" data-bench="quantization"
|
|
3168
|
-
<button class="bench-panel-btn" data-bench="cost"
|
|
3169
|
-
<button class="bench-panel-btn" data-bench="history"
|
|
3989
|
+
<button class="bench-panel-btn active" data-bench="latency"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Latency</button>
|
|
3990
|
+
<button class="bench-panel-btn" data-bench="ranking"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>Ranking</button>
|
|
3991
|
+
<button class="bench-panel-btn" data-bench="competitors"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22"/></svg>vs Competitors</button>
|
|
3992
|
+
<button class="bench-panel-btn" data-bench="quantization"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10"/></svg>Quantization</button>
|
|
3993
|
+
<button class="bench-panel-btn" data-bench="cost"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Cost</button>
|
|
3994
|
+
<button class="bench-panel-btn" data-bench="history"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>History</button>
|
|
3170
3995
|
</div>
|
|
3171
3996
|
|
|
3172
3997
|
<!-- ── Latency Panel ── -->
|
|
@@ -3188,7 +4013,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3188
4013
|
<option value="10">10</option>
|
|
3189
4014
|
</select>
|
|
3190
4015
|
</div>
|
|
3191
|
-
<button class="btn" id="benchLatencyBtn" onclick="doBenchLatency()"
|
|
4016
|
+
<button class="btn" id="benchLatencyBtn" onclick="doBenchLatency()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>Run Benchmark</button>
|
|
3192
4017
|
</div>
|
|
3193
4018
|
</div>
|
|
3194
4019
|
|
|
@@ -3242,7 +4067,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3242
4067
|
<option value="8">8</option>
|
|
3243
4068
|
</select>
|
|
3244
4069
|
</div>
|
|
3245
|
-
<button class="btn" id="benchRankBtn" onclick="doBenchRanking()"
|
|
4070
|
+
<button class="btn" id="benchRankBtn" onclick="doBenchRanking()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>Compare Rankings</button>
|
|
3246
4071
|
</div>
|
|
3247
4072
|
</div>
|
|
3248
4073
|
|
|
@@ -3318,7 +4143,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3318
4143
|
|
|
3319
4144
|
<!-- Cost Comparison -->
|
|
3320
4145
|
<div class="card" style="margin-top:16px;">
|
|
3321
|
-
<div class="card-title"
|
|
4146
|
+
<div class="card-title"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:3px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>Cost per Million Tokens</div>
|
|
3322
4147
|
<p style="color:var(--text-dim);font-size:13px;margin-bottom:16px;">
|
|
3323
4148
|
Voyage AI offers significant cost savings, especially with asymmetric retrieval strategies.
|
|
3324
4149
|
</p>
|
|
@@ -3479,7 +4304,7 @@ Approximate nearest neighbor algorithms like HNSW enable fast similarity search
|
|
|
3479
4304
|
Reranking models rescore initial search results to improve relevance ordering.</textarea>
|
|
3480
4305
|
</div>
|
|
3481
4306
|
<div style="margin-top:12px;">
|
|
3482
|
-
<button class="btn" id="quantBtn" onclick="doBenchQuantization()"
|
|
4307
|
+
<button class="btn" id="quantBtn" onclick="doBenchQuantization()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10"/></svg>Run Quantization Benchmark</button>
|
|
3483
4308
|
</div>
|
|
3484
4309
|
</div>
|
|
3485
4310
|
|
|
@@ -3507,7 +4332,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3507
4332
|
<!-- ── Cost Panel ── -->
|
|
3508
4333
|
<div class="bench-view" id="bench-cost">
|
|
3509
4334
|
<div class="card">
|
|
3510
|
-
<div class="card-title"
|
|
4335
|
+
<div class="card-title"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>RAG Cost Calculator <button class="cost-help-btn" id="costHelpBtn" title="How the math works">?</button></div>
|
|
3511
4336
|
|
|
3512
4337
|
<!-- Mode toggle -->
|
|
3513
4338
|
<div style="margin-bottom: 20px;">
|
|
@@ -3628,7 +4453,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3628
4453
|
<div class="history-empty">No benchmarks recorded yet. Run a latency benchmark to start tracking.</div>
|
|
3629
4454
|
</div>
|
|
3630
4455
|
<div style="margin-top:12px;text-align:right;">
|
|
3631
|
-
<button class="btn btn-secondary btn-small" onclick="clearHistory()"
|
|
4456
|
+
<button class="btn btn-secondary btn-small" onclick="clearHistory()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>Clear History</button>
|
|
3632
4457
|
</div>
|
|
3633
4458
|
</div>
|
|
3634
4459
|
</div>
|
|
@@ -3645,7 +4470,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3645
4470
|
<span id="chatStatusDb">No database</span>
|
|
3646
4471
|
</div>
|
|
3647
4472
|
<button class="chat-config-toggle" id="chatOpenSettings" onclick="openChatSettings()" title="Configure in Settings">
|
|
3648
|
-
<svg width="14" height="14" viewBox="0 0
|
|
4473
|
+
<svg width="14" height="14" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
|
|
3649
4474
|
Configure
|
|
3650
4475
|
</button>
|
|
3651
4476
|
</div>
|
|
@@ -3664,6 +4489,103 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3664
4489
|
</div>
|
|
3665
4490
|
</div>
|
|
3666
4491
|
|
|
4492
|
+
<!-- ========== WORKFLOWS TAB ========== -->
|
|
4493
|
+
<div class="tab-panel" id="tab-workflows" role="tabpanel" aria-labelledby="tab-btn-workflows" tabindex="0">
|
|
4494
|
+
<div class="wf-container">
|
|
4495
|
+
<div class="wf-library">
|
|
4496
|
+
<div class="wf-library-header">
|
|
4497
|
+
<div class="wf-library-tabs">
|
|
4498
|
+
<button class="wf-lib-tab active" data-lib-tab="library" onclick="wfSwitchLibTab('library')">Library</button>
|
|
4499
|
+
<button class="wf-lib-tab" data-lib-tab="palette" onclick="wfSwitchLibTab('palette')">Palette</button>
|
|
4500
|
+
</div>
|
|
4501
|
+
</div>
|
|
4502
|
+
<div class="wf-library-list" id="wfLibraryList">
|
|
4503
|
+
<div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
|
|
4504
|
+
</div>
|
|
4505
|
+
<div class="wf-palette-list" id="wfPaletteList" style="display:none; flex:1; overflow-y:auto; padding:8px;"></div>
|
|
4506
|
+
<div class="wf-library-footer">
|
|
4507
|
+
<button class="wf-load-file-btn" onclick="wfLoadFromFile()" title="Load workflow JSON from file">
|
|
4508
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 14h10M8 2v9M5 8l3 3 3-3"/></svg>
|
|
4509
|
+
Load File
|
|
4510
|
+
</button>
|
|
4511
|
+
<input type="file" id="wfFileInput" accept=".json" style="display:none;" onchange="wfHandleFileLoad(event)">
|
|
4512
|
+
</div>
|
|
4513
|
+
</div>
|
|
4514
|
+
<div class="wf-canvas-area">
|
|
4515
|
+
<div class="wf-canvas-toolbar" id="wfToolbar">
|
|
4516
|
+
<button class="wf-new-btn" onclick="wfNewWorkflow()" title="Create new workflow">+ New</button>
|
|
4517
|
+
<button class="wf-edit-btn" id="wfEditBtn" onclick="wfEditWorkflow()" disabled title="Edit current workflow">✎ Edit</button>
|
|
4518
|
+
<span class="wf-toolbar-sep"></span>
|
|
4519
|
+
<button onclick="wfZoom(1)" title="Zoom in">+</button>
|
|
4520
|
+
<button onclick="wfZoom(-1)" title="Zoom out">−</button>
|
|
4521
|
+
<button onclick="wfFitToView()" title="Fit to view">⊞</button>
|
|
4522
|
+
<button onclick="wfResetExecution()" title="Reset">↻</button>
|
|
4523
|
+
<span class="wf-toolbar-sep"></span>
|
|
4524
|
+
<button class="wf-plan-btn" onclick="wfDryRun()" id="wfDryRunBtn" disabled title="Dry run: show execution plan">⚙ Plan</button>
|
|
4525
|
+
<button class="wf-run-btn" id="wfRunBtn" onclick="wfExecute()" disabled title="Run workflow">▶ Run</button>
|
|
4526
|
+
<button class="wf-stop-btn" id="wfStopBtn" onclick="wfStopExecution()" style="display:none;" title="Stop workflow">■ Stop</button>
|
|
4527
|
+
<span class="wf-toolbar-sep"></span>
|
|
4528
|
+
<button onclick="wfExportJson()" id="wfExportBtn" disabled title="Export workflow JSON">
|
|
4529
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h10M8 14V5M5 8l3-3 3 3"/></svg>
|
|
4530
|
+
</button>
|
|
4531
|
+
</div>
|
|
4532
|
+
<!-- Execution status bar -->
|
|
4533
|
+
<div class="wf-exec-status" id="wfExecStatus" style="display:none;">
|
|
4534
|
+
<span class="wf-exec-status-dot"></span>
|
|
4535
|
+
<span class="wf-exec-status-text" id="wfExecStatusText">Running...</span>
|
|
4536
|
+
<span class="wf-exec-status-time" id="wfExecStatusTime"></span>
|
|
4537
|
+
</div>
|
|
4538
|
+
<div class="wf-canvas-empty" id="wfCanvasEmpty">
|
|
4539
|
+
<div class="wf-canvas-empty-icon"><img src="/icons/watermark.png" alt="Vai"></div>
|
|
4540
|
+
<div class="wf-canvas-empty-text">Select a workflow from the library</div>
|
|
4541
|
+
</div>
|
|
4542
|
+
<div class="wf-canvas-watermark" id="wfCanvasWatermark"><img src="/icons/watermark.png" alt=""></div>
|
|
4543
|
+
<svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg" ondragover="event.preventDefault()" ondrop="wfCanvasDrop(event)"></svg>
|
|
4544
|
+
</div>
|
|
4545
|
+
<div class="wf-inspector collapsed" id="wfInspector">
|
|
4546
|
+
<button class="wf-inspector-toggle" id="wfInspectorToggle" onclick="wfToggleInspector()" title="Toggle inspector">‹</button>
|
|
4547
|
+
<div class="wf-inspector-content">
|
|
4548
|
+
<div class="wf-inspector-header" id="wfInspectorHeader">Inspector</div>
|
|
4549
|
+
<div class="wf-inspector-body" id="wfInspectorBody">
|
|
4550
|
+
<div class="wf-inspector-empty">Click a node to inspect</div>
|
|
4551
|
+
</div>
|
|
4552
|
+
</div>
|
|
4553
|
+
</div>
|
|
4554
|
+
</div>
|
|
4555
|
+
</div>
|
|
4556
|
+
|
|
4557
|
+
<!-- ── Workflow Output Modal ── -->
|
|
4558
|
+
<div class="wf-output-modal-backdrop" id="wfOutputModalBackdrop" style="display:none;" onclick="wfCloseOutputModal()">
|
|
4559
|
+
<div class="wf-output-modal" onclick="event.stopPropagation()">
|
|
4560
|
+
<div class="wf-output-modal-header">
|
|
4561
|
+
<span class="wf-output-modal-title" id="wfOutputModalTitle">Output</span>
|
|
4562
|
+
<div class="wf-output-modal-actions">
|
|
4563
|
+
<button class="wf-output-modal-btn" onclick="wfCopyOutput()" title="Copy to clipboard" id="wfOutputCopyBtn">
|
|
4564
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5"/></svg>
|
|
4565
|
+
<span id="wfOutputCopyLabel">Copy</span>
|
|
4566
|
+
</button>
|
|
4567
|
+
<button class="wf-output-modal-btn close" onclick="wfCloseOutputModal()" title="Close">×</button>
|
|
4568
|
+
</div>
|
|
4569
|
+
</div>
|
|
4570
|
+
<pre class="wf-output-modal-body" id="wfOutputModalBody"></pre>
|
|
4571
|
+
</div>
|
|
4572
|
+
</div>
|
|
4573
|
+
|
|
4574
|
+
<!-- ── Workflow Input Modal (pre-execution) ── -->
|
|
4575
|
+
<div class="wf-input-modal-backdrop" id="wfInputModalBackdrop" style="display:none;" onclick="wfCloseInputModal()">
|
|
4576
|
+
<div class="wf-input-modal" onclick="event.stopPropagation()">
|
|
4577
|
+
<div class="wf-input-modal-header">
|
|
4578
|
+
<span class="wf-input-modal-title" id="wfInputModalTitle">Workflow Inputs</span>
|
|
4579
|
+
<button class="wf-output-modal-btn close" onclick="wfCloseInputModal()" title="Close">×</button>
|
|
4580
|
+
</div>
|
|
4581
|
+
<div class="wf-input-modal-body" id="wfInputModalBody"></div>
|
|
4582
|
+
<div class="wf-input-modal-footer">
|
|
4583
|
+
<button class="wf-input-modal-cancel" onclick="wfCloseInputModal()">Cancel</button>
|
|
4584
|
+
<button class="wf-input-modal-run" onclick="wfInputModalSubmit()">Run Workflow</button>
|
|
4585
|
+
</div>
|
|
4586
|
+
</div>
|
|
4587
|
+
</div>
|
|
4588
|
+
|
|
3667
4589
|
<!-- ========== ABOUT TAB ========== -->
|
|
3668
4590
|
<div class="tab-panel" id="tab-about" role="tabpanel" aria-labelledby="tab-btn-about" tabindex="0">
|
|
3669
4591
|
<div class="about-container">
|
|
@@ -3674,9 +4596,9 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3674
4596
|
<div class="about-name">Michael Lynn</div>
|
|
3675
4597
|
<div class="about-role">Principal Staff Developer Advocate · MongoDB</div>
|
|
3676
4598
|
<div class="about-links">
|
|
3677
|
-
<a href="https://github.com/mrlynn" target="_blank" rel="noopener"
|
|
3678
|
-
<a href="https://mlynn.org" target="_blank" rel="noopener"
|
|
3679
|
-
<a href="https://www.npmjs.com/package/voyageai-cli" target="_blank" rel="noopener"
|
|
4599
|
+
<a href="https://github.com/mrlynn" target="_blank" rel="noopener"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>GitHub</a>
|
|
4600
|
+
<a href="https://mlynn.org" target="_blank" rel="noopener"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>mlynn.org</a>
|
|
4601
|
+
<a href="https://www.npmjs.com/package/voyageai-cli" target="_blank" rel="noopener"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12"/></svg>npm</a>
|
|
3680
4602
|
</div>
|
|
3681
4603
|
</div>
|
|
3682
4604
|
</div>
|
|
@@ -3709,8 +4631,11 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3709
4631
|
<strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product & euclidean distance<br>
|
|
3710
4632
|
<strong>🔍 Search</strong> — Semantic search with optional reranking<br>
|
|
3711
4633
|
<strong>🔮 Multimodal</strong> — Compare images and text in the same vector space with voyage-multimodal-3.5<br>
|
|
4634
|
+
<strong>🛠️ Generate</strong> — Generate code snippets and scaffold full projects with templates<br>
|
|
4635
|
+
<strong>💬 Chat</strong> — RAG-powered chat with your documents, configurable system prompts<br>
|
|
4636
|
+
<strong>🔄 Workflows</strong> — Multi-step agent workflows with thinking panels and tool orchestration<br>
|
|
3712
4637
|
<strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
|
|
3713
|
-
<strong>📚 Explore</strong> —
|
|
4638
|
+
<strong>📚 Explore</strong> — 22 interactive concepts covering embeddings, vector search, multimodal, RAG, and more
|
|
3714
4639
|
</div>
|
|
3715
4640
|
</div>
|
|
3716
4641
|
|
|
@@ -3741,9 +4666,12 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3741
4666
|
<div class="about-section" style="padding-bottom:0;">
|
|
3742
4667
|
<div class="about-section-title">What's New</div>
|
|
3743
4668
|
<div class="about-text" style="font-size:13px;">
|
|
3744
|
-
<strong>v1.
|
|
3745
|
-
|
|
3746
|
-
|
|
4669
|
+
<strong>v1.26</strong> — Agent workflows with thinking panel & markdown rendering, multi-step tool orchestration<br>
|
|
4670
|
+
<strong>v1.25</strong> — Code generation & project scaffolding tabs<br>
|
|
4671
|
+
<strong>v1.24</strong> — MCP server install/uninstall/status commands, Electron app v1.5<br>
|
|
4672
|
+
<strong>v1.23</strong> — MCP server (expose vai tools to AI agents), HTTP transport, bearer auth, 71+ MCP tests<br>
|
|
4673
|
+
<strong>v1.22</strong> — RAG chat with smart source labels, configurable system prompts, streaming responses<br>
|
|
4674
|
+
<strong>v1.2</strong> — Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg 🕹️
|
|
3747
4675
|
</div>
|
|
3748
4676
|
</div>
|
|
3749
4677
|
</div>
|
|
@@ -3760,16 +4688,17 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3760
4688
|
<div class="page-header">
|
|
3761
4689
|
<h2 class="page-header-title">Generate</h2>
|
|
3762
4690
|
<p class="page-header-subtitle">Generate code and scaffold projects</p>
|
|
4691
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/project-setup/generate" target="_blank" rel="noopener" title="Generate documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
3763
4692
|
</div>
|
|
3764
4693
|
|
|
3765
4694
|
<!-- Mode Toggle -->
|
|
3766
4695
|
<div class="subtabs" role="tablist">
|
|
3767
4696
|
<button class="subtab active" id="genModeCode" onclick="setGenerateMode('code')" role="tab" aria-selected="true">
|
|
3768
|
-
<svg viewBox="0 0
|
|
4697
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
3769
4698
|
Generate Code
|
|
3770
4699
|
</button>
|
|
3771
4700
|
<button class="subtab" id="genModeScaffold" onclick="setGenerateMode('scaffold')" role="tab" aria-selected="false">
|
|
3772
|
-
<svg viewBox="0 0
|
|
4701
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 13h4"/><path d="M10 17h4"/><path d="M10 9h1"/></svg>
|
|
3773
4702
|
Scaffold Project
|
|
3774
4703
|
</button>
|
|
3775
4704
|
</div>
|
|
@@ -3911,7 +4840,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3911
4840
|
</div>
|
|
3912
4841
|
<div id="scaffoldWebButtons" style="display:none;">
|
|
3913
4842
|
<button class="btn btn-primary" onclick="downloadScaffoldZip()" id="scaffoldDownloadBtn">
|
|
3914
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4843
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
|
|
3915
4844
|
Download ZIP
|
|
3916
4845
|
</button>
|
|
3917
4846
|
</div>
|
|
@@ -3933,7 +4862,8 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3933
4862
|
<div class="page-header">
|
|
3934
4863
|
<h2 class="page-header-title">Explore</h2>
|
|
3935
4864
|
<p class="page-header-subtitle">Learn embedding and vector search concepts</p>
|
|
3936
|
-
<p class="page-header-hint">Browse interactive explanations of key topics
|
|
4865
|
+
<p class="page-header-hint">Browse interactive explanations of key topics, from cosine similarity to quantization to RAG pipelines.</p>
|
|
4866
|
+
<a class="page-header-docs" href="https://docs.vaicli.com/docs/commands/tools-and-learning/explain" target="_blank" rel="noopener" title="Explore documentation"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 1h7l3 3v11H3z"/><path d="M10 1v3h3"/></svg>Docs</a>
|
|
3937
4867
|
</div>
|
|
3938
4868
|
<div style="margin-bottom:16px;">
|
|
3939
4869
|
<input type="text" id="exploreSearch" placeholder="🔍 Search concepts..." oninput="filterExplore()" style="max-width:400px;" aria-label="Search concepts">
|
|
@@ -3967,29 +4897,33 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3967
4897
|
<nav class="settings-nav">
|
|
3968
4898
|
<div class="settings-nav-header">Settings</div>
|
|
3969
4899
|
<button class="settings-nav-item active" data-settings-section="general">
|
|
3970
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4900
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-config"/></svg>
|
|
3971
4901
|
<span>General</span>
|
|
3972
4902
|
</button>
|
|
3973
4903
|
<button class="settings-nav-item" data-settings-section="appearance">
|
|
3974
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4904
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-palette"/></svg>
|
|
3975
4905
|
<span>Appearance</span>
|
|
3976
4906
|
</button>
|
|
3977
4907
|
<button class="settings-nav-item" data-settings-section="models">
|
|
3978
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4908
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-cube"/></svg>
|
|
3979
4909
|
<span>Models</span>
|
|
3980
4910
|
</button>
|
|
3981
4911
|
<button class="settings-nav-item" data-settings-section="chat">
|
|
3982
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4912
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-chat"/></svg>
|
|
3983
4913
|
<span>Chat</span>
|
|
3984
4914
|
</button>
|
|
3985
4915
|
<button class="settings-nav-item" data-settings-section="benchmark">
|
|
3986
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4916
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-gauge"/></svg>
|
|
3987
4917
|
<span>Benchmark</span>
|
|
3988
4918
|
</button>
|
|
3989
4919
|
<button class="settings-nav-item" data-settings-section="privacy">
|
|
3990
|
-
<svg width="16" height="16" viewBox="0 0
|
|
4920
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-shield"/></svg>
|
|
3991
4921
|
<span>Data & Privacy</span>
|
|
3992
4922
|
</button>
|
|
4923
|
+
<button class="settings-nav-item" data-settings-section="health">
|
|
4924
|
+
<svg width="16" height="16" viewBox="0 0 24 24"><use href="#lg-pulse"/></svg>
|
|
4925
|
+
<span>Health Check</span>
|
|
4926
|
+
</button>
|
|
3993
4927
|
</nav>
|
|
3994
4928
|
|
|
3995
4929
|
<!-- Settings content panels -->
|
|
@@ -4009,12 +4943,12 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4009
4943
|
<div class="settings-row">
|
|
4010
4944
|
<div class="settings-label">
|
|
4011
4945
|
<span class="settings-label-text">API Key <span class="settings-origin" data-origin-key="apiKey"></span></span>
|
|
4012
|
-
<span class="settings-label-hint">Encrypted via OS keychain · <a href="https://dash.voyageai.com" target="_blank" class="settings-key-link"
|
|
4946
|
+
<span class="settings-label-hint">Encrypted via OS keychain · <a href="https://dash.voyageai.com" target="_blank" class="settings-key-link"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:2px;"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>Get a key</a></span>
|
|
4013
4947
|
</div>
|
|
4014
4948
|
<div class="settings-control" style="min-width:260px;">
|
|
4015
4949
|
<div class="settings-api-field">
|
|
4016
4950
|
<input type="password" id="settingsApiKey" placeholder="pa-..." autocomplete="off" spellcheck="false">
|
|
4017
|
-
<button type="button" id="settingsApiKeyToggle" title="Show/hide key"
|
|
4951
|
+
<button type="button" id="settingsApiKeyToggle" title="Show/hide key"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></button>
|
|
4018
4952
|
<button type="button" id="settingsApiKeySave" class="save-btn" title="Save key">Save</button>
|
|
4019
4953
|
</div>
|
|
4020
4954
|
</div>
|
|
@@ -4168,7 +5102,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4168
5102
|
title="Show/hide API key"
|
|
4169
5103
|
style="padding:8px 12px;min-width:auto"
|
|
4170
5104
|
>
|
|
4171
|
-
|
|
5105
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
|
|
4172
5106
|
</button>
|
|
4173
5107
|
<button
|
|
4174
5108
|
class="btn"
|
|
@@ -4177,7 +5111,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4177
5111
|
title="Save API key"
|
|
4178
5112
|
style="padding:8px 12px;min-width:auto"
|
|
4179
5113
|
>
|
|
4180
|
-
|
|
5114
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>
|
|
4181
5115
|
</button>
|
|
4182
5116
|
</div>
|
|
4183
5117
|
</div>
|
|
@@ -4349,6 +5283,30 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4349
5283
|
</div>
|
|
4350
5284
|
</div>
|
|
4351
5285
|
|
|
5286
|
+
<!-- ── Health Check ── -->
|
|
5287
|
+
<div class="settings-panel" id="settings-health">
|
|
5288
|
+
<div class="settings-panel-header">
|
|
5289
|
+
<h3 class="settings-panel-title">Health Check</h3>
|
|
5290
|
+
<p class="settings-panel-subtitle">Validate your vai setup and connectivity</p>
|
|
5291
|
+
</div>
|
|
5292
|
+
<div class="settings-section">
|
|
5293
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
|
5294
|
+
<span class="settings-section-title" style="margin:0;">Diagnostics</span>
|
|
5295
|
+
<button class="btn" id="doctorRunBtn" style="padding:6px 16px;font-size:13px;">Run Health Check</button>
|
|
5296
|
+
</div>
|
|
5297
|
+
<div id="doctorResults" style="display:none;">
|
|
5298
|
+
<div id="doctorCheckList" style="display:flex;flex-direction:column;gap:6px;"></div>
|
|
5299
|
+
<div id="doctorSummary" style="margin-top:16px;padding:12px 16px;border-radius:var(--radius);font-size:13px;"></div>
|
|
5300
|
+
</div>
|
|
5301
|
+
<div id="doctorLoading" style="display:none;text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">
|
|
5302
|
+
Running health checks...
|
|
5303
|
+
</div>
|
|
5304
|
+
<div id="doctorEmpty" style="text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">
|
|
5305
|
+
Click "Run Health Check" to validate your setup.
|
|
5306
|
+
</div>
|
|
5307
|
+
</div>
|
|
5308
|
+
</div>
|
|
5309
|
+
|
|
4352
5310
|
<div style="text-align:center;padding:8px 0;">
|
|
4353
5311
|
<span class="settings-saved" id="settingsSavedMsg">✓ Saved</span>
|
|
4354
5312
|
</div>
|
|
@@ -5169,12 +6127,12 @@ async function downloadScaffoldZip() {
|
|
|
5169
6127
|
// Show success
|
|
5170
6128
|
btn.innerHTML = '<span style="margin-right:6px;">✓</span> Downloaded!';
|
|
5171
6129
|
setTimeout(() => {
|
|
5172
|
-
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0
|
|
6130
|
+
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download ZIP';
|
|
5173
6131
|
btn.disabled = false;
|
|
5174
6132
|
}, 2000);
|
|
5175
6133
|
} catch (err) {
|
|
5176
6134
|
alert('Error: ' + err.message);
|
|
5177
|
-
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0
|
|
6135
|
+
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download ZIP';
|
|
5178
6136
|
btn.disabled = false;
|
|
5179
6137
|
}
|
|
5180
6138
|
}
|
|
@@ -5270,37 +6228,73 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5270
6228
|
});
|
|
5271
6229
|
|
|
5272
6230
|
// ── Explore: icons and tab mappings per concept ──
|
|
6231
|
+
// Lucide SVG icon helper — returns an inline <svg> string
|
|
6232
|
+
function lucideIcon(d, size = 16) {
|
|
6233
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
6234
|
+
}
|
|
6235
|
+
|
|
6236
|
+
// Lucide path constants for concept/button icons
|
|
6237
|
+
const LI = {
|
|
6238
|
+
zap: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z',
|
|
6239
|
+
search: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
|
|
6240
|
+
trophy: 'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z',
|
|
6241
|
+
bot: 'M12 8V4H8M8 2h8M2 14a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zM6 14v4M10 14v4M14 14v4M18 14v4',
|
|
6242
|
+
ruler: 'M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z M15 5l4 4',
|
|
6243
|
+
target: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0',
|
|
6244
|
+
tag: 'M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42zM7.5 7.5m-.5 0a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0',
|
|
6245
|
+
brain: 'M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2zM14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z',
|
|
6246
|
+
key: 'M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4zM16.5 7.5m-.5 0a.5.5 0 1 0 1 0 .5.5 0 1 0-1 0',
|
|
6247
|
+
globe: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z',
|
|
6248
|
+
package: 'M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12',
|
|
6249
|
+
timer: 'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
|
|
6250
|
+
flask: 'M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10',
|
|
6251
|
+
puzzle: 'M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.878.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.968 1.02z',
|
|
6252
|
+
link: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
|
|
6253
|
+
barChart: 'M12 20V10M18 20V4M6 20v-4',
|
|
6254
|
+
microscope: 'M6 18h8M3 22h18M14 22a7 7 0 1 0 0-14h-1M9 14h2M9 12a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2z',
|
|
6255
|
+
image: 'M3 3h18a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM21 15l-5-5L5 21',
|
|
6256
|
+
shuffle: 'M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22M18 2l4 4-4 4M2 6h1.9c1.5 0 2.9.9 3.6 2.2M22 18l-4 4-4-4M19 14h3',
|
|
6257
|
+
circle: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0',
|
|
6258
|
+
fileTxt: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM14 2v4a2 2 0 0 0 2 2h4M10 13h4M10 17h4M10 9h1',
|
|
6259
|
+
scale: 'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22',
|
|
6260
|
+
laptop: 'M20 16V7a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9M2 20h20M12 12h.01',
|
|
6261
|
+
blocks: 'M2 12h10v10H2zM14 4l6 3.5v7L14 18l-6-3.5v-7zM12 2l10 6',
|
|
6262
|
+
refresh: 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16',
|
|
6263
|
+
sparkle: 'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z',
|
|
6264
|
+
download: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
|
|
6265
|
+
copy: 'M9 9V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-3M3 15a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3z',
|
|
6266
|
+
filter: 'M3 6h18M7 12h10M10 18h4',
|
|
6267
|
+
};
|
|
6268
|
+
|
|
5273
6269
|
const CONCEPT_META = {
|
|
5274
|
-
embeddings: { icon:
|
|
5275
|
-
reranking: { icon:
|
|
5276
|
-
'vector-search': { icon:
|
|
5277
|
-
rag: { icon:
|
|
5278
|
-
'cosine-similarity': { icon:
|
|
5279
|
-
'two-stage-retrieval': { icon:
|
|
5280
|
-
'input-type': { icon:
|
|
5281
|
-
models: { icon:
|
|
5282
|
-
'api-keys': { icon:
|
|
5283
|
-
'api-access': { icon:
|
|
5284
|
-
'batch-processing': { icon:
|
|
5285
|
-
benchmarking: { icon:
|
|
5286
|
-
quantization: { icon:
|
|
5287
|
-
'mixture-of-experts': { icon:
|
|
5288
|
-
'shared-embedding-space': { icon:
|
|
5289
|
-
'rteb-benchmarks': { icon:
|
|
5290
|
-
'voyage-4-nano': { icon:
|
|
5291
|
-
'rerank-eval': { icon:
|
|
5292
|
-
'multimodal-embeddings': { icon:
|
|
5293
|
-
'cross-modal-search': { icon:
|
|
5294
|
-
'modality-gap': { icon:
|
|
5295
|
-
'multimodal-rag': { icon:
|
|
5296
|
-
'provider-comparison': { icon:
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
'
|
|
5301
|
-
|
|
5302
|
-
'auto-embedding': { icon: '⚡', tab: 'explore' },
|
|
5303
|
-
'vai-vs-auto-embedding': { icon: '🔄', tab: 'explore' },
|
|
6270
|
+
embeddings: { icon: LI.zap, tab: 'embed' },
|
|
6271
|
+
reranking: { icon: LI.trophy, tab: 'search' },
|
|
6272
|
+
'vector-search': { icon: LI.search, tab: 'search' },
|
|
6273
|
+
rag: { icon: LI.bot, tab: 'search' },
|
|
6274
|
+
'cosine-similarity': { icon: LI.ruler, tab: 'compare' },
|
|
6275
|
+
'two-stage-retrieval': { icon: LI.target, tab: 'search' },
|
|
6276
|
+
'input-type': { icon: LI.tag, tab: 'embed' },
|
|
6277
|
+
models: { icon: LI.brain, tab: 'embed' },
|
|
6278
|
+
'api-keys': { icon: LI.key, tab: 'embed' },
|
|
6279
|
+
'api-access': { icon: LI.globe, tab: 'embed' },
|
|
6280
|
+
'batch-processing': { icon: LI.package, tab: 'embed' },
|
|
6281
|
+
benchmarking: { icon: LI.timer, tab: 'benchmark' },
|
|
6282
|
+
quantization: { icon: LI.flask, tab: 'benchmark' },
|
|
6283
|
+
'mixture-of-experts': { icon: LI.puzzle, tab: 'embed' },
|
|
6284
|
+
'shared-embedding-space': { icon: LI.link, tab: 'compare' },
|
|
6285
|
+
'rteb-benchmarks': { icon: LI.barChart, tab: 'benchmark' },
|
|
6286
|
+
'voyage-4-nano': { icon: LI.microscope, tab: 'embed' },
|
|
6287
|
+
'rerank-eval': { icon: LI.ruler, tab: 'benchmark' },
|
|
6288
|
+
'multimodal-embeddings': { icon: LI.image, tab: 'multimodal' },
|
|
6289
|
+
'cross-modal-search': { icon: LI.shuffle, tab: 'multimodal' },
|
|
6290
|
+
'modality-gap': { icon: LI.circle, tab: 'multimodal' },
|
|
6291
|
+
'multimodal-rag': { icon: LI.fileTxt, tab: 'multimodal' },
|
|
6292
|
+
'provider-comparison': { icon: LI.scale, tab: 'explore' },
|
|
6293
|
+
'code-generation': { icon: LI.laptop, tab: 'explore' },
|
|
6294
|
+
scaffolding: { icon: LI.blocks, tab: 'explore' },
|
|
6295
|
+
'eval-comparison': { icon: LI.barChart, tab: 'benchmark' },
|
|
6296
|
+
'auto-embedding': { icon: LI.zap, tab: 'explore' },
|
|
6297
|
+
'vai-vs-auto-embedding': { icon: LI.refresh, tab: 'explore' },
|
|
5304
6298
|
};
|
|
5305
6299
|
|
|
5306
6300
|
let exploreConcepts = {};
|
|
@@ -5316,7 +6310,9 @@ async function loadConcepts() {
|
|
|
5316
6310
|
}
|
|
5317
6311
|
|
|
5318
6312
|
function escapeHtml(str) {
|
|
5319
|
-
|
|
6313
|
+
if (str == null) return '';
|
|
6314
|
+
const s = typeof str === 'string' ? str : String(str);
|
|
6315
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
5320
6316
|
}
|
|
5321
6317
|
|
|
5322
6318
|
function buildExploreCards() {
|
|
@@ -5324,13 +6320,13 @@ function buildExploreCards() {
|
|
|
5324
6320
|
grid.innerHTML = '';
|
|
5325
6321
|
|
|
5326
6322
|
for (const [key, concept] of Object.entries(exploreConcepts)) {
|
|
5327
|
-
const meta = CONCEPT_META[key] || { icon:
|
|
6323
|
+
const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
|
|
5328
6324
|
const card = document.createElement('div');
|
|
5329
6325
|
card.className = 'explore-card';
|
|
5330
6326
|
card.dataset.key = key;
|
|
5331
6327
|
|
|
5332
6328
|
card.innerHTML = `
|
|
5333
|
-
<div class="explore-card-icon">${meta.icon}</div>
|
|
6329
|
+
<div class="explore-card-icon">${lucideIcon(meta.icon, 28)}</div>
|
|
5334
6330
|
<div class="explore-card-title">${escapeHtml(concept.title)}</div>
|
|
5335
6331
|
<div class="explore-card-summary">${escapeHtml(concept.summary)}</div>
|
|
5336
6332
|
`;
|
|
@@ -5357,12 +6353,12 @@ let exploreModalPreviousFocus = null;
|
|
|
5357
6353
|
function openExploreModal(key) {
|
|
5358
6354
|
const concept = exploreConcepts[key];
|
|
5359
6355
|
if (!concept) return;
|
|
5360
|
-
const meta = CONCEPT_META[key] || { icon:
|
|
6356
|
+
const meta = CONCEPT_META[key] || { icon: LI.package, tab: 'embed' };
|
|
5361
6357
|
|
|
5362
6358
|
// Save the currently focused element to restore when modal closes
|
|
5363
6359
|
exploreModalPreviousFocus = document.activeElement;
|
|
5364
6360
|
|
|
5365
|
-
document.getElementById('exploreModalIcon').
|
|
6361
|
+
document.getElementById('exploreModalIcon').innerHTML = lucideIcon(meta.icon, 32);
|
|
5366
6362
|
document.getElementById('exploreModalTitle').textContent = concept.title;
|
|
5367
6363
|
document.getElementById('exploreModalSummary').textContent = concept.summary;
|
|
5368
6364
|
|
|
@@ -6676,6 +7672,95 @@ function initSettings() {
|
|
|
6676
7672
|
|
|
6677
7673
|
// Set up settings sub-navigation
|
|
6678
7674
|
setupSettingsNav();
|
|
7675
|
+
|
|
7676
|
+
// Set up doctor health check button
|
|
7677
|
+
setupDoctorPanel();
|
|
7678
|
+
}
|
|
7679
|
+
|
|
7680
|
+
// ── Doctor Health Check ──
|
|
7681
|
+
function setupDoctorPanel() {
|
|
7682
|
+
const runBtn = document.getElementById('doctorRunBtn');
|
|
7683
|
+
if (!runBtn) return;
|
|
7684
|
+
runBtn.addEventListener('click', runDoctorCheck);
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
async function runDoctorCheck() {
|
|
7688
|
+
const btn = document.getElementById('doctorRunBtn');
|
|
7689
|
+
const loading = document.getElementById('doctorLoading');
|
|
7690
|
+
const results = document.getElementById('doctorResults');
|
|
7691
|
+
const empty = document.getElementById('doctorEmpty');
|
|
7692
|
+
const checkList = document.getElementById('doctorCheckList');
|
|
7693
|
+
const summary = document.getElementById('doctorSummary');
|
|
7694
|
+
|
|
7695
|
+
btn.disabled = true;
|
|
7696
|
+
btn.textContent = 'Running...';
|
|
7697
|
+
empty.style.display = 'none';
|
|
7698
|
+
results.style.display = 'none';
|
|
7699
|
+
loading.style.display = 'block';
|
|
7700
|
+
|
|
7701
|
+
try {
|
|
7702
|
+
const res = await fetch('/api/doctor');
|
|
7703
|
+
const data = await res.json();
|
|
7704
|
+
|
|
7705
|
+
checkList.innerHTML = '';
|
|
7706
|
+
let hasError = false;
|
|
7707
|
+
let hasWarning = false;
|
|
7708
|
+
|
|
7709
|
+
for (const [key, check] of Object.entries(data)) {
|
|
7710
|
+
let icon, badgeClass, badgeText;
|
|
7711
|
+
if (check.ok === true) {
|
|
7712
|
+
icon = '✓';
|
|
7713
|
+
badgeClass = 'pass';
|
|
7714
|
+
badgeText = 'pass';
|
|
7715
|
+
} else if (check.ok === false) {
|
|
7716
|
+
icon = '✗';
|
|
7717
|
+
if (check.required) { hasError = true; badgeClass = 'fail'; badgeText = 'fail'; }
|
|
7718
|
+
else { hasWarning = true; badgeClass = 'warn'; badgeText = 'warn'; }
|
|
7719
|
+
} else {
|
|
7720
|
+
icon = '⚠';
|
|
7721
|
+
hasWarning = true;
|
|
7722
|
+
badgeClass = 'optional';
|
|
7723
|
+
badgeText = 'optional';
|
|
7724
|
+
}
|
|
7725
|
+
|
|
7726
|
+
const el = document.createElement('div');
|
|
7727
|
+
el.className = 'doctor-check';
|
|
7728
|
+
el.innerHTML = `
|
|
7729
|
+
<span class="doctor-check-icon" style="color:var(--${badgeClass === 'pass' ? 'success' : badgeClass === 'fail' ? 'error' : 'warning'})">${icon}</span>
|
|
7730
|
+
<div class="doctor-check-body">
|
|
7731
|
+
<div class="doctor-check-name">${check.name}</div>
|
|
7732
|
+
<div class="doctor-check-msg">${check.message || ''}</div>
|
|
7733
|
+
${check.hint ? `<div class="doctor-check-hint">${check.hint}</div>` : ''}
|
|
7734
|
+
</div>
|
|
7735
|
+
<span class="doctor-check-badge ${badgeClass}">${badgeText}</span>
|
|
7736
|
+
`;
|
|
7737
|
+
checkList.appendChild(el);
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
if (hasError) {
|
|
7741
|
+
summary.style.background = 'rgba(255,105,96,0.08)';
|
|
7742
|
+
summary.style.color = 'var(--error)';
|
|
7743
|
+
summary.textContent = 'Some required checks failed. Fix the issues above to use vai.';
|
|
7744
|
+
} else if (hasWarning) {
|
|
7745
|
+
summary.style.background = 'rgba(255,192,16,0.08)';
|
|
7746
|
+
summary.style.color = 'var(--warning)';
|
|
7747
|
+
summary.textContent = 'All required checks passed. Some optional features are not configured.';
|
|
7748
|
+
} else {
|
|
7749
|
+
summary.style.background = 'rgba(0,212,170,0.08)';
|
|
7750
|
+
summary.style.color = 'var(--success)';
|
|
7751
|
+
summary.textContent = 'All checks passed. vai is ready to use!';
|
|
7752
|
+
}
|
|
7753
|
+
|
|
7754
|
+
loading.style.display = 'none';
|
|
7755
|
+
results.style.display = 'block';
|
|
7756
|
+
} catch (err) {
|
|
7757
|
+
loading.style.display = 'none';
|
|
7758
|
+
empty.style.display = 'block';
|
|
7759
|
+
empty.textContent = 'Health check failed: ' + err.message;
|
|
7760
|
+
}
|
|
7761
|
+
|
|
7762
|
+
btn.disabled = false;
|
|
7763
|
+
btn.textContent = 'Run Health Check';
|
|
6679
7764
|
}
|
|
6680
7765
|
|
|
6681
7766
|
// ── Update Checker ──
|
|
@@ -6867,6 +7952,13 @@ function initOnboarding() {
|
|
|
6867
7952
|
body: '<strong>Chat with your knowledge base</strong> using retrieval-augmented generation. Voyage AI finds relevant documents, then your chosen LLM generates grounded answers with source citations.',
|
|
6868
7953
|
arrow: 'left',
|
|
6869
7954
|
},
|
|
7955
|
+
{
|
|
7956
|
+
target: '[data-tab="workflows"]',
|
|
7957
|
+
icon: '\u2699\uFE0F',
|
|
7958
|
+
title: 'Workflow Visualizer',
|
|
7959
|
+
body: '<strong>Visualize and execute workflows</strong> as interactive DAGs. Browse built-in templates, inspect step configuration, and watch execution animate in real time.',
|
|
7960
|
+
arrow: 'left',
|
|
7961
|
+
},
|
|
6870
7962
|
{
|
|
6871
7963
|
target: '[data-tab="benchmark"]',
|
|
6872
7964
|
icon: '⏱️',
|
|
@@ -8341,6 +9433,145 @@ function renderMarkdown(md) {
|
|
|
8341
9433
|
return result.join('\n');
|
|
8342
9434
|
}
|
|
8343
9435
|
|
|
9436
|
+
/**
|
|
9437
|
+
* Tool metadata: icon, label, and a function to summarize the call for the thinking panel.
|
|
9438
|
+
*/
|
|
9439
|
+
const TOOL_META = {
|
|
9440
|
+
vai_query: { icon: '\uD83D\uDD0D', label: 'RAG Query', verb: 'Searching', descFn: a => a.query ? `"${a.query}"` : '' },
|
|
9441
|
+
vai_search: { icon: '\uD83D\uDD0E', label: 'Vector Search', verb: 'Searching vectors', descFn: a => a.query ? `"${a.query}"` : '' },
|
|
9442
|
+
vai_rerank: { icon: '\u2195\uFE0F', label: 'Rerank', verb: 'Reranking', descFn: a => a.query ? `${a.documents?.length || '?'} docs for "${a.query}"` : '' },
|
|
9443
|
+
vai_embed: { icon: '\uD83E\uDDE0', label: 'Embed', verb: 'Embedding', descFn: a => a.text ? `"${a.text.slice(0, 60)}${a.text.length > 60 ? '...' : ''}"` : '' },
|
|
9444
|
+
vai_similarity: { icon: '\uD83C\uDFAF', label: 'Similarity', verb: 'Comparing', descFn: a => a.text1 ? `two texts` : '' },
|
|
9445
|
+
vai_collections: { icon: '\uD83D\uDDC4\uFE0F', label: 'Collections', verb: 'Discovering', descFn: a => a.db ? `in ${a.db}` : 'available databases' },
|
|
9446
|
+
vai_models: { icon: '\uD83E\uDD16', label: 'Models', verb: 'Listing', descFn: () => 'available models' },
|
|
9447
|
+
vai_topics: { icon: '\uD83D\uDCDA', label: 'Topics', verb: 'Browsing', descFn: () => 'educational topics' },
|
|
9448
|
+
vai_explain: { icon: '\uD83D\uDCA1', label: 'Explain', verb: 'Explaining', descFn: a => a.topic || '' },
|
|
9449
|
+
vai_estimate: { icon: '\uD83D\uDCB0', label: 'Cost Estimate', verb: 'Estimating', descFn: a => a.docs ? `${a.docs} docs` : '' },
|
|
9450
|
+
vai_ingest: { icon: '\uD83D\uDCE5', label: 'Ingest', verb: 'Ingesting', descFn: a => a.source || 'document' },
|
|
9451
|
+
};
|
|
9452
|
+
const DEFAULT_TOOL_META = { icon: '\u2699\uFE0F', label: '', verb: 'Running', descFn: () => '' };
|
|
9453
|
+
|
|
9454
|
+
/**
|
|
9455
|
+
* Create the thinking panel <details> element.
|
|
9456
|
+
* Returns { panel, timeline, addStep, finalize }.
|
|
9457
|
+
*/
|
|
9458
|
+
function createThinkingPanel() {
|
|
9459
|
+
const panel = document.createElement('details');
|
|
9460
|
+
panel.className = 'chat-thinking';
|
|
9461
|
+
panel.open = true;
|
|
9462
|
+
|
|
9463
|
+
const summary = document.createElement('summary');
|
|
9464
|
+
summary.innerHTML =
|
|
9465
|
+
'<span class="thinking-icon">\uD83E\uDDE0</span>' +
|
|
9466
|
+
'<span class="thinking-label">Thinking</span>' +
|
|
9467
|
+
'<span class="thinking-count">0</span>' +
|
|
9468
|
+
'<span class="thinking-elapsed"></span>' +
|
|
9469
|
+
'<span class="thinking-chevron">\u25B6</span>';
|
|
9470
|
+
panel.appendChild(summary);
|
|
9471
|
+
|
|
9472
|
+
const timeline = document.createElement('div');
|
|
9473
|
+
timeline.className = 'thinking-timeline';
|
|
9474
|
+
panel.appendChild(timeline);
|
|
9475
|
+
|
|
9476
|
+
let stepCount = 0;
|
|
9477
|
+
let activeStep = null;
|
|
9478
|
+
const startTime = Date.now();
|
|
9479
|
+
let elapsedTimer = null;
|
|
9480
|
+
|
|
9481
|
+
// Update the elapsed time in the summary
|
|
9482
|
+
function tickElapsed() {
|
|
9483
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
9484
|
+
summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
|
|
9485
|
+
}
|
|
9486
|
+
elapsedTimer = setInterval(tickElapsed, 200);
|
|
9487
|
+
tickElapsed();
|
|
9488
|
+
|
|
9489
|
+
function addStep(data) {
|
|
9490
|
+
const meta = TOOL_META[data.name] || DEFAULT_TOOL_META;
|
|
9491
|
+
const desc = meta.descFn(data.args || {});
|
|
9492
|
+
|
|
9493
|
+
// Mark previous active step as done
|
|
9494
|
+
if (activeStep) {
|
|
9495
|
+
activeStep.classList.remove('active');
|
|
9496
|
+
activeStep.classList.add('done');
|
|
9497
|
+
}
|
|
9498
|
+
|
|
9499
|
+
stepCount++;
|
|
9500
|
+
summary.querySelector('.thinking-count').textContent = stepCount;
|
|
9501
|
+
|
|
9502
|
+
const step = document.createElement('div');
|
|
9503
|
+
step.className = 'thinking-step active';
|
|
9504
|
+
if (data.error) step.className = 'thinking-step error';
|
|
9505
|
+
|
|
9506
|
+
const iconDiv = document.createElement('div');
|
|
9507
|
+
iconDiv.className = 'thinking-step-icon';
|
|
9508
|
+
iconDiv.textContent = meta.icon;
|
|
9509
|
+
step.appendChild(iconDiv);
|
|
9510
|
+
|
|
9511
|
+
const body = document.createElement('div');
|
|
9512
|
+
body.className = 'thinking-step-body';
|
|
9513
|
+
|
|
9514
|
+
const header = document.createElement('div');
|
|
9515
|
+
header.className = 'thinking-step-header';
|
|
9516
|
+
const nameSpan = document.createElement('span');
|
|
9517
|
+
nameSpan.className = 'thinking-step-name';
|
|
9518
|
+
nameSpan.textContent = meta.verb + (desc ? ' ' : '') + (meta.label || data.name);
|
|
9519
|
+
header.appendChild(nameSpan);
|
|
9520
|
+
|
|
9521
|
+
if (data.timeMs !== undefined) {
|
|
9522
|
+
const timeSpan = document.createElement('span');
|
|
9523
|
+
timeSpan.className = 'thinking-step-time';
|
|
9524
|
+
timeSpan.textContent = data.timeMs + 'ms';
|
|
9525
|
+
header.appendChild(timeSpan);
|
|
9526
|
+
}
|
|
9527
|
+
body.appendChild(header);
|
|
9528
|
+
|
|
9529
|
+
if (desc) {
|
|
9530
|
+
const descDiv = document.createElement('div');
|
|
9531
|
+
descDiv.className = 'thinking-step-desc';
|
|
9532
|
+
descDiv.textContent = desc;
|
|
9533
|
+
body.appendChild(descDiv);
|
|
9534
|
+
}
|
|
9535
|
+
|
|
9536
|
+
if (data.error) {
|
|
9537
|
+
const errDiv = document.createElement('div');
|
|
9538
|
+
errDiv.className = 'thinking-step-detail';
|
|
9539
|
+
errDiv.style.color = '#e74c3c';
|
|
9540
|
+
errDiv.textContent = data.error;
|
|
9541
|
+
body.appendChild(errDiv);
|
|
9542
|
+
} else if (data.resultSummary) {
|
|
9543
|
+
const detailDiv = document.createElement('div');
|
|
9544
|
+
detailDiv.className = 'thinking-step-detail';
|
|
9545
|
+
detailDiv.innerHTML = data.resultSummary;
|
|
9546
|
+
body.appendChild(detailDiv);
|
|
9547
|
+
}
|
|
9548
|
+
|
|
9549
|
+
step.appendChild(body);
|
|
9550
|
+
timeline.appendChild(step);
|
|
9551
|
+
|
|
9552
|
+
if (!data.error) activeStep = step;
|
|
9553
|
+
|
|
9554
|
+
return step;
|
|
9555
|
+
}
|
|
9556
|
+
|
|
9557
|
+
function finalize() {
|
|
9558
|
+
clearInterval(elapsedTimer);
|
|
9559
|
+
// Final elapsed
|
|
9560
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
9561
|
+
summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
|
|
9562
|
+
summary.querySelector('.thinking-label').textContent = 'Thought for ' + elapsed + 's';
|
|
9563
|
+
summary.querySelector('.thinking-icon').textContent = '\u2728';
|
|
9564
|
+
// Mark last step done and collapse
|
|
9565
|
+
if (activeStep) {
|
|
9566
|
+
activeStep.classList.remove('active');
|
|
9567
|
+
activeStep.classList.add('done');
|
|
9568
|
+
}
|
|
9569
|
+
panel.open = false;
|
|
9570
|
+
}
|
|
9571
|
+
|
|
9572
|
+
return { panel, addStep, finalize };
|
|
9573
|
+
}
|
|
9574
|
+
|
|
8344
9575
|
function addChatMessage(role, content, sources) {
|
|
8345
9576
|
const container = document.getElementById('chatMessages');
|
|
8346
9577
|
const div = document.createElement('div');
|
|
@@ -8350,6 +9581,26 @@ function addChatMessage(role, content, sources) {
|
|
|
8350
9581
|
contentSpan.textContent = content;
|
|
8351
9582
|
div.appendChild(contentSpan);
|
|
8352
9583
|
|
|
9584
|
+
// Add copy button for assistant messages
|
|
9585
|
+
if (role === 'assistant') {
|
|
9586
|
+
const copyBtn = document.createElement('button');
|
|
9587
|
+
copyBtn.className = 'chat-copy-btn';
|
|
9588
|
+
copyBtn.title = 'Copy to clipboard';
|
|
9589
|
+
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5"/></svg>';
|
|
9590
|
+
copyBtn.addEventListener('click', () => {
|
|
9591
|
+
const text = contentSpan.textContent || contentSpan.innerText;
|
|
9592
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
9593
|
+
copyBtn.classList.add('copied');
|
|
9594
|
+
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 8.5l3 3 7-7"/></svg>';
|
|
9595
|
+
setTimeout(() => {
|
|
9596
|
+
copyBtn.classList.remove('copied');
|
|
9597
|
+
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M5 11H3.5A1.5 1.5 0 0 1 2 9.5V3.5A1.5 1.5 0 0 1 3.5 2h6A1.5 1.5 0 0 1 11 3.5V5"/></svg>';
|
|
9598
|
+
}, 2000);
|
|
9599
|
+
});
|
|
9600
|
+
});
|
|
9601
|
+
div.appendChild(copyBtn);
|
|
9602
|
+
}
|
|
9603
|
+
|
|
8353
9604
|
if (sources && sources.length > 0) {
|
|
8354
9605
|
const details = document.createElement('details');
|
|
8355
9606
|
details.className = 'chat-sources';
|
|
@@ -8433,7 +9684,7 @@ async function sendChatMessage() {
|
|
|
8433
9684
|
let assistantDiv = null;
|
|
8434
9685
|
let fullText = '';
|
|
8435
9686
|
let sources = [];
|
|
8436
|
-
let
|
|
9687
|
+
let thinkingPanel = null;
|
|
8437
9688
|
|
|
8438
9689
|
while (true) {
|
|
8439
9690
|
const { done, value } = await reader.read();
|
|
@@ -8457,33 +9708,13 @@ async function sendChatMessage() {
|
|
|
8457
9708
|
}
|
|
8458
9709
|
|
|
8459
9710
|
if (currentEvent === 'tool_call') {
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
const tc = document.createElement('div');
|
|
8466
|
-
tc.className = 'chat-tool-call' + (data.error ? ' error' : '');
|
|
8467
|
-
const icon = document.createElement('span');
|
|
8468
|
-
icon.className = 'tool-icon';
|
|
8469
|
-
icon.textContent = '\u2699';
|
|
8470
|
-
tc.appendChild(icon);
|
|
8471
|
-
const nameSpan = document.createElement('span');
|
|
8472
|
-
nameSpan.className = 'tool-name';
|
|
8473
|
-
nameSpan.textContent = data.name;
|
|
8474
|
-
tc.appendChild(nameSpan);
|
|
8475
|
-
const timeSpan = document.createElement('span');
|
|
8476
|
-
timeSpan.className = 'tool-time';
|
|
8477
|
-
timeSpan.textContent = (data.timeMs || 0) + 'ms';
|
|
8478
|
-
tc.appendChild(timeSpan);
|
|
8479
|
-
if (data.error) {
|
|
8480
|
-
const errSpan = document.createElement('span');
|
|
8481
|
-
errSpan.className = 'tool-error';
|
|
8482
|
-
errSpan.textContent = data.error;
|
|
8483
|
-
tc.appendChild(errSpan);
|
|
9711
|
+
// Create thinking panel on first tool call
|
|
9712
|
+
if (!thinkingPanel) {
|
|
9713
|
+
typing.remove();
|
|
9714
|
+
thinkingPanel = createThinkingPanel();
|
|
9715
|
+
document.getElementById('chatMessages').appendChild(thinkingPanel.panel);
|
|
8484
9716
|
}
|
|
8485
|
-
|
|
8486
|
-
typing.textContent = 'Calling ' + data.name + '...';
|
|
9717
|
+
thinkingPanel.addStep(data);
|
|
8487
9718
|
document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
|
|
8488
9719
|
}
|
|
8489
9720
|
|
|
@@ -8498,6 +9729,8 @@ async function sendChatMessage() {
|
|
|
8498
9729
|
}
|
|
8499
9730
|
|
|
8500
9731
|
if (currentEvent === 'done') {
|
|
9732
|
+
// Finalize the thinking panel (collapse, show elapsed)
|
|
9733
|
+
if (thinkingPanel) thinkingPanel.finalize();
|
|
8501
9734
|
// Render accumulated text as markdown for assistant messages
|
|
8502
9735
|
if (assistantDiv && fullText) {
|
|
8503
9736
|
const contentEl = assistantDiv.querySelector('.chat-message-content');
|
|
@@ -8537,6 +9770,7 @@ async function sendChatMessage() {
|
|
|
8537
9770
|
|
|
8538
9771
|
} catch (err) {
|
|
8539
9772
|
if (typing.parentNode) typing.remove();
|
|
9773
|
+
if (thinkingPanel) thinkingPanel.finalize();
|
|
8540
9774
|
addChatMessage('system-msg', `Error: ${err.message}`);
|
|
8541
9775
|
} finally {
|
|
8542
9776
|
sendBtn.disabled = false;
|
|
@@ -8751,6 +9985,1981 @@ init();
|
|
|
8751
9985
|
</div>
|
|
8752
9986
|
</div>
|
|
8753
9987
|
|
|
9988
|
+
<!-- ========== WORKFLOW VISUALIZER JS ========== -->
|
|
9989
|
+
<script>
|
|
9990
|
+
// ── Workflow Visualizer ──
|
|
9991
|
+
|
|
9992
|
+
// escapeHtml is defined in the main IIFE scope and not accessible here — redeclare it
|
|
9993
|
+
function escapeHtml(str) {
|
|
9994
|
+
if (str == null) return '';
|
|
9995
|
+
const s = typeof str === 'string' ? str : String(str);
|
|
9996
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9997
|
+
}
|
|
9998
|
+
|
|
9999
|
+
const WF_NODE_META = {
|
|
10000
|
+
query: { icon: '\u{1F50D}', label: 'RAG Query', color: '#40E0FF', category: 'retrieval' },
|
|
10001
|
+
search: { icon: '\u{1F50E}', label: 'Vector Search', color: '#40E0FF', category: 'retrieval' },
|
|
10002
|
+
rerank: { icon: '\u{1F3C6}', label: 'Rerank', color: '#40E0FF', category: 'retrieval' },
|
|
10003
|
+
ingest: { icon: '\u{1F4E5}', label: 'Ingest', color: '#40E0FF', category: 'retrieval' },
|
|
10004
|
+
embed: { icon: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z', label: 'Embed', color: '#B388FF', category: 'embedding' },
|
|
10005
|
+
similarity: { icon: 'M8 3 4 7l4 4M4 7h16M16 21l4-4-4-4M20 17H4', label: 'Similarity', color: '#B388FF', category: 'embedding' },
|
|
10006
|
+
collections: { icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z', label: 'Collections', color: '#00D4AA', category: 'management' },
|
|
10007
|
+
models: { icon: 'M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6M9 14l2 2 4-4', label: 'Models', color: '#00D4AA', category: 'management' },
|
|
10008
|
+
estimate: { icon: 'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6', label: 'Cost Estimate', color: '#FFB74D', category: 'utility' },
|
|
10009
|
+
explain: { icon: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z', label: 'Explain', color: '#FFB74D', category: 'utility' },
|
|
10010
|
+
topics: { icon: 'M12 6V2M8 18H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-4M12 18v4M8 22h8', label: 'Topics', color: '#FFB74D', category: 'utility' },
|
|
10011
|
+
merge: { icon: 'M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 9v6M18 15l-6-6-6 6', label: 'Merge', color: '#90A4AE', category: 'control' },
|
|
10012
|
+
filter: { icon: 'M22 3H2l8 9.46V19l4 2v-8.54L22 3z', label: 'Filter', color: '#90A4AE', category: 'control' },
|
|
10013
|
+
transform: { icon: 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16', label: 'Transform', color: '#90A4AE', category: 'control' },
|
|
10014
|
+
generate: { icon: 'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z', label: 'Generate', color: '#69F0AE', category: 'generation' },
|
|
10015
|
+
query: { icon: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Query', color: '#64B5F6', category: 'retrieval' },
|
|
10016
|
+
rerank: { icon: 'M3 6h18M7 12h10M10 18h4', label: 'Rerank', color: '#CE93D8', category: 'retrieval' },
|
|
10017
|
+
search: { icon: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z', label: 'Search', color: '#64B5F6', category: 'retrieval' },
|
|
10018
|
+
ingest: { icon: 'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3', label: 'Ingest', color: '#4DB6AC', category: 'management' },
|
|
10019
|
+
};
|
|
10020
|
+
|
|
10021
|
+
// Fallback icon (gear) for unknown workflow node types
|
|
10022
|
+
const WF_FALLBACK_ICON = 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2zM12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0';
|
|
10023
|
+
|
|
10024
|
+
const WF_NODE_W = 180;
|
|
10025
|
+
const WF_NODE_H = 64;
|
|
10026
|
+
const WF_LAYER_GAP = 260;
|
|
10027
|
+
const WF_NODE_GAP = 100;
|
|
10028
|
+
const WF_PAD = 80;
|
|
10029
|
+
const WF_PORT_R = 5; // Port circle radius
|
|
10030
|
+
|
|
10031
|
+
let wfState = {
|
|
10032
|
+
workflows: [],
|
|
10033
|
+
examples: [],
|
|
10034
|
+
activeWorkflow: null,
|
|
10035
|
+
selectedNodeId: null,
|
|
10036
|
+
executionState: {},
|
|
10037
|
+
executionResults: {},
|
|
10038
|
+
nodePositions: {},
|
|
10039
|
+
layers: [],
|
|
10040
|
+
graph: {},
|
|
10041
|
+
zoom: 1,
|
|
10042
|
+
panX: 0,
|
|
10043
|
+
panY: 0,
|
|
10044
|
+
isPanning: false,
|
|
10045
|
+
panStart: { x: 0, y: 0 },
|
|
10046
|
+
executing: false,
|
|
10047
|
+
// Builder state
|
|
10048
|
+
builderMode: false,
|
|
10049
|
+
draggingEdge: null,
|
|
10050
|
+
dragNode: null,
|
|
10051
|
+
dirtyFlag: false,
|
|
10052
|
+
};
|
|
10053
|
+
|
|
10054
|
+
// ── Library ──
|
|
10055
|
+
async function wfLoadLibrary() {
|
|
10056
|
+
try {
|
|
10057
|
+
const [wfRes, exRes] = await Promise.all([
|
|
10058
|
+
fetch('/api/workflows'),
|
|
10059
|
+
fetch('/api/workflows/examples'),
|
|
10060
|
+
]);
|
|
10061
|
+
const wfData = await wfRes.json();
|
|
10062
|
+
const exData = await exRes.json();
|
|
10063
|
+
wfState.workflows = wfData.workflows || [];
|
|
10064
|
+
wfState.examples = exData.examples || [];
|
|
10065
|
+
wfRenderLibrary();
|
|
10066
|
+
} catch (err) {
|
|
10067
|
+
const list = document.getElementById('wfLibraryList');
|
|
10068
|
+
if (list) list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">Failed to load workflows</div>';
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10071
|
+
|
|
10072
|
+
function wfRenderLibrary() {
|
|
10073
|
+
const list = document.getElementById('wfLibraryList');
|
|
10074
|
+
if (!list) return;
|
|
10075
|
+
if (wfState.workflows.length === 0 && (!wfState.examples || wfState.examples.length === 0)) {
|
|
10076
|
+
list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
|
|
10077
|
+
return;
|
|
10078
|
+
}
|
|
10079
|
+
|
|
10080
|
+
// Built-in templates
|
|
10081
|
+
let html = wfState.workflows.map(w => {
|
|
10082
|
+
const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
10083
|
+
return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
|
|
10084
|
+
<div class="wf-library-item-name">${displayName}</div>
|
|
10085
|
+
<div class="wf-library-item-desc">${w.description || ''}</div>
|
|
10086
|
+
</div>`;
|
|
10087
|
+
}).join('');
|
|
10088
|
+
|
|
10089
|
+
// Collapsible examples section
|
|
10090
|
+
const examples = wfState.examples || [];
|
|
10091
|
+
if (examples.length > 0) {
|
|
10092
|
+
html += `<div class="wf-library-section">
|
|
10093
|
+
<button class="wf-library-section-toggle" onclick="wfToggleExamples(this)">
|
|
10094
|
+
<span class="arrow">▶</span> Examples (${examples.length})
|
|
10095
|
+
</button>
|
|
10096
|
+
<div class="wf-examples-content" style="display:none;">`;
|
|
10097
|
+
|
|
10098
|
+
const categories = ['Retrieval', 'RAG', 'Ingestion', 'Analysis', 'Other'];
|
|
10099
|
+
for (const cat of categories) {
|
|
10100
|
+
const items = examples.filter(e => e.category === cat);
|
|
10101
|
+
if (items.length === 0) continue;
|
|
10102
|
+
html += `<div class="wf-library-category">${cat}</div>`;
|
|
10103
|
+
html += items.map(w => {
|
|
10104
|
+
const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
10105
|
+
return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
|
|
10106
|
+
<div class="wf-library-item-name">${displayName}</div>
|
|
10107
|
+
<div class="wf-library-item-desc">${w.description || ''}</div>
|
|
10108
|
+
</div>`;
|
|
10109
|
+
}).join('');
|
|
10110
|
+
}
|
|
10111
|
+
html += '</div></div>';
|
|
10112
|
+
}
|
|
10113
|
+
|
|
10114
|
+
list.innerHTML = html;
|
|
10115
|
+
}
|
|
10116
|
+
|
|
10117
|
+
function wfToggleExamples(btn) {
|
|
10118
|
+
const content = btn.nextElementSibling;
|
|
10119
|
+
const isOpen = content.style.display !== 'none';
|
|
10120
|
+
content.style.display = isOpen ? 'none' : '';
|
|
10121
|
+
btn.classList.toggle('open', !isOpen);
|
|
10122
|
+
}
|
|
10123
|
+
|
|
10124
|
+
async function wfSelectWorkflow(name) {
|
|
10125
|
+
// Highlight in library
|
|
10126
|
+
document.querySelectorAll('.wf-library-item').forEach(el => {
|
|
10127
|
+
el.classList.toggle('active', el.dataset.wfName === name);
|
|
10128
|
+
});
|
|
10129
|
+
|
|
10130
|
+
try {
|
|
10131
|
+
const res = await fetch('/api/workflows/' + encodeURIComponent(name));
|
|
10132
|
+
const data = await res.json();
|
|
10133
|
+
wfState.activeWorkflow = data.definition;
|
|
10134
|
+
wfState.builderMode = false;
|
|
10135
|
+
wfState.selectedNodeId = null;
|
|
10136
|
+
wfState.executionState = {};
|
|
10137
|
+
wfState.executionResults = {};
|
|
10138
|
+
wfSetToolbarEnabled(true);
|
|
10139
|
+
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
10140
|
+
await wfRenderWorkflow(data.definition);
|
|
10141
|
+
wfSwitchLibTab('library');
|
|
10142
|
+
wfOpenInspector();
|
|
10143
|
+
wfUpdateInspector();
|
|
10144
|
+
} catch (err) {
|
|
10145
|
+
console.error('Failed to load workflow:', err);
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
|
|
10149
|
+
// ── Load workflow from file ──
|
|
10150
|
+
function wfLoadFromFile() {
|
|
10151
|
+
document.getElementById('wfFileInput').click();
|
|
10152
|
+
}
|
|
10153
|
+
|
|
10154
|
+
function wfHandleFileLoad(event) {
|
|
10155
|
+
const file = event.target.files[0];
|
|
10156
|
+
if (!file) return;
|
|
10157
|
+
const reader = new FileReader();
|
|
10158
|
+
reader.onload = async (e) => {
|
|
10159
|
+
try {
|
|
10160
|
+
const definition = JSON.parse(e.target.result);
|
|
10161
|
+
if (!definition.steps || !Array.isArray(definition.steps)) {
|
|
10162
|
+
alert('Invalid workflow: missing "steps" array');
|
|
10163
|
+
return;
|
|
10164
|
+
}
|
|
10165
|
+
// Validate via server
|
|
10166
|
+
const valRes = await fetch('/api/workflows/validate', {
|
|
10167
|
+
method: 'POST',
|
|
10168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10169
|
+
body: JSON.stringify({ definition }),
|
|
10170
|
+
});
|
|
10171
|
+
const valData = await valRes.json();
|
|
10172
|
+
if (!valData.valid) {
|
|
10173
|
+
alert('Workflow validation failed:\n' + (valData.errors || []).join('\n'));
|
|
10174
|
+
return;
|
|
10175
|
+
}
|
|
10176
|
+
// Deselect any library item
|
|
10177
|
+
document.querySelectorAll('.wf-library-item.active').forEach(el => el.classList.remove('active'));
|
|
10178
|
+
// Load the workflow
|
|
10179
|
+
wfState.activeWorkflow = definition;
|
|
10180
|
+
wfState.selectedNodeId = null;
|
|
10181
|
+
wfState.executionState = {};
|
|
10182
|
+
wfState.executionResults = {};
|
|
10183
|
+
wfSetToolbarEnabled(true);
|
|
10184
|
+
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
10185
|
+
wfHideExecStatus();
|
|
10186
|
+
await wfRenderWorkflow(definition);
|
|
10187
|
+
wfOpenInspector();
|
|
10188
|
+
wfUpdateInspector();
|
|
10189
|
+
} catch (err) {
|
|
10190
|
+
alert('Failed to parse workflow file: ' + err.message);
|
|
10191
|
+
}
|
|
10192
|
+
};
|
|
10193
|
+
reader.readAsText(file);
|
|
10194
|
+
// Reset file input so the same file can be re-loaded
|
|
10195
|
+
event.target.value = '';
|
|
10196
|
+
}
|
|
10197
|
+
|
|
10198
|
+
// ── DAG Layout + SVG Rendering ──
|
|
10199
|
+
async function wfRenderWorkflow(definition) {
|
|
10200
|
+
const svg = document.getElementById('wf-canvas');
|
|
10201
|
+
// Clear previous nodes and edges (keep defs)
|
|
10202
|
+
svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
|
|
10203
|
+
|
|
10204
|
+
// Get execution plan from server
|
|
10205
|
+
let layers, graph;
|
|
10206
|
+
try {
|
|
10207
|
+
const res = await fetch('/api/workflows/plan', {
|
|
10208
|
+
method: 'POST',
|
|
10209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10210
|
+
body: JSON.stringify({ definition }),
|
|
10211
|
+
});
|
|
10212
|
+
const data = await res.json();
|
|
10213
|
+
layers = data.layers;
|
|
10214
|
+
graph = data.graph;
|
|
10215
|
+
} catch (err) {
|
|
10216
|
+
console.error('Failed to get execution plan:', err);
|
|
10217
|
+
return;
|
|
10218
|
+
}
|
|
10219
|
+
|
|
10220
|
+
wfState.layers = layers;
|
|
10221
|
+
wfState.graph = graph;
|
|
10222
|
+
|
|
10223
|
+
// Build step lookup
|
|
10224
|
+
const stepMap = {};
|
|
10225
|
+
definition.steps.forEach(s => { stepMap[s.id] = s; });
|
|
10226
|
+
|
|
10227
|
+
// Calculate positions
|
|
10228
|
+
const positions = {};
|
|
10229
|
+
const maxLayerSize = Math.max(...layers.map(l => l.length));
|
|
10230
|
+
const totalW = layers.length * WF_LAYER_GAP;
|
|
10231
|
+
const totalH = maxLayerSize * (WF_NODE_H + WF_NODE_GAP);
|
|
10232
|
+
|
|
10233
|
+
layers.forEach((layer, li) => {
|
|
10234
|
+
const x = WF_PAD + li * WF_LAYER_GAP;
|
|
10235
|
+
const layerH = layer.length * WF_NODE_H + (layer.length - 1) * WF_NODE_GAP;
|
|
10236
|
+
const startY = WF_PAD + (totalH - layerH) / 2;
|
|
10237
|
+
layer.forEach((stepId, ni) => {
|
|
10238
|
+
positions[stepId] = {
|
|
10239
|
+
x,
|
|
10240
|
+
y: startY + ni * (WF_NODE_H + WF_NODE_GAP),
|
|
10241
|
+
};
|
|
10242
|
+
});
|
|
10243
|
+
});
|
|
10244
|
+
wfState.nodePositions = positions;
|
|
10245
|
+
|
|
10246
|
+
// Build port-visibility maps: which nodes have input deps, which have dependents
|
|
10247
|
+
const nodeHasDeps = {}; // node has incoming edges (show input port)
|
|
10248
|
+
const nodeHasDependents = {}; // node has outgoing edges (show output port)
|
|
10249
|
+
for (const [stepId, deps] of Object.entries(graph)) {
|
|
10250
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
10251
|
+
deps.forEach(rawDepId => {
|
|
10252
|
+
const depId = rawDepId.replace(/^!/, '');
|
|
10253
|
+
if (positions[depId] && positions[stepId]) {
|
|
10254
|
+
nodeHasDeps[stepId] = true;
|
|
10255
|
+
nodeHasDependents[depId] = true;
|
|
10256
|
+
}
|
|
10257
|
+
});
|
|
10258
|
+
}
|
|
10259
|
+
|
|
10260
|
+
// Draw edges first (behind nodes)
|
|
10261
|
+
const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10262
|
+
edgeGroup.classList.add('wf-edge-group');
|
|
10263
|
+
for (const [stepId, deps] of Object.entries(graph)) {
|
|
10264
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
10265
|
+
deps.forEach(rawDepId => {
|
|
10266
|
+
// Strip negation prefix (e.g., "!similarity_check" -> "similarity_check")
|
|
10267
|
+
const depId = rawDepId.replace(/^!/, '');
|
|
10268
|
+
if (positions[depId] && positions[stepId]) {
|
|
10269
|
+
const edge = wfDrawEdge(depId, stepId, positions);
|
|
10270
|
+
edgeGroup.appendChild(edge);
|
|
10271
|
+
}
|
|
10272
|
+
});
|
|
10273
|
+
}
|
|
10274
|
+
svg.appendChild(edgeGroup);
|
|
10275
|
+
|
|
10276
|
+
// Draw nodes
|
|
10277
|
+
for (const step of definition.steps) {
|
|
10278
|
+
const pos = positions[step.id];
|
|
10279
|
+
if (!pos) continue;
|
|
10280
|
+
const state = wfState.executionState[step.id] || 'idle';
|
|
10281
|
+
const hasDeps = !!nodeHasDeps[step.id];
|
|
10282
|
+
const hasDependents = !!nodeHasDependents[step.id];
|
|
10283
|
+
const nodeGroup = wfDrawNode(step, pos.x, pos.y, state, hasDeps, hasDependents);
|
|
10284
|
+
svg.appendChild(nodeGroup);
|
|
10285
|
+
}
|
|
10286
|
+
|
|
10287
|
+
// Set viewBox to fit
|
|
10288
|
+
wfFitToView();
|
|
10289
|
+
}
|
|
10290
|
+
|
|
10291
|
+
function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
|
|
10292
|
+
const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666', category: 'unknown' };
|
|
10293
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10294
|
+
g.classList.add('wf-node');
|
|
10295
|
+
if (state !== 'idle') g.classList.add('wf-node--' + state);
|
|
10296
|
+
if (wfState.selectedNodeId === step.id) g.classList.add('selected');
|
|
10297
|
+
g.dataset.stepId = step.id;
|
|
10298
|
+
g.setAttribute('transform', `translate(${x}, ${y})`);
|
|
10299
|
+
g.addEventListener('click', (e) => {
|
|
10300
|
+
e.stopPropagation();
|
|
10301
|
+
wfSelectNode(step.id);
|
|
10302
|
+
});
|
|
10303
|
+
|
|
10304
|
+
// Builder: node drag
|
|
10305
|
+
if (wfState.builderMode) {
|
|
10306
|
+
g.style.cursor = 'move';
|
|
10307
|
+
g.addEventListener('mousedown', (e) => {
|
|
10308
|
+
// Don't drag if clicking a port
|
|
10309
|
+
if (e.target.classList.contains('wf-port')) return;
|
|
10310
|
+
e.stopPropagation();
|
|
10311
|
+
const svg = document.getElementById('wf-canvas');
|
|
10312
|
+
const rect = svg.getBoundingClientRect();
|
|
10313
|
+
const startSvgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
|
|
10314
|
+
const startSvgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
|
|
10315
|
+
const pos = wfState.nodePositions[step.id];
|
|
10316
|
+
if (!pos) return;
|
|
10317
|
+
const offX = startSvgX - pos.x;
|
|
10318
|
+
const offY = startSvgY - pos.y;
|
|
10319
|
+
wfState.dragNode = step.id;
|
|
10320
|
+
|
|
10321
|
+
function onMove(ev) {
|
|
10322
|
+
const mx = (ev.clientX - rect.left) / wfState.zoom + wfState.panX;
|
|
10323
|
+
const my = (ev.clientY - rect.top) / wfState.zoom + wfState.panY;
|
|
10324
|
+
wfState.nodePositions[step.id] = { x: mx - offX, y: my - offY };
|
|
10325
|
+
wfRefreshNodes();
|
|
10326
|
+
}
|
|
10327
|
+
function onUp() {
|
|
10328
|
+
wfState.dragNode = null;
|
|
10329
|
+
document.removeEventListener('mousemove', onMove);
|
|
10330
|
+
document.removeEventListener('mouseup', onUp);
|
|
10331
|
+
}
|
|
10332
|
+
document.addEventListener('mousemove', onMove);
|
|
10333
|
+
document.addEventListener('mouseup', onUp);
|
|
10334
|
+
});
|
|
10335
|
+
}
|
|
10336
|
+
|
|
10337
|
+
// Background rect
|
|
10338
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
10339
|
+
rect.setAttribute('width', WF_NODE_W);
|
|
10340
|
+
rect.setAttribute('height', WF_NODE_H);
|
|
10341
|
+
rect.setAttribute('fill', meta.color);
|
|
10342
|
+
rect.setAttribute('stroke', meta.color);
|
|
10343
|
+
rect.setAttribute('opacity', '0.85');
|
|
10344
|
+
g.appendChild(rect);
|
|
10345
|
+
|
|
10346
|
+
// Input port (left side): show in builder mode always, otherwise only if has deps
|
|
10347
|
+
const showInPort = wfState.builderMode || hasDeps;
|
|
10348
|
+
if (showInPort) {
|
|
10349
|
+
const inPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
10350
|
+
inPort.classList.add('wf-port', 'wf-port-in');
|
|
10351
|
+
if (wfState.builderMode) inPort.classList.add('wf-port-builder');
|
|
10352
|
+
inPort.setAttribute('cx', 0);
|
|
10353
|
+
inPort.setAttribute('cy', WF_NODE_H / 2);
|
|
10354
|
+
inPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
|
|
10355
|
+
inPort.setAttribute('stroke', meta.color);
|
|
10356
|
+
inPort.setAttribute('stroke-width', '2');
|
|
10357
|
+
inPort.setAttribute('pointer-events', 'all');
|
|
10358
|
+
if (wfState.builderMode) {
|
|
10359
|
+
inPort.addEventListener('mouseup', () => {
|
|
10360
|
+
if (wfState.draggingEdge) wfEdgeDropOnInput(step.id);
|
|
10361
|
+
});
|
|
10362
|
+
}
|
|
10363
|
+
g.appendChild(inPort);
|
|
10364
|
+
}
|
|
10365
|
+
|
|
10366
|
+
// Output port (right side): show in builder mode always, otherwise only if has dependents
|
|
10367
|
+
const showOutPort = wfState.builderMode || hasDependents;
|
|
10368
|
+
if (showOutPort) {
|
|
10369
|
+
const outPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
10370
|
+
outPort.classList.add('wf-port', 'wf-port-out');
|
|
10371
|
+
if (wfState.builderMode) outPort.classList.add('wf-port-builder');
|
|
10372
|
+
outPort.setAttribute('cx', WF_NODE_W);
|
|
10373
|
+
outPort.setAttribute('cy', WF_NODE_H / 2);
|
|
10374
|
+
outPort.setAttribute('r', wfState.builderMode ? 7 : WF_PORT_R);
|
|
10375
|
+
outPort.setAttribute('fill', meta.color);
|
|
10376
|
+
outPort.setAttribute('stroke-width', '2');
|
|
10377
|
+
outPort.setAttribute('pointer-events', 'all');
|
|
10378
|
+
if (wfState.builderMode) {
|
|
10379
|
+
outPort.addEventListener('mousedown', (e) => {
|
|
10380
|
+
e.stopPropagation();
|
|
10381
|
+
const pos = wfState.nodePositions[step.id];
|
|
10382
|
+
if (!pos) return;
|
|
10383
|
+
wfEdgeDragStart(step.id, pos.x + WF_NODE_W, pos.y + WF_NODE_H / 2);
|
|
10384
|
+
});
|
|
10385
|
+
}
|
|
10386
|
+
g.appendChild(outPort);
|
|
10387
|
+
}
|
|
10388
|
+
|
|
10389
|
+
// Icon (Lucide SVG)
|
|
10390
|
+
const iconG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10391
|
+
iconG.classList.add('wf-node-icon');
|
|
10392
|
+
const iconSize = 18;
|
|
10393
|
+
const ix = 22 - iconSize / 2;
|
|
10394
|
+
const iy = WF_NODE_H / 2 - iconSize / 2;
|
|
10395
|
+
iconG.setAttribute('transform', `translate(${ix},${iy}) scale(${iconSize / 24})`);
|
|
10396
|
+
const fullD = meta.icon || WF_FALLBACK_ICON;
|
|
10397
|
+
const iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
10398
|
+
iconPath.setAttribute('d', fullD);
|
|
10399
|
+
iconPath.setAttribute('fill', 'none');
|
|
10400
|
+
iconPath.setAttribute('stroke', 'currentColor');
|
|
10401
|
+
iconPath.setAttribute('stroke-width', '1.75');
|
|
10402
|
+
iconPath.setAttribute('stroke-linecap', 'round');
|
|
10403
|
+
iconPath.setAttribute('stroke-linejoin', 'round');
|
|
10404
|
+
iconG.appendChild(iconPath);
|
|
10405
|
+
g.appendChild(iconG);
|
|
10406
|
+
|
|
10407
|
+
// Label (step name, truncated)
|
|
10408
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10409
|
+
label.classList.add('wf-node-label');
|
|
10410
|
+
label.setAttribute('x', WF_NODE_W / 2 + 10);
|
|
10411
|
+
label.setAttribute('y', WF_NODE_H / 2 - 7);
|
|
10412
|
+
const maxChars = 20;
|
|
10413
|
+
const displayName = (step.name || step.id).length > maxChars
|
|
10414
|
+
? (step.name || step.id).slice(0, maxChars - 2) + '..'
|
|
10415
|
+
: (step.name || step.id);
|
|
10416
|
+
label.textContent = displayName;
|
|
10417
|
+
g.appendChild(label);
|
|
10418
|
+
|
|
10419
|
+
// Tool badge
|
|
10420
|
+
const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10421
|
+
badge.classList.add('wf-node-badge');
|
|
10422
|
+
badge.setAttribute('x', WF_NODE_W / 2 + 10);
|
|
10423
|
+
badge.setAttribute('y', WF_NODE_H / 2 + 10);
|
|
10424
|
+
badge.textContent = meta.label;
|
|
10425
|
+
g.appendChild(badge);
|
|
10426
|
+
|
|
10427
|
+
// Condition indicator
|
|
10428
|
+
if (step.condition) {
|
|
10429
|
+
const cond = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10430
|
+
cond.classList.add('wf-node-condition');
|
|
10431
|
+
cond.setAttribute('x', WF_NODE_W - 8);
|
|
10432
|
+
cond.setAttribute('y', 14);
|
|
10433
|
+
cond.textContent = '\u26A1';
|
|
10434
|
+
g.appendChild(cond);
|
|
10435
|
+
}
|
|
10436
|
+
|
|
10437
|
+
// Status overlay (for execution)
|
|
10438
|
+
if (state === 'completed') {
|
|
10439
|
+
const check = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10440
|
+
check.classList.add('wf-node-status');
|
|
10441
|
+
check.setAttribute('x', WF_NODE_W - 18);
|
|
10442
|
+
check.setAttribute('y', WF_NODE_H / 2);
|
|
10443
|
+
check.textContent = '\u2713';
|
|
10444
|
+
g.appendChild(check);
|
|
10445
|
+
} else if (state === 'error') {
|
|
10446
|
+
const errIcon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10447
|
+
errIcon.classList.add('wf-node-status');
|
|
10448
|
+
errIcon.setAttribute('x', WF_NODE_W - 18);
|
|
10449
|
+
errIcon.setAttribute('y', WF_NODE_H / 2);
|
|
10450
|
+
errIcon.textContent = '\u2717';
|
|
10451
|
+
g.appendChild(errIcon);
|
|
10452
|
+
} else if (state === 'skipped') {
|
|
10453
|
+
const skip = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10454
|
+
skip.classList.add('wf-node-status');
|
|
10455
|
+
skip.setAttribute('x', WF_NODE_W - 18);
|
|
10456
|
+
skip.setAttribute('y', WF_NODE_H / 2);
|
|
10457
|
+
skip.textContent = '\u2500';
|
|
10458
|
+
g.appendChild(skip);
|
|
10459
|
+
}
|
|
10460
|
+
|
|
10461
|
+
// Time badge (if completed)
|
|
10462
|
+
const result = wfState.executionResults[step.id];
|
|
10463
|
+
if (result && result.timeMs !== undefined) {
|
|
10464
|
+
const time = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10465
|
+
time.classList.add('wf-node-time');
|
|
10466
|
+
time.setAttribute('x', WF_NODE_W / 2);
|
|
10467
|
+
time.setAttribute('y', WF_NODE_H + 16);
|
|
10468
|
+
time.textContent = result.timeMs + 'ms';
|
|
10469
|
+
g.appendChild(time);
|
|
10470
|
+
}
|
|
10471
|
+
|
|
10472
|
+
return g;
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10475
|
+
function wfDrawEdge(fromId, toId, positions) {
|
|
10476
|
+
const from = positions[fromId];
|
|
10477
|
+
const to = positions[toId];
|
|
10478
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10479
|
+
g.classList.add('wf-edge-group-item');
|
|
10480
|
+
g.dataset.from = fromId;
|
|
10481
|
+
g.dataset.to = toId;
|
|
10482
|
+
|
|
10483
|
+
// Port positions: output port on right side of source, input port on left side of target
|
|
10484
|
+
const x1 = from.x + WF_NODE_W;
|
|
10485
|
+
const y1 = from.y + WF_NODE_H / 2;
|
|
10486
|
+
const x2 = to.x;
|
|
10487
|
+
const y2 = to.y + WF_NODE_H / 2;
|
|
10488
|
+
|
|
10489
|
+
// Detect backward edges (target is same column or to the left)
|
|
10490
|
+
const isBackward = x2 <= x1;
|
|
10491
|
+
|
|
10492
|
+
let d;
|
|
10493
|
+
if (isBackward) {
|
|
10494
|
+
// Route backward edges: go down from output, loop under/over, come in from left of target
|
|
10495
|
+
const midY = Math.max(from.y + WF_NODE_H, to.y + WF_NODE_H) + 40;
|
|
10496
|
+
d = `M ${x1} ${y1} C ${x1 + 50} ${y1}, ${x1 + 50} ${midY}, ${(x1 + x2) / 2} ${midY} C ${x2 - 50} ${midY}, ${x2 - 50} ${y2}, ${x2} ${y2}`;
|
|
10497
|
+
} else {
|
|
10498
|
+
// Normal left-to-right bezier
|
|
10499
|
+
const dx = Math.abs(x2 - x1);
|
|
10500
|
+
const tension = Math.max(dx * 0.4, 40);
|
|
10501
|
+
d = `M ${x1} ${y1} C ${x1 + tension} ${y1}, ${x2 - tension} ${y2}, ${x2} ${y2}`;
|
|
10502
|
+
}
|
|
10503
|
+
|
|
10504
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
10505
|
+
path.classList.add('wf-edge');
|
|
10506
|
+
if (isBackward) path.classList.add('wf-edge--backward');
|
|
10507
|
+
path.setAttribute('d', d);
|
|
10508
|
+
|
|
10509
|
+
// Set state-based classes
|
|
10510
|
+
const fromState = wfState.executionState[fromId];
|
|
10511
|
+
const toState = wfState.executionState[toId];
|
|
10512
|
+
if (toState === 'running') path.classList.add('wf-edge--active');
|
|
10513
|
+
else if (fromState === 'completed' && (toState === 'completed' || toState === 'skipped')) path.classList.add('wf-edge--complete');
|
|
10514
|
+
|
|
10515
|
+
g.appendChild(path);
|
|
10516
|
+
return g;
|
|
10517
|
+
}
|
|
10518
|
+
|
|
10519
|
+
// ── Inspector Toggle ──
|
|
10520
|
+
function wfToggleInspector() {
|
|
10521
|
+
const panel = document.getElementById('wfInspector');
|
|
10522
|
+
const btn = document.getElementById('wfInspectorToggle');
|
|
10523
|
+
if (!panel) return;
|
|
10524
|
+
panel.classList.toggle('collapsed');
|
|
10525
|
+
if (btn) btn.innerHTML = panel.classList.contains('collapsed') ? '‹' : '›';
|
|
10526
|
+
}
|
|
10527
|
+
|
|
10528
|
+
function wfOpenInspector() {
|
|
10529
|
+
const panel = document.getElementById('wfInspector');
|
|
10530
|
+
const btn = document.getElementById('wfInspectorToggle');
|
|
10531
|
+
if (!panel || !panel.classList.contains('collapsed')) return;
|
|
10532
|
+
panel.classList.remove('collapsed');
|
|
10533
|
+
if (btn) btn.innerHTML = '›';
|
|
10534
|
+
}
|
|
10535
|
+
|
|
10536
|
+
// ── Node Selection ──
|
|
10537
|
+
function wfSelectNode(stepId) {
|
|
10538
|
+
wfState.selectedNodeId = stepId;
|
|
10539
|
+
// Update visual selection
|
|
10540
|
+
document.querySelectorAll('.wf-node').forEach(n => {
|
|
10541
|
+
n.classList.toggle('selected', n.dataset.stepId === stepId);
|
|
10542
|
+
});
|
|
10543
|
+
wfOpenInspector();
|
|
10544
|
+
wfUpdateInspector();
|
|
10545
|
+
}
|
|
10546
|
+
|
|
10547
|
+
function wfDeselectNode() {
|
|
10548
|
+
wfState.selectedNodeId = null;
|
|
10549
|
+
document.querySelectorAll('.wf-node.selected').forEach(n => n.classList.remove('selected'));
|
|
10550
|
+
wfUpdateInspector();
|
|
10551
|
+
}
|
|
10552
|
+
|
|
10553
|
+
// ── Inspector ──
|
|
10554
|
+
function wfUpdateInspector() {
|
|
10555
|
+
const body = document.getElementById('wfInspectorBody');
|
|
10556
|
+
const header = document.getElementById('wfInspectorHeader');
|
|
10557
|
+
if (!body || !header) return;
|
|
10558
|
+
|
|
10559
|
+
const def = wfState.activeWorkflow;
|
|
10560
|
+
if (!def) {
|
|
10561
|
+
header.textContent = 'Inspector';
|
|
10562
|
+
body.innerHTML = '<div class="wf-inspector-empty">Select a workflow to view details</div>';
|
|
10563
|
+
return;
|
|
10564
|
+
}
|
|
10565
|
+
|
|
10566
|
+
try {
|
|
10567
|
+
if (!wfState.selectedNodeId) {
|
|
10568
|
+
// Show workflow-level info
|
|
10569
|
+
header.textContent = def.name || 'Workflow';
|
|
10570
|
+
let html = '';
|
|
10571
|
+
|
|
10572
|
+
if (wfState.builderMode) {
|
|
10573
|
+
// Builder: editable workflow fields
|
|
10574
|
+
html += `<div class="wf-inspector-section">
|
|
10575
|
+
<div class="wf-inspector-section-title">Name</div>
|
|
10576
|
+
<input class="wf-inspector-input" value="${escapeHtml(def.name || '')}" onchange="wfEditWorkflowField('name', this.value); document.getElementById('wfInspectorHeader').textContent = this.value || 'Workflow';">
|
|
10577
|
+
</div>`;
|
|
10578
|
+
html += `<div class="wf-inspector-section">
|
|
10579
|
+
<div class="wf-inspector-section-title">Description</div>
|
|
10580
|
+
<textarea class="wf-inspector-input" rows="2" style="resize:vertical;" onchange="wfEditWorkflowField('description', this.value)">${escapeHtml(def.description || '')}</textarea>
|
|
10581
|
+
</div>`;
|
|
10582
|
+
html += `<div class="wf-inspector-section">
|
|
10583
|
+
<div class="wf-inspector-section-title">Steps</div>
|
|
10584
|
+
<div style="font-size:12px;color:var(--text);">${def.steps.length} step${def.steps.length !== 1 ? 's' : ''}</div>
|
|
10585
|
+
</div>`;
|
|
10586
|
+
if (def.steps.length > 0) {
|
|
10587
|
+
html += `<div class="wf-inspector-section">
|
|
10588
|
+
<div class="wf-inspector-section-title">Output Mapping</div>
|
|
10589
|
+
<textarea class="wf-inspector-input" rows="3" style="resize:vertical;font-family:monospace;font-size:11px;" onchange="try { wfEditWorkflowField('output', JSON.parse(this.value)); } catch(e) {}">${escapeHtml(JSON.stringify(def.output || {}, null, 2))}</textarea>
|
|
10590
|
+
</div>`;
|
|
10591
|
+
}
|
|
10592
|
+
html += `<div class="wf-inspector-section" style="margin-top:8px;">
|
|
10593
|
+
<div style="font-size:10px;color:var(--text-muted);">Add steps from the Palette tab, then drag between ports to connect them.</div>
|
|
10594
|
+
</div>`;
|
|
10595
|
+
} else {
|
|
10596
|
+
// Read-only: Description
|
|
10597
|
+
if (def.description) {
|
|
10598
|
+
html += `<div class="wf-inspector-section">
|
|
10599
|
+
<div class="wf-inspector-section-title">Description</div>
|
|
10600
|
+
<div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
|
|
10601
|
+
</div>`;
|
|
10602
|
+
}
|
|
10603
|
+
|
|
10604
|
+
// Read-only: Inputs
|
|
10605
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
10606
|
+
html += '<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>';
|
|
10607
|
+
for (const [key, spec] of Object.entries(def.inputs)) {
|
|
10608
|
+
const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
|
|
10609
|
+
const defVal = spec.default !== undefined ? ` (default: ${spec.default})` : '';
|
|
10610
|
+
html += `<div style="margin-bottom:8px;">
|
|
10611
|
+
<div style="font-size:12px;font-weight:600;color:var(--text);">${escapeHtml(key)}${req}</div>
|
|
10612
|
+
<div style="font-size:11px;color:var(--text-muted);">${escapeHtml(spec.description || spec.type || '')}${defVal}</div>
|
|
10613
|
+
<input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? spec.default : ''}">
|
|
10614
|
+
</div>`;
|
|
10615
|
+
}
|
|
10616
|
+
html += '</div>';
|
|
10617
|
+
}
|
|
10618
|
+
|
|
10619
|
+
// Steps summary
|
|
10620
|
+
html += `<div class="wf-inspector-section">
|
|
10621
|
+
<div class="wf-inspector-section-title">Steps</div>
|
|
10622
|
+
<div style="font-size:12px;color:var(--text);">${def.steps.length} step${def.steps.length !== 1 ? 's' : ''}${wfState.layers ? ' in ' + wfState.layers.length + ' layer' + (wfState.layers.length !== 1 ? 's' : '') : ''}</div>
|
|
10623
|
+
</div>`;
|
|
10624
|
+
|
|
10625
|
+
// Output mapping
|
|
10626
|
+
if (def.output) {
|
|
10627
|
+
html += `<div class="wf-inspector-section">
|
|
10628
|
+
<div class="wf-inspector-section-title">Output</div>
|
|
10629
|
+
<div class="wf-inspector-code">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>
|
|
10630
|
+
</div>`;
|
|
10631
|
+
}
|
|
10632
|
+
}
|
|
10633
|
+
|
|
10634
|
+
// Execution result (shown in both modes)
|
|
10635
|
+
if (wfState.executionResults._done) {
|
|
10636
|
+
const r = wfState.executionResults._done;
|
|
10637
|
+
const doneJson = JSON.stringify(r.output, null, 2);
|
|
10638
|
+
html += `<div class="wf-inspector-section">
|
|
10639
|
+
<div class="wf-inspector-section-title">Result</div>
|
|
10640
|
+
<div class="wf-inspector-result success">
|
|
10641
|
+
<div style="font-weight:600;margin-bottom:4px;">Completed in ${r.totalTimeMs}ms</div>
|
|
10642
|
+
<div class="wf-inspector-code" style="max-height:150px;overflow:auto;">${escapeHtml(doneJson)}</div>
|
|
10643
|
+
</div>
|
|
10644
|
+
<button class="wf-output-expand-btn" data-expand-step="_done">⤢ Expand</button>
|
|
10645
|
+
</div>`;
|
|
10646
|
+
}
|
|
10647
|
+
|
|
10648
|
+
body.innerHTML = html;
|
|
10649
|
+
wfBindExpandButtons(body);
|
|
10650
|
+
return;
|
|
10651
|
+
}
|
|
10652
|
+
|
|
10653
|
+
// Show step details
|
|
10654
|
+
const step = def.steps.find(s => s.id === wfState.selectedNodeId);
|
|
10655
|
+
if (!step) {
|
|
10656
|
+
body.innerHTML = '<div class="wf-inspector-empty">Step not found: ' + escapeHtml(wfState.selectedNodeId) + '</div>';
|
|
10657
|
+
return;
|
|
10658
|
+
}
|
|
10659
|
+
|
|
10660
|
+
const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
|
|
10661
|
+
header.textContent = step.name || step.id;
|
|
10662
|
+
|
|
10663
|
+
let html = '';
|
|
10664
|
+
|
|
10665
|
+
// Tool badge (always shown)
|
|
10666
|
+
html += `<div class="wf-inspector-section">
|
|
10667
|
+
<div class="wf-inspector-section-title">Tool</div>
|
|
10668
|
+
<span class="wf-tool-badge" style="background:${meta.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><path d="${meta.icon || WF_FALLBACK_ICON}"/></svg>${meta.label}</span>
|
|
10669
|
+
</div>`;
|
|
10670
|
+
|
|
10671
|
+
if (wfState.builderMode) {
|
|
10672
|
+
// Builder: editable step fields
|
|
10673
|
+
const sid = escapeHtml(step.id);
|
|
10674
|
+
html += `<div class="wf-inspector-section">
|
|
10675
|
+
<div class="wf-inspector-section-title">Step ID</div>
|
|
10676
|
+
<input class="wf-inspector-input" value="${sid}" style="font-family:monospace;" onchange="wfEditStepId('${sid}', this.value)">
|
|
10677
|
+
</div>`;
|
|
10678
|
+
html += `<div class="wf-inspector-section">
|
|
10679
|
+
<div class="wf-inspector-section-title">Name</div>
|
|
10680
|
+
<input class="wf-inspector-input" value="${escapeHtml(step.name || '')}" onchange="wfEditStepField('${sid}', 'name', this.value)">
|
|
10681
|
+
</div>`;
|
|
10682
|
+
|
|
10683
|
+
// Inputs from WF_INPUT_DEFS
|
|
10684
|
+
const inputDefs = WF_INPUT_DEFS[step.tool] || [];
|
|
10685
|
+
if (inputDefs.length > 0) {
|
|
10686
|
+
html += `<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>`;
|
|
10687
|
+
for (const d of inputDefs) {
|
|
10688
|
+
const val = step.inputs?.[d.key] ?? '';
|
|
10689
|
+
const display = typeof val === 'string' ? val : JSON.stringify(val);
|
|
10690
|
+
const req = d.required ? ' <span style="color:#e74c3c;font-size:10px;">required</span>' : '';
|
|
10691
|
+
html += `<div style="margin-bottom:8px;">
|
|
10692
|
+
<div style="font-size:11px;font-weight:600;color:var(--text);">${escapeHtml(d.key)}${req}</div>`;
|
|
10693
|
+
|
|
10694
|
+
if (d.type === 'textarea' || d.type === 'json') {
|
|
10695
|
+
html += `<textarea class="wf-inspector-input" rows="2" style="resize:vertical;font-family:monospace;font-size:11px;" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">${escapeHtml(display)}</textarea>`;
|
|
10696
|
+
} else if (d.type === 'select' && d.options) {
|
|
10697
|
+
html += `<select class="wf-inspector-input" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">
|
|
10698
|
+
<option value="">--</option>`;
|
|
10699
|
+
for (const opt of d.options) {
|
|
10700
|
+
html += `<option value="${escapeHtml(opt)}" ${val === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`;
|
|
10701
|
+
}
|
|
10702
|
+
html += `</select>`;
|
|
10703
|
+
} else if (d.type === 'number') {
|
|
10704
|
+
html += `<input class="wf-inspector-input" type="number" value="${escapeHtml(String(val))}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
|
|
10705
|
+
} else {
|
|
10706
|
+
html += `<input class="wf-inspector-input" value="${escapeHtml(display)}" placeholder="${escapeHtml(d.placeholder || '')}" onchange="wfEditStepInput('${sid}','${d.key}',this.value)">`;
|
|
10707
|
+
}
|
|
10708
|
+
html += `</div>`;
|
|
10709
|
+
}
|
|
10710
|
+
html += '</div>';
|
|
10711
|
+
}
|
|
10712
|
+
|
|
10713
|
+
// Condition
|
|
10714
|
+
html += `<div class="wf-inspector-section">
|
|
10715
|
+
<div class="wf-inspector-section-title">Condition <span style="font-size:10px;color:var(--text-muted)">(optional)</span></div>
|
|
10716
|
+
<input class="wf-inspector-input" value="${escapeHtml(step.condition || '')}" placeholder="e.g. results.length > 0" onchange="wfEditStepField('${sid}', 'condition', this.value || undefined)">
|
|
10717
|
+
</div>`;
|
|
10718
|
+
|
|
10719
|
+
// continueOnError toggle
|
|
10720
|
+
html += `<div class="wf-inspector-section" style="display:flex;align-items:center;gap:8px;">
|
|
10721
|
+
<input type="checkbox" id="wf-coe-${sid}" ${step.continueOnError ? 'checked' : ''} onchange="wfEditStepField('${sid}', 'continueOnError', this.checked)">
|
|
10722
|
+
<label for="wf-coe-${sid}" style="font-size:11px;color:var(--text);cursor:pointer;">Continue on error</label>
|
|
10723
|
+
</div>`;
|
|
10724
|
+
|
|
10725
|
+
// Delete button
|
|
10726
|
+
html += `<button class="wf-inspector-delete-btn" onclick="if(confirm('Delete step ${sid}?')) wfDeleteStep('${sid}')">Delete Step</button>`;
|
|
10727
|
+
} else {
|
|
10728
|
+
// Read-only step view
|
|
10729
|
+
html += `<div class="wf-inspector-section">
|
|
10730
|
+
<div class="wf-inspector-section-title">ID</div>
|
|
10731
|
+
<div style="font-size:12px;color:var(--text);font-family:monospace;">${escapeHtml(step.id)}</div>
|
|
10732
|
+
</div>`;
|
|
10733
|
+
|
|
10734
|
+
if (step.inputs) {
|
|
10735
|
+
html += `<div class="wf-inspector-section">
|
|
10736
|
+
<div class="wf-inspector-section-title">Inputs</div>`;
|
|
10737
|
+
for (const [key, val] of Object.entries(step.inputs)) {
|
|
10738
|
+
const display = typeof val === 'string' ? val : JSON.stringify(val);
|
|
10739
|
+
html += `<div class="wf-inspector-field">
|
|
10740
|
+
<span class="wf-inspector-field-label">${escapeHtml(key)}</span>
|
|
10741
|
+
<span class="wf-inspector-field-value" style="font-family:monospace;font-size:11px;">${escapeHtml(display)}</span>
|
|
10742
|
+
</div>`;
|
|
10743
|
+
}
|
|
10744
|
+
html += '</div>';
|
|
10745
|
+
}
|
|
10746
|
+
|
|
10747
|
+
if (step.condition) {
|
|
10748
|
+
html += `<div class="wf-inspector-section">
|
|
10749
|
+
<div class="wf-inspector-section-title">Condition \u26A1</div>
|
|
10750
|
+
<div class="wf-inspector-code">${escapeHtml(step.condition)}</div>
|
|
10751
|
+
</div>`;
|
|
10752
|
+
}
|
|
10753
|
+
|
|
10754
|
+
if (step.forEach) {
|
|
10755
|
+
html += `<div class="wf-inspector-section">
|
|
10756
|
+
<div class="wf-inspector-section-title">ForEach</div>
|
|
10757
|
+
<div class="wf-inspector-code">${escapeHtml(JSON.stringify(step.forEach, null, 2))}</div>
|
|
10758
|
+
</div>`;
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
|
|
10762
|
+
// Execution result for this step (shown in both modes)
|
|
10763
|
+
const result = wfState.executionResults[step.id];
|
|
10764
|
+
const state = wfState.executionState[step.id];
|
|
10765
|
+
if (state === 'completed' && result) {
|
|
10766
|
+
const outputJson = result.output ? JSON.stringify(result.output, null, 2) : '';
|
|
10767
|
+
const stepTitle = step.name || step.id;
|
|
10768
|
+
html += `<div class="wf-inspector-section">
|
|
10769
|
+
<div class="wf-inspector-section-title">Result</div>
|
|
10770
|
+
<div class="wf-inspector-result success">
|
|
10771
|
+
<div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">${result.timeMs}ms${result.summary ? ', ' + result.summary : ''}</div>
|
|
10772
|
+
${outputJson ? '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(outputJson) + '</div>' : ''}
|
|
10773
|
+
</div>
|
|
10774
|
+
${outputJson ? '<button class="wf-output-expand-btn" data-expand-step="' + escapeHtml(step.id) + '">⤢ Expand</button>' : ''}
|
|
10775
|
+
</div>`;
|
|
10776
|
+
} else if (state === 'error' && result) {
|
|
10777
|
+
html += `<div class="wf-inspector-section">
|
|
10778
|
+
<div class="wf-inspector-section-title">Error</div>
|
|
10779
|
+
<div class="wf-inspector-result error">
|
|
10780
|
+
<div style="font-size:12px;margin-bottom:4px;">${escapeHtml(result.error || 'Unknown error')}</div>
|
|
10781
|
+
</div>
|
|
10782
|
+
</div>`;
|
|
10783
|
+
} else if (state === 'skipped' && result) {
|
|
10784
|
+
html += `<div class="wf-inspector-section">
|
|
10785
|
+
<div class="wf-inspector-section-title">Skipped</div>
|
|
10786
|
+
<div style="font-size:12px;color:var(--text-muted);">${escapeHtml(result.reason || 'Condition not met')}</div>
|
|
10787
|
+
</div>`;
|
|
10788
|
+
}
|
|
10789
|
+
|
|
10790
|
+
body.innerHTML = html;
|
|
10791
|
+
wfBindExpandButtons(body);
|
|
10792
|
+
} catch (err) {
|
|
10793
|
+
console.error('Inspector render error:', err);
|
|
10794
|
+
body.innerHTML = '<div class="wf-inspector-empty" style="color:#e74c3c;">Error rendering inspector: ' + escapeHtml(err.message) + '</div>';
|
|
10795
|
+
}
|
|
10796
|
+
}
|
|
10797
|
+
|
|
10798
|
+
function wfBindExpandButtons(container) {
|
|
10799
|
+
container.querySelectorAll('.wf-output-expand-btn[data-expand-step]').forEach(btn => {
|
|
10800
|
+
btn.addEventListener('click', () => {
|
|
10801
|
+
const stepId = btn.dataset.expandStep;
|
|
10802
|
+
const result = wfState.executionResults[stepId];
|
|
10803
|
+
if (!result) return;
|
|
10804
|
+
let title, content;
|
|
10805
|
+
if (stepId === '_done') {
|
|
10806
|
+
title = 'Workflow Output';
|
|
10807
|
+
content = JSON.stringify(result.output, null, 2);
|
|
10808
|
+
} else {
|
|
10809
|
+
const def = wfState.activeWorkflow;
|
|
10810
|
+
const step = def ? def.steps.find(s => s.id === stepId) : null;
|
|
10811
|
+
title = (step ? step.name || step.id : stepId) + ' Output';
|
|
10812
|
+
content = JSON.stringify(result.output, null, 2);
|
|
10813
|
+
}
|
|
10814
|
+
wfOpenOutputModal(title, content);
|
|
10815
|
+
});
|
|
10816
|
+
});
|
|
10817
|
+
}
|
|
10818
|
+
|
|
10819
|
+
// escapeHtml is already defined globally — reuse it
|
|
10820
|
+
|
|
10821
|
+
// ── Toolbar helpers ──
|
|
10822
|
+
function wfSetToolbarEnabled(enabled) {
|
|
10823
|
+
document.getElementById('wfRunBtn').disabled = !enabled;
|
|
10824
|
+
document.getElementById('wfDryRunBtn').disabled = !enabled;
|
|
10825
|
+
document.getElementById('wfExportBtn').disabled = !enabled;
|
|
10826
|
+
document.getElementById('wfEditBtn').disabled = !enabled;
|
|
10827
|
+
}
|
|
10828
|
+
|
|
10829
|
+
// ── Export workflow JSON ──
|
|
10830
|
+
function wfExportJson() {
|
|
10831
|
+
const def = wfState.activeWorkflow;
|
|
10832
|
+
if (!def) return;
|
|
10833
|
+
const json = JSON.stringify(def, null, 2);
|
|
10834
|
+
const name = (def.name || 'workflow').replace(/\s+/g, '-').toLowerCase();
|
|
10835
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
10836
|
+
const url = URL.createObjectURL(blob);
|
|
10837
|
+
const a = document.createElement('a');
|
|
10838
|
+
a.href = url;
|
|
10839
|
+
a.download = name + '.vai-workflow.json';
|
|
10840
|
+
document.body.appendChild(a);
|
|
10841
|
+
a.click();
|
|
10842
|
+
document.body.removeChild(a);
|
|
10843
|
+
URL.revokeObjectURL(url);
|
|
10844
|
+
}
|
|
10845
|
+
|
|
10846
|
+
// ── Dry Run ──
|
|
10847
|
+
function wfDryRun() {
|
|
10848
|
+
const def = wfState.activeWorkflow;
|
|
10849
|
+
if (!def || !wfState.layers) return;
|
|
10850
|
+
|
|
10851
|
+
const layers = wfState.layers;
|
|
10852
|
+
const stepMap = {};
|
|
10853
|
+
def.steps.forEach(s => { stepMap[s.id] = s; });
|
|
10854
|
+
|
|
10855
|
+
// Build layer HTML
|
|
10856
|
+
let layersHtml = '';
|
|
10857
|
+
let totalSteps = 0;
|
|
10858
|
+
let conditionalSteps = 0;
|
|
10859
|
+
|
|
10860
|
+
layers.forEach((layer, i) => {
|
|
10861
|
+
const parallel = layer.length > 1 ? ' (parallel)' : '';
|
|
10862
|
+
layersHtml += '<div class="wf-dryrun-layer">';
|
|
10863
|
+
layersHtml += '<div class="wf-dryrun-layer-title">';
|
|
10864
|
+
layersHtml += '<span class="wf-dryrun-layer-badge">' + (i + 1) + '</span>';
|
|
10865
|
+
layersHtml += ' Layer ' + (i + 1) + parallel;
|
|
10866
|
+
layersHtml += '</div>';
|
|
10867
|
+
|
|
10868
|
+
layer.forEach(stepId => {
|
|
10869
|
+
const step = stepMap[stepId];
|
|
10870
|
+
if (!step) return;
|
|
10871
|
+
totalSteps++;
|
|
10872
|
+
const meta = WF_NODE_META[step.tool] || { icon: WF_FALLBACK_ICON, label: step.tool, color: '#666' };
|
|
10873
|
+
layersHtml += '<div class="wf-dryrun-step">';
|
|
10874
|
+
layersHtml += '<span class="wf-dryrun-step-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="' + (meta.icon || WF_FALLBACK_ICON) + '"/></svg></span>';
|
|
10875
|
+
layersHtml += '<div class="wf-dryrun-step-info">';
|
|
10876
|
+
layersHtml += '<div class="wf-dryrun-step-name">' + escapeHtml(step.name || step.id) + '</div>';
|
|
10877
|
+
layersHtml += '<div class="wf-dryrun-step-tool">' + escapeHtml(meta.label) + ' (' + escapeHtml(step.id) + ')</div>';
|
|
10878
|
+
if (step.condition) {
|
|
10879
|
+
conditionalSteps++;
|
|
10880
|
+
layersHtml += '<div class="wf-dryrun-step-cond">\u26A1 ' + escapeHtml(step.condition) + '</div>';
|
|
10881
|
+
}
|
|
10882
|
+
layersHtml += '</div></div>';
|
|
10883
|
+
});
|
|
10884
|
+
|
|
10885
|
+
layersHtml += '</div>';
|
|
10886
|
+
});
|
|
10887
|
+
|
|
10888
|
+
// Summary stats
|
|
10889
|
+
const parallelLayers = layers.filter(l => l.length > 1).length;
|
|
10890
|
+
let summaryHtml = '<div class="wf-dryrun-summary">';
|
|
10891
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + totalSteps + '</div><div class="wf-dryrun-stat-label">Steps</div></div>';
|
|
10892
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + layers.length + '</div><div class="wf-dryrun-stat-label">Layers</div></div>';
|
|
10893
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + parallelLayers + '</div><div class="wf-dryrun-stat-label">Parallel</div></div>';
|
|
10894
|
+
if (conditionalSteps > 0) {
|
|
10895
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + conditionalSteps + '</div><div class="wf-dryrun-stat-label">Conditional</div></div>';
|
|
10896
|
+
}
|
|
10897
|
+
summaryHtml += '</div>';
|
|
10898
|
+
|
|
10899
|
+
// Inputs summary
|
|
10900
|
+
let inputsHtml = '';
|
|
10901
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
10902
|
+
inputsHtml += '<div style="margin-bottom:12px;font-size:12px;">';
|
|
10903
|
+
inputsHtml += '<div style="font-weight:700;font-size:11px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text-muted);margin-bottom:6px;">Required Inputs</div>';
|
|
10904
|
+
for (const [key, spec] of Object.entries(def.inputs)) {
|
|
10905
|
+
const req = spec.required ? ' *' : '';
|
|
10906
|
+
inputsHtml += '<div style="margin-bottom:2px;"><span style="font-weight:600;color:var(--text);">' + escapeHtml(key) + req + '</span>';
|
|
10907
|
+
inputsHtml += ' <span style="color:var(--text-muted);">(' + escapeHtml(spec.type || 'string') + ')</span></div>';
|
|
10908
|
+
}
|
|
10909
|
+
inputsHtml += '</div>';
|
|
10910
|
+
}
|
|
10911
|
+
|
|
10912
|
+
// Create overlay
|
|
10913
|
+
const canvasArea = document.querySelector('.wf-canvas-area');
|
|
10914
|
+
// Remove any existing overlay
|
|
10915
|
+
canvasArea.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
|
|
10916
|
+
|
|
10917
|
+
const overlay = document.createElement('div');
|
|
10918
|
+
overlay.className = 'wf-dryrun-overlay';
|
|
10919
|
+
overlay.innerHTML = '<div class="wf-dryrun-panel">' +
|
|
10920
|
+
'<div class="wf-dryrun-header">' +
|
|
10921
|
+
'<span class="wf-dryrun-title">Execution Plan: ' + escapeHtml(def.name || 'Workflow') + '</span>' +
|
|
10922
|
+
'<button class="wf-dryrun-close" onclick="wfCloseDryRun()" title="Close">×</button>' +
|
|
10923
|
+
'</div>' +
|
|
10924
|
+
'<div class="wf-dryrun-body">' +
|
|
10925
|
+
inputsHtml +
|
|
10926
|
+
summaryHtml +
|
|
10927
|
+
'<div style="margin-top:16px;">' + layersHtml + '</div>' +
|
|
10928
|
+
'</div>' +
|
|
10929
|
+
'</div>';
|
|
10930
|
+
|
|
10931
|
+
// Click backdrop to close
|
|
10932
|
+
overlay.addEventListener('click', (e) => {
|
|
10933
|
+
if (e.target === overlay) wfCloseDryRun();
|
|
10934
|
+
});
|
|
10935
|
+
|
|
10936
|
+
canvasArea.appendChild(overlay);
|
|
10937
|
+
}
|
|
10938
|
+
|
|
10939
|
+
function wfCloseDryRun() {
|
|
10940
|
+
document.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
|
|
10941
|
+
}
|
|
10942
|
+
|
|
10943
|
+
// ── Execution ──
|
|
10944
|
+
let wfAbortController = null;
|
|
10945
|
+
let wfExecTimer = null;
|
|
10946
|
+
let wfExecStartTime = 0;
|
|
10947
|
+
const WF_EXEC_TIMEOUT_MS = 120000; // 2 minute total timeout
|
|
10948
|
+
|
|
10949
|
+
function wfShowExecStatus(text, state) {
|
|
10950
|
+
const bar = document.getElementById('wfExecStatus');
|
|
10951
|
+
const textEl = document.getElementById('wfExecStatusText');
|
|
10952
|
+
if (!bar || !textEl) return;
|
|
10953
|
+
bar.style.display = 'flex';
|
|
10954
|
+
bar.className = 'wf-exec-status' + (state ? ' ' + state : '');
|
|
10955
|
+
textEl.textContent = text;
|
|
10956
|
+
}
|
|
10957
|
+
|
|
10958
|
+
function wfHideExecStatus() {
|
|
10959
|
+
const bar = document.getElementById('wfExecStatus');
|
|
10960
|
+
if (bar) bar.style.display = 'none';
|
|
10961
|
+
}
|
|
10962
|
+
|
|
10963
|
+
function wfUpdateExecTimer() {
|
|
10964
|
+
const timeEl = document.getElementById('wfExecStatusTime');
|
|
10965
|
+
if (!timeEl || !wfState.executing) return;
|
|
10966
|
+
const elapsed = Date.now() - wfExecStartTime;
|
|
10967
|
+
const secs = (elapsed / 1000).toFixed(1);
|
|
10968
|
+
timeEl.textContent = secs + 's';
|
|
10969
|
+
|
|
10970
|
+
// Check timeout
|
|
10971
|
+
if (elapsed > WF_EXEC_TIMEOUT_MS) {
|
|
10972
|
+
wfStopExecution('Execution timed out after ' + (WF_EXEC_TIMEOUT_MS / 1000) + 's');
|
|
10973
|
+
return;
|
|
10974
|
+
}
|
|
10975
|
+
wfExecTimer = requestAnimationFrame(wfUpdateExecTimer);
|
|
10976
|
+
}
|
|
10977
|
+
|
|
10978
|
+
function wfStopExecution(reason) {
|
|
10979
|
+
if (!wfState.executing) return;
|
|
10980
|
+
if (wfAbortController) {
|
|
10981
|
+
wfAbortController.abort();
|
|
10982
|
+
wfAbortController = null;
|
|
10983
|
+
}
|
|
10984
|
+
if (wfExecTimer) {
|
|
10985
|
+
cancelAnimationFrame(wfExecTimer);
|
|
10986
|
+
wfExecTimer = null;
|
|
10987
|
+
}
|
|
10988
|
+
|
|
10989
|
+
// Mark any still-running nodes as error
|
|
10990
|
+
const def = wfState.activeWorkflow;
|
|
10991
|
+
if (def) {
|
|
10992
|
+
def.steps.forEach(s => {
|
|
10993
|
+
if (wfState.executionState[s.id] === 'running') {
|
|
10994
|
+
wfState.executionState[s.id] = 'error';
|
|
10995
|
+
wfState.executionResults[s.id] = { error: reason || 'Stopped by user' };
|
|
10996
|
+
} else if (wfState.executionState[s.id] === 'pending') {
|
|
10997
|
+
wfState.executionState[s.id] = 'skipped';
|
|
10998
|
+
wfState.executionResults[s.id] = { reason: reason || 'Stopped by user' };
|
|
10999
|
+
}
|
|
11000
|
+
});
|
|
11001
|
+
wfRefreshNodes();
|
|
11002
|
+
}
|
|
11003
|
+
|
|
11004
|
+
const stopReason = reason || 'Stopped by user';
|
|
11005
|
+
wfShowExecStatus(stopReason, reason && reason.includes('timed out') ? 'error' : 'stopped');
|
|
11006
|
+
wfState.executing = false;
|
|
11007
|
+
wfSetToolbarEnabled(true);
|
|
11008
|
+
document.getElementById('wfStopBtn').style.display = 'none';
|
|
11009
|
+
wfUpdateInspector();
|
|
11010
|
+
}
|
|
11011
|
+
|
|
11012
|
+
// ── Input Modal (pre-execution) ──
|
|
11013
|
+
|
|
11014
|
+
function wfShowInputModal() {
|
|
11015
|
+
const def = wfState.activeWorkflow;
|
|
11016
|
+
if (!def || !def.inputs) return;
|
|
11017
|
+
const entries = Object.entries(def.inputs);
|
|
11018
|
+
if (entries.length === 0) { wfExecuteWithInputs({}); return; }
|
|
11019
|
+
|
|
11020
|
+
document.getElementById('wfInputModalTitle').textContent = (def.name || 'Workflow') + ' Inputs';
|
|
11021
|
+
let html = '';
|
|
11022
|
+
for (const [key, spec] of entries) {
|
|
11023
|
+
const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
|
|
11024
|
+
const desc = spec.description ? `<div class="wf-input-modal-desc">${escapeHtml(spec.description)}</div>` : '';
|
|
11025
|
+
// Pre-fill from inspector fields if available, then from defaults
|
|
11026
|
+
const inspectorEl = document.getElementById('wf-input-' + key);
|
|
11027
|
+
let prefill = inspectorEl ? inspectorEl.value : '';
|
|
11028
|
+
if (!prefill && spec.default !== undefined) prefill = String(spec.default);
|
|
11029
|
+
const placeholder = spec.type === 'number' ? 'number' : (spec.type || 'string');
|
|
11030
|
+
html += `<div class="wf-input-modal-field">
|
|
11031
|
+
<div class="wf-input-modal-label">${escapeHtml(key)}${req}</div>
|
|
11032
|
+
${desc}
|
|
11033
|
+
<input class="wf-input-modal-input" id="wf-modal-input-${key}" placeholder="${escapeHtml(placeholder)}" value="${escapeHtml(prefill)}" data-key="${escapeHtml(key)}" data-type="${spec.type || 'string'}" data-required="${!!spec.required}">
|
|
11034
|
+
<div class="wf-input-modal-error" id="wf-modal-err-${key}">This field is required</div>
|
|
11035
|
+
</div>`;
|
|
11036
|
+
}
|
|
11037
|
+
document.getElementById('wfInputModalBody').innerHTML = html;
|
|
11038
|
+
document.getElementById('wfInputModalBackdrop').style.display = '';
|
|
11039
|
+
|
|
11040
|
+
// Focus first empty required field, or first field
|
|
11041
|
+
const firstEmpty = entries.find(([k, s]) => {
|
|
11042
|
+
const el = document.getElementById('wf-modal-input-' + k);
|
|
11043
|
+
return s.required && el && !el.value;
|
|
11044
|
+
});
|
|
11045
|
+
const focusKey = firstEmpty ? firstEmpty[0] : entries[0][0];
|
|
11046
|
+
const focusEl = document.getElementById('wf-modal-input-' + focusKey);
|
|
11047
|
+
if (focusEl) setTimeout(() => focusEl.focus(), 50);
|
|
11048
|
+
}
|
|
11049
|
+
|
|
11050
|
+
function wfCloseInputModal() {
|
|
11051
|
+
document.getElementById('wfInputModalBackdrop').style.display = 'none';
|
|
11052
|
+
}
|
|
11053
|
+
|
|
11054
|
+
function wfInputModalSubmit() {
|
|
11055
|
+
const def = wfState.activeWorkflow;
|
|
11056
|
+
if (!def || !def.inputs) return;
|
|
11057
|
+
|
|
11058
|
+
const inputs = {};
|
|
11059
|
+
let hasError = false;
|
|
11060
|
+
|
|
11061
|
+
for (const [key, spec] of Object.entries(def.inputs)) {
|
|
11062
|
+
const el = document.getElementById('wf-modal-input-' + key);
|
|
11063
|
+
const errEl = document.getElementById('wf-modal-err-' + key);
|
|
11064
|
+
if (!el) continue;
|
|
11065
|
+
|
|
11066
|
+
let val = el.value.trim();
|
|
11067
|
+
|
|
11068
|
+
// Clear previous error
|
|
11069
|
+
el.classList.remove('error');
|
|
11070
|
+
if (errEl) errEl.style.display = 'none';
|
|
11071
|
+
|
|
11072
|
+
// Required check
|
|
11073
|
+
if (!val && spec.required && spec.default === undefined) {
|
|
11074
|
+
el.classList.add('error');
|
|
11075
|
+
if (errEl) { errEl.textContent = 'This field is required'; errEl.style.display = ''; }
|
|
11076
|
+
hasError = true;
|
|
11077
|
+
continue;
|
|
11078
|
+
}
|
|
11079
|
+
|
|
11080
|
+
// Use default if empty
|
|
11081
|
+
if (!val && spec.default !== undefined) val = String(spec.default);
|
|
11082
|
+
|
|
11083
|
+
// Type validation
|
|
11084
|
+
if (spec.type === 'number' && val && isNaN(Number(val))) {
|
|
11085
|
+
el.classList.add('error');
|
|
11086
|
+
if (errEl) { errEl.textContent = 'Must be a number'; errEl.style.display = ''; }
|
|
11087
|
+
hasError = true;
|
|
11088
|
+
continue;
|
|
11089
|
+
}
|
|
11090
|
+
|
|
11091
|
+
// Coerce
|
|
11092
|
+
if (spec.type === 'number' && val) val = Number(val);
|
|
11093
|
+
if (val !== undefined && val !== '') inputs[key] = val;
|
|
11094
|
+
}
|
|
11095
|
+
|
|
11096
|
+
if (hasError) return;
|
|
11097
|
+
|
|
11098
|
+
// Also update inspector fields to keep them in sync
|
|
11099
|
+
for (const [key, val] of Object.entries(inputs)) {
|
|
11100
|
+
const inspEl = document.getElementById('wf-input-' + key);
|
|
11101
|
+
if (inspEl) inspEl.value = typeof val === 'number' ? String(val) : val;
|
|
11102
|
+
}
|
|
11103
|
+
|
|
11104
|
+
wfCloseInputModal();
|
|
11105
|
+
wfExecuteWithInputs(inputs);
|
|
11106
|
+
}
|
|
11107
|
+
|
|
11108
|
+
// Keyboard handler for input modal
|
|
11109
|
+
document.addEventListener('keydown', (e) => {
|
|
11110
|
+
const backdrop = document.getElementById('wfInputModalBackdrop');
|
|
11111
|
+
if (!backdrop || backdrop.style.display === 'none') return;
|
|
11112
|
+
if (e.key === 'Escape') { wfCloseInputModal(); e.preventDefault(); }
|
|
11113
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { wfInputModalSubmit(); e.preventDefault(); }
|
|
11114
|
+
});
|
|
11115
|
+
|
|
11116
|
+
async function wfExecute() {
|
|
11117
|
+
const def = wfState.activeWorkflow;
|
|
11118
|
+
if (!def || wfState.executing) return;
|
|
11119
|
+
|
|
11120
|
+
// If workflow has inputs, show the input modal instead of executing directly
|
|
11121
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
11122
|
+
wfShowInputModal();
|
|
11123
|
+
return;
|
|
11124
|
+
}
|
|
11125
|
+
|
|
11126
|
+
// No inputs needed, execute directly
|
|
11127
|
+
wfExecuteWithInputs({});
|
|
11128
|
+
}
|
|
11129
|
+
|
|
11130
|
+
async function wfExecuteWithInputs(inputs) {
|
|
11131
|
+
const def = wfState.activeWorkflow;
|
|
11132
|
+
if (!def || wfState.executing) return;
|
|
11133
|
+
|
|
11134
|
+
wfState.executing = true;
|
|
11135
|
+
wfState.executionResults = {};
|
|
11136
|
+
wfAbortController = new AbortController();
|
|
11137
|
+
wfExecStartTime = Date.now();
|
|
11138
|
+
wfSetToolbarEnabled(false);
|
|
11139
|
+
document.getElementById('wfStopBtn').style.display = '';
|
|
11140
|
+
wfShowExecStatus('Running...', '');
|
|
11141
|
+
wfExecTimer = requestAnimationFrame(wfUpdateExecTimer);
|
|
11142
|
+
|
|
11143
|
+
// Reset all nodes to pending
|
|
11144
|
+
def.steps.forEach(s => { wfState.executionState[s.id] = 'pending'; });
|
|
11145
|
+
wfRefreshNodes();
|
|
11146
|
+
|
|
11147
|
+
let hasError = false;
|
|
11148
|
+
let errorMessage = '';
|
|
11149
|
+
|
|
11150
|
+
try {
|
|
11151
|
+
const res = await fetch('/api/workflows/execute', {
|
|
11152
|
+
method: 'POST',
|
|
11153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11154
|
+
body: JSON.stringify({ definition: def, inputs }),
|
|
11155
|
+
signal: wfAbortController.signal,
|
|
11156
|
+
});
|
|
11157
|
+
|
|
11158
|
+
if (!res.ok) {
|
|
11159
|
+
throw new Error('Server returned ' + res.status + ': ' + (await res.text()));
|
|
11160
|
+
}
|
|
11161
|
+
|
|
11162
|
+
const reader = res.body.getReader();
|
|
11163
|
+
const decoder = new TextDecoder();
|
|
11164
|
+
let buffer = '';
|
|
11165
|
+
let currentEvent = '';
|
|
11166
|
+
|
|
11167
|
+
while (true) {
|
|
11168
|
+
const { done, value } = await reader.read();
|
|
11169
|
+
if (done) break;
|
|
11170
|
+
buffer += decoder.decode(value, { stream: true });
|
|
11171
|
+
|
|
11172
|
+
const lines = buffer.split('\n');
|
|
11173
|
+
buffer = lines.pop() || '';
|
|
11174
|
+
|
|
11175
|
+
for (const line of lines) {
|
|
11176
|
+
if (line.startsWith('event: ')) {
|
|
11177
|
+
currentEvent = line.slice(7).trim();
|
|
11178
|
+
} else if (line.startsWith('data: ') && currentEvent) {
|
|
11179
|
+
let data;
|
|
11180
|
+
try { data = JSON.parse(line.slice(6)); } catch { continue; }
|
|
11181
|
+
|
|
11182
|
+
if (currentEvent === 'step_start') {
|
|
11183
|
+
wfState.executionState[data.stepId] = 'running';
|
|
11184
|
+
const stepName = (def.steps.find(s => s.id === data.stepId) || {}).name || data.stepId;
|
|
11185
|
+
wfShowExecStatus('Running: ' + stepName, '');
|
|
11186
|
+
wfRefreshNodes();
|
|
11187
|
+
} else if (currentEvent === 'step_complete') {
|
|
11188
|
+
wfState.executionState[data.stepId] = 'completed';
|
|
11189
|
+
wfState.executionResults[data.stepId] = {
|
|
11190
|
+
output: data.output,
|
|
11191
|
+
timeMs: data.timeMs,
|
|
11192
|
+
summary: data.summary || '',
|
|
11193
|
+
};
|
|
11194
|
+
wfRefreshNodes();
|
|
11195
|
+
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
11196
|
+
} else if (currentEvent === 'step_skip') {
|
|
11197
|
+
wfState.executionState[data.stepId] = 'skipped';
|
|
11198
|
+
wfState.executionResults[data.stepId] = { reason: data.reason };
|
|
11199
|
+
wfRefreshNodes();
|
|
11200
|
+
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
11201
|
+
} else if (currentEvent === 'step_error') {
|
|
11202
|
+
wfState.executionState[data.stepId] = 'error';
|
|
11203
|
+
wfState.executionResults[data.stepId] = { error: data.error };
|
|
11204
|
+
hasError = true;
|
|
11205
|
+
errorMessage = data.error || 'Step failed';
|
|
11206
|
+
wfRefreshNodes();
|
|
11207
|
+
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
11208
|
+
} else if (currentEvent === 'done') {
|
|
11209
|
+
wfState.executionResults._done = data;
|
|
11210
|
+
if (!wfState.selectedNodeId) wfUpdateInspector();
|
|
11211
|
+
} else if (currentEvent === 'error') {
|
|
11212
|
+
hasError = true;
|
|
11213
|
+
errorMessage = data.error || 'Workflow error';
|
|
11214
|
+
console.error('Workflow execution error:', data.error);
|
|
11215
|
+
}
|
|
11216
|
+
currentEvent = '';
|
|
11217
|
+
}
|
|
11218
|
+
}
|
|
11219
|
+
}
|
|
11220
|
+
} catch (err) {
|
|
11221
|
+
if (err.name === 'AbortError') {
|
|
11222
|
+
// Handled by wfStopExecution
|
|
11223
|
+
return;
|
|
11224
|
+
}
|
|
11225
|
+
hasError = true;
|
|
11226
|
+
errorMessage = err.message || 'Execution failed';
|
|
11227
|
+
console.error('Workflow execution failed:', err);
|
|
11228
|
+
// Mark running nodes as error
|
|
11229
|
+
def.steps.forEach(s => {
|
|
11230
|
+
if (wfState.executionState[s.id] === 'running') {
|
|
11231
|
+
wfState.executionState[s.id] = 'error';
|
|
11232
|
+
wfState.executionResults[s.id] = { error: errorMessage };
|
|
11233
|
+
}
|
|
11234
|
+
});
|
|
11235
|
+
wfRefreshNodes();
|
|
11236
|
+
}
|
|
11237
|
+
|
|
11238
|
+
if (wfExecTimer) { cancelAnimationFrame(wfExecTimer); wfExecTimer = null; }
|
|
11239
|
+
wfAbortController = null;
|
|
11240
|
+
wfState.executing = false;
|
|
11241
|
+
wfSetToolbarEnabled(true);
|
|
11242
|
+
document.getElementById('wfStopBtn').style.display = 'none';
|
|
11243
|
+
|
|
11244
|
+
const elapsed = ((Date.now() - wfExecStartTime) / 1000).toFixed(1);
|
|
11245
|
+
if (hasError) {
|
|
11246
|
+
wfShowExecStatus('Failed: ' + errorMessage, 'error');
|
|
11247
|
+
} else {
|
|
11248
|
+
wfShowExecStatus('Completed in ' + elapsed + 's', 'done');
|
|
11249
|
+
}
|
|
11250
|
+
wfUpdateInspector();
|
|
11251
|
+
}
|
|
11252
|
+
|
|
11253
|
+
function wfRefreshNodes() {
|
|
11254
|
+
// Re-render the full SVG (simple approach: redraw)
|
|
11255
|
+
if (wfState.activeWorkflow) {
|
|
11256
|
+
const svg = document.getElementById('wf-canvas');
|
|
11257
|
+
const def = wfState.activeWorkflow;
|
|
11258
|
+
const positions = wfState.nodePositions;
|
|
11259
|
+
|
|
11260
|
+
// Remove existing nodes and edges
|
|
11261
|
+
svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
|
|
11262
|
+
|
|
11263
|
+
// Redraw edges
|
|
11264
|
+
const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
11265
|
+
edgeGroup.classList.add('wf-edge-group');
|
|
11266
|
+
for (const [stepId, deps] of Object.entries(wfState.graph)) {
|
|
11267
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
11268
|
+
deps.forEach(rawDepId => {
|
|
11269
|
+
const depId = rawDepId.replace(/^!/, '');
|
|
11270
|
+
if (positions[depId] && positions[stepId]) {
|
|
11271
|
+
edgeGroup.appendChild(wfDrawEdge(depId, stepId, positions));
|
|
11272
|
+
}
|
|
11273
|
+
});
|
|
11274
|
+
}
|
|
11275
|
+
svg.appendChild(edgeGroup);
|
|
11276
|
+
|
|
11277
|
+
// Redraw nodes
|
|
11278
|
+
for (const step of def.steps) {
|
|
11279
|
+
const pos = positions[step.id];
|
|
11280
|
+
if (!pos) continue;
|
|
11281
|
+
const state = wfState.executionState[step.id] || 'idle';
|
|
11282
|
+
svg.appendChild(wfDrawNode(step, pos.x, pos.y, state));
|
|
11283
|
+
}
|
|
11284
|
+
}
|
|
11285
|
+
}
|
|
11286
|
+
|
|
11287
|
+
function wfResetExecution() {
|
|
11288
|
+
if (wfState.executing) return;
|
|
11289
|
+
wfState.executionState = {};
|
|
11290
|
+
wfState.executionResults = {};
|
|
11291
|
+
wfHideExecStatus();
|
|
11292
|
+
wfRefreshNodes();
|
|
11293
|
+
wfUpdateInspector();
|
|
11294
|
+
}
|
|
11295
|
+
|
|
11296
|
+
// ── Output Modal ──
|
|
11297
|
+
let wfOutputModalData = '';
|
|
11298
|
+
|
|
11299
|
+
function wfOpenOutputModal(title, content) {
|
|
11300
|
+
wfOutputModalData = content;
|
|
11301
|
+
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
11302
|
+
const titleEl = document.getElementById('wfOutputModalTitle');
|
|
11303
|
+
const bodyEl = document.getElementById('wfOutputModalBody');
|
|
11304
|
+
const copyLabel = document.getElementById('wfOutputCopyLabel');
|
|
11305
|
+
if (!backdrop || !bodyEl) return;
|
|
11306
|
+
titleEl.textContent = title || 'Output';
|
|
11307
|
+
bodyEl.textContent = content;
|
|
11308
|
+
copyLabel.textContent = 'Copy';
|
|
11309
|
+
backdrop.style.display = 'flex';
|
|
11310
|
+
}
|
|
11311
|
+
|
|
11312
|
+
function wfCloseOutputModal() {
|
|
11313
|
+
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
11314
|
+
if (backdrop) backdrop.style.display = 'none';
|
|
11315
|
+
}
|
|
11316
|
+
|
|
11317
|
+
function wfCopyOutput() {
|
|
11318
|
+
const label = document.getElementById('wfOutputCopyLabel');
|
|
11319
|
+
navigator.clipboard.writeText(wfOutputModalData).then(() => {
|
|
11320
|
+
if (label) { label.textContent = 'Copied!'; setTimeout(() => { label.textContent = 'Copy'; }, 2000); }
|
|
11321
|
+
}).catch(() => {
|
|
11322
|
+
if (label) label.textContent = 'Failed';
|
|
11323
|
+
});
|
|
11324
|
+
}
|
|
11325
|
+
|
|
11326
|
+
// Close modal on Escape
|
|
11327
|
+
document.addEventListener('keydown', (e) => {
|
|
11328
|
+
if (e.key === 'Escape') {
|
|
11329
|
+
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
11330
|
+
if (backdrop && backdrop.style.display !== 'none') {
|
|
11331
|
+
wfCloseOutputModal();
|
|
11332
|
+
e.preventDefault();
|
|
11333
|
+
}
|
|
11334
|
+
}
|
|
11335
|
+
});
|
|
11336
|
+
|
|
11337
|
+
// ── Zoom & Pan ──
|
|
11338
|
+
function wfZoom(direction) {
|
|
11339
|
+
// Multiplicative zoom: feels consistent at any zoom level
|
|
11340
|
+
const factor = direction > 0 ? 1.1 : 1 / 1.1;
|
|
11341
|
+
wfState.zoom = Math.max(0.2, Math.min(5, wfState.zoom * factor));
|
|
11342
|
+
wfApplyViewBox();
|
|
11343
|
+
}
|
|
11344
|
+
|
|
11345
|
+
function wfFitToView() {
|
|
11346
|
+
const svg = document.getElementById('wf-canvas');
|
|
11347
|
+
if (!svg) return;
|
|
11348
|
+
const positions = wfState.nodePositions;
|
|
11349
|
+
const ids = Object.keys(positions);
|
|
11350
|
+
if (ids.length === 0) return;
|
|
11351
|
+
|
|
11352
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
11353
|
+
ids.forEach(id => {
|
|
11354
|
+
const p = positions[id];
|
|
11355
|
+
minX = Math.min(minX, p.x);
|
|
11356
|
+
minY = Math.min(minY, p.y);
|
|
11357
|
+
maxX = Math.max(maxX, p.x + WF_NODE_W);
|
|
11358
|
+
maxY = Math.max(maxY, p.y + WF_NODE_H + 50); // extra for time label + backward edge loops
|
|
11359
|
+
});
|
|
11360
|
+
|
|
11361
|
+
const pad = 40;
|
|
11362
|
+
const vbW = maxX - minX + pad * 2;
|
|
11363
|
+
const vbH = maxY - minY + pad * 2;
|
|
11364
|
+
wfState.panX = minX - pad;
|
|
11365
|
+
wfState.panY = minY - pad;
|
|
11366
|
+
// Calculate zoom to fit
|
|
11367
|
+
const rect = svg.parentElement.getBoundingClientRect();
|
|
11368
|
+
const scaleX = rect.width / vbW;
|
|
11369
|
+
const scaleY = rect.height / vbH;
|
|
11370
|
+
wfState.zoom = Math.min(scaleX, scaleY, 1.5);
|
|
11371
|
+
wfApplyViewBox();
|
|
11372
|
+
}
|
|
11373
|
+
|
|
11374
|
+
function wfApplyViewBox() {
|
|
11375
|
+
const svg = document.getElementById('wf-canvas');
|
|
11376
|
+
if (!svg) return;
|
|
11377
|
+
const rect = svg.parentElement.getBoundingClientRect();
|
|
11378
|
+
const w = rect.width / wfState.zoom;
|
|
11379
|
+
const h = rect.height / wfState.zoom;
|
|
11380
|
+
svg.setAttribute('viewBox', `${wfState.panX} ${wfState.panY} ${w} ${h}`);
|
|
11381
|
+
}
|
|
11382
|
+
|
|
11383
|
+
// Pan via mouse drag
|
|
11384
|
+
function wfInitPan() {
|
|
11385
|
+
const svg = document.getElementById('wf-canvas');
|
|
11386
|
+
if (!svg) return;
|
|
11387
|
+
|
|
11388
|
+
svg.addEventListener('mousedown', (e) => {
|
|
11389
|
+
if (e.target === svg || e.target.tagName === 'svg') {
|
|
11390
|
+
wfState.isPanning = true;
|
|
11391
|
+
wfState.panStart = { x: e.clientX, y: e.clientY };
|
|
11392
|
+
svg.style.cursor = 'grabbing';
|
|
11393
|
+
}
|
|
11394
|
+
});
|
|
11395
|
+
|
|
11396
|
+
svg.addEventListener('mousemove', (e) => {
|
|
11397
|
+
if (!wfState.isPanning) return;
|
|
11398
|
+
const dx = (e.clientX - wfState.panStart.x) / wfState.zoom;
|
|
11399
|
+
const dy = (e.clientY - wfState.panStart.y) / wfState.zoom;
|
|
11400
|
+
wfState.panX -= dx;
|
|
11401
|
+
wfState.panY -= dy;
|
|
11402
|
+
wfState.panStart = { x: e.clientX, y: e.clientY };
|
|
11403
|
+
wfApplyViewBox();
|
|
11404
|
+
});
|
|
11405
|
+
|
|
11406
|
+
svg.addEventListener('mouseup', () => {
|
|
11407
|
+
wfState.isPanning = false;
|
|
11408
|
+
svg.style.cursor = '';
|
|
11409
|
+
});
|
|
11410
|
+
|
|
11411
|
+
svg.addEventListener('mouseleave', () => {
|
|
11412
|
+
wfState.isPanning = false;
|
|
11413
|
+
svg.style.cursor = '';
|
|
11414
|
+
});
|
|
11415
|
+
|
|
11416
|
+
// Scroll wheel zoom, centered on cursor position
|
|
11417
|
+
svg.addEventListener('wheel', (e) => {
|
|
11418
|
+
e.preventDefault();
|
|
11419
|
+
const factor = e.deltaY < 0 ? 1.03 : 1 / 1.03;
|
|
11420
|
+
const oldZoom = wfState.zoom;
|
|
11421
|
+
const newZoom = Math.max(0.2, Math.min(5, oldZoom * factor));
|
|
11422
|
+
// Zoom toward cursor: keep the SVG point under the mouse fixed
|
|
11423
|
+
const rect = svg.getBoundingClientRect();
|
|
11424
|
+
const mx = (e.clientX - rect.left) / oldZoom + wfState.panX;
|
|
11425
|
+
const my = (e.clientY - rect.top) / oldZoom + wfState.panY;
|
|
11426
|
+
wfState.zoom = newZoom;
|
|
11427
|
+
wfState.panX = mx - (e.clientX - rect.left) / newZoom;
|
|
11428
|
+
wfState.panY = my - (e.clientY - rect.top) / newZoom;
|
|
11429
|
+
wfApplyViewBox();
|
|
11430
|
+
}, { passive: false });
|
|
11431
|
+
|
|
11432
|
+
// Click on canvas background to deselect
|
|
11433
|
+
svg.addEventListener('click', (e) => {
|
|
11434
|
+
if (e.target === svg || e.target.tagName === 'svg') {
|
|
11435
|
+
wfDeselectNode();
|
|
11436
|
+
}
|
|
11437
|
+
});
|
|
11438
|
+
}
|
|
11439
|
+
|
|
11440
|
+
// ── Builder: Input Definitions ──
|
|
11441
|
+
const WF_INPUT_DEFS = {
|
|
11442
|
+
query: [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false, placeholder: 'Collection name' }, { key: 'db', type: 'text', required: false, placeholder: 'Database name' }, { key: 'limit', type: 'number', required: false, placeholder: '5' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
|
|
11443
|
+
search: [{ key: 'query', type: 'text', required: true, placeholder: 'Search query' }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'limit', type: 'number', required: false, placeholder: '10' }, { key: 'filter', type: 'json', required: false, placeholder: '{}' }],
|
|
11444
|
+
rerank: [{ key: 'query', type: 'text', required: true }, { key: 'documents', type: 'json', required: true, placeholder: '["doc1","doc2"]' }, { key: 'model', type: 'text', required: false, placeholder: 'rerank-2.5' }],
|
|
11445
|
+
ingest: [{ key: 'text', type: 'textarea', required: true }, { key: 'collection', type: 'text', required: false }, { key: 'db', type: 'text', required: false }, { key: 'source', type: 'text', required: false }, { key: 'chunkSize', type: 'number', required: false, placeholder: '512' }, { key: 'chunkStrategy', type: 'select', required: false, options: ['fixed','sentence','paragraph','recursive','markdown'] }],
|
|
11446
|
+
embed: [{ key: 'text', type: 'text', required: true, placeholder: 'Text to embed' }, { key: 'model', type: 'text', required: false, placeholder: 'voyage-3-large' }, { key: 'inputType', type: 'select', required: false, options: ['document','query'] }],
|
|
11447
|
+
similarity: [{ key: 'text1', type: 'text', required: true }, { key: 'text2', type: 'text', required: true }, { key: 'model', type: 'text', required: false }],
|
|
11448
|
+
collections: [{ key: 'db', type: 'text', required: false }],
|
|
11449
|
+
models: [{ key: 'category', type: 'select', required: false, options: ['embedding','rerank','all'] }],
|
|
11450
|
+
estimate: [{ key: 'docs', type: 'number', required: true, placeholder: '1000' }, { key: 'queries', type: 'number', required: false, placeholder: '0' }, { key: 'months', type: 'number', required: false, placeholder: '12' }],
|
|
11451
|
+
explain: [{ key: 'topic', type: 'text', required: true }],
|
|
11452
|
+
topics: [{ key: 'search', type: 'text', required: false }],
|
|
11453
|
+
merge: [{ key: 'sources', type: 'json', required: true, placeholder: '["step1.output","step2.output"]' }, { key: 'strategy', type: 'select', required: false, options: ['concat','interleave','unique'] }],
|
|
11454
|
+
filter: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'condition', type: 'text', required: true, placeholder: 'item.score > 0.5' }],
|
|
11455
|
+
transform: [{ key: 'input', type: 'text', required: true, placeholder: '{{ step.output }}' }, { key: 'expression', type: 'text', required: true, placeholder: 'item.text' }],
|
|
11456
|
+
generate: [{ key: 'prompt', type: 'textarea', required: true, placeholder: 'Generate a summary of...' }, { key: 'context', type: 'text', required: false, placeholder: '{{ step.output }}' }],
|
|
11457
|
+
};
|
|
11458
|
+
|
|
11459
|
+
const WF_CATEGORY_ORDER = ['retrieval', 'embedding', 'management', 'utility', 'control', 'generation'];
|
|
11460
|
+
const WF_CATEGORY_LABELS = { retrieval: 'Retrieval', embedding: 'Embedding', management: 'Management', utility: 'Utility', control: 'Control Flow', generation: 'Generation' };
|
|
11461
|
+
|
|
11462
|
+
// ── Builder: Library/Palette tab toggle ──
|
|
11463
|
+
function wfSwitchLibTab(tab) {
|
|
11464
|
+
const libraryList = document.getElementById('wfLibraryList');
|
|
11465
|
+
const paletteList = document.getElementById('wfPaletteList');
|
|
11466
|
+
document.querySelectorAll('.wf-lib-tab').forEach(b => b.classList.toggle('active', b.dataset.libTab === tab));
|
|
11467
|
+
if (libraryList) libraryList.style.display = tab === 'library' ? '' : 'none';
|
|
11468
|
+
if (paletteList) paletteList.style.display = tab === 'palette' ? '' : 'none';
|
|
11469
|
+
if (tab === 'palette') wfRenderPalette();
|
|
11470
|
+
}
|
|
11471
|
+
|
|
11472
|
+
// ── Builder: Palette rendering ──
|
|
11473
|
+
function wfRenderPalette() {
|
|
11474
|
+
const container = document.getElementById('wfPaletteList');
|
|
11475
|
+
if (!container) return;
|
|
11476
|
+
const grouped = {};
|
|
11477
|
+
for (const [tool, meta] of Object.entries(WF_NODE_META)) {
|
|
11478
|
+
const cat = meta.category || 'unknown';
|
|
11479
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
11480
|
+
grouped[cat].push({ tool, ...meta });
|
|
11481
|
+
}
|
|
11482
|
+
let html = '';
|
|
11483
|
+
for (const cat of WF_CATEGORY_ORDER) {
|
|
11484
|
+
const items = grouped[cat];
|
|
11485
|
+
if (!items) continue;
|
|
11486
|
+
html += `<div class="wf-palette-category"><div class="wf-palette-category-title">${WF_CATEGORY_LABELS[cat] || cat}</div>`;
|
|
11487
|
+
for (const item of items) {
|
|
11488
|
+
html += `<div class="wf-palette-item" draggable="true" ondragstart="event.dataTransfer.setData('text/plain','${item.tool}')" onclick="wfAddNodeFromPalette('${item.tool}')">
|
|
11489
|
+
<span class="wf-palette-icon" style="color:${item.color}"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="${item.icon || WF_FALLBACK_ICON}"/></svg></span>
|
|
11490
|
+
<span class="wf-palette-label">${item.label}</span>
|
|
11491
|
+
</div>`;
|
|
11492
|
+
}
|
|
11493
|
+
html += '</div>';
|
|
11494
|
+
}
|
|
11495
|
+
container.innerHTML = html;
|
|
11496
|
+
}
|
|
11497
|
+
|
|
11498
|
+
// ── Builder: New Workflow ──
|
|
11499
|
+
function wfNewWorkflow() {
|
|
11500
|
+
wfState.activeWorkflow = {
|
|
11501
|
+
name: 'Untitled Workflow',
|
|
11502
|
+
description: '',
|
|
11503
|
+
steps: [],
|
|
11504
|
+
inputs: {},
|
|
11505
|
+
defaults: {},
|
|
11506
|
+
output: {}
|
|
11507
|
+
};
|
|
11508
|
+
wfState.builderMode = true;
|
|
11509
|
+
wfState.dirtyFlag = false;
|
|
11510
|
+
wfState.selectedNodeId = null;
|
|
11511
|
+
wfState.executionState = {};
|
|
11512
|
+
wfState.executionResults = {};
|
|
11513
|
+
wfState.nodePositions = {};
|
|
11514
|
+
wfState.layers = [];
|
|
11515
|
+
wfState.graph = {};
|
|
11516
|
+
wfState.zoom = 1;
|
|
11517
|
+
wfState.panX = 0;
|
|
11518
|
+
wfState.panY = 0;
|
|
11519
|
+
wfState.draggingEdge = null;
|
|
11520
|
+
wfState.dragNode = null;
|
|
11521
|
+
|
|
11522
|
+
// Clear canvas
|
|
11523
|
+
const svg = document.getElementById('wf-canvas');
|
|
11524
|
+
if (svg) svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
|
|
11525
|
+
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
11526
|
+
svg.style.display = '';
|
|
11527
|
+
|
|
11528
|
+
// Enable toolbar buttons
|
|
11529
|
+
document.getElementById('wfDryRunBtn').disabled = false;
|
|
11530
|
+
document.getElementById('wfRunBtn').disabled = false;
|
|
11531
|
+
document.getElementById('wfExportBtn').disabled = false;
|
|
11532
|
+
|
|
11533
|
+
// Switch to palette tab
|
|
11534
|
+
wfSwitchLibTab('palette');
|
|
11535
|
+
|
|
11536
|
+
// Open inspector for workflow-level editing
|
|
11537
|
+
wfOpenInspector();
|
|
11538
|
+
wfUpdateInspector();
|
|
11539
|
+
|
|
11540
|
+
// Set viewBox
|
|
11541
|
+
wfApplyViewBox();
|
|
11542
|
+
}
|
|
11543
|
+
|
|
11544
|
+
// ── Builder: Edit current workflow ──
|
|
11545
|
+
function wfEditWorkflow() {
|
|
11546
|
+
if (!wfState.activeWorkflow) return;
|
|
11547
|
+
wfState.builderMode = true;
|
|
11548
|
+
wfState.dirtyFlag = false;
|
|
11549
|
+
wfState.selectedNodeId = null;
|
|
11550
|
+
wfState.draggingEdge = null;
|
|
11551
|
+
wfState.dragNode = null;
|
|
11552
|
+
|
|
11553
|
+
// Re-render nodes with builder ports and drag handles
|
|
11554
|
+
wfRefreshNodes();
|
|
11555
|
+
|
|
11556
|
+
// Switch to palette tab and open inspector for workflow-level editing
|
|
11557
|
+
wfSwitchLibTab('palette');
|
|
11558
|
+
wfOpenInspector();
|
|
11559
|
+
wfUpdateInspector();
|
|
11560
|
+
}
|
|
11561
|
+
|
|
11562
|
+
// ── Builder: Add node from palette ──
|
|
11563
|
+
function wfAddNodeFromPalette(tool) {
|
|
11564
|
+
if (!wfState.activeWorkflow) wfNewWorkflow();
|
|
11565
|
+
wfState.builderMode = true;
|
|
11566
|
+
const def = wfState.activeWorkflow;
|
|
11567
|
+
|
|
11568
|
+
// Generate unique step ID
|
|
11569
|
+
let baseId = tool;
|
|
11570
|
+
let id = baseId;
|
|
11571
|
+
let counter = 2;
|
|
11572
|
+
const existingIds = new Set(def.steps.map(s => s.id));
|
|
11573
|
+
while (existingIds.has(id)) { id = baseId + '_' + counter; counter++; }
|
|
11574
|
+
|
|
11575
|
+
// Build default inputs
|
|
11576
|
+
const inputDefs = WF_INPUT_DEFS[tool] || [];
|
|
11577
|
+
const inputs = {};
|
|
11578
|
+
for (const d of inputDefs) {
|
|
11579
|
+
if (d.required) inputs[d.key] = '';
|
|
11580
|
+
}
|
|
11581
|
+
|
|
11582
|
+
const meta = WF_NODE_META[tool] || {};
|
|
11583
|
+
const step = {
|
|
11584
|
+
id,
|
|
11585
|
+
name: meta.label || tool,
|
|
11586
|
+
tool,
|
|
11587
|
+
inputs,
|
|
11588
|
+
};
|
|
11589
|
+
def.steps.push(step);
|
|
11590
|
+
|
|
11591
|
+
// Position: place to the right of all existing nodes
|
|
11592
|
+
let maxX = WF_PAD;
|
|
11593
|
+
for (const pos of Object.values(wfState.nodePositions)) {
|
|
11594
|
+
if (pos.x + WF_NODE_W + WF_LAYER_GAP > maxX) maxX = pos.x + WF_NODE_W + WF_LAYER_GAP;
|
|
11595
|
+
}
|
|
11596
|
+
let y = WF_PAD;
|
|
11597
|
+
// Stack vertically if there are nodes in the same column
|
|
11598
|
+
const nodesAtX = Object.values(wfState.nodePositions).filter(p => Math.abs(p.x - maxX) < 20);
|
|
11599
|
+
if (nodesAtX.length > 0) {
|
|
11600
|
+
y = Math.max(...nodesAtX.map(p => p.y)) + WF_NODE_H + WF_NODE_GAP;
|
|
11601
|
+
}
|
|
11602
|
+
wfState.nodePositions[id] = { x: maxX, y };
|
|
11603
|
+
|
|
11604
|
+
// Rebuild graph
|
|
11605
|
+
wfBuildGraph();
|
|
11606
|
+
wfRefreshNodes();
|
|
11607
|
+
wfSelectNode(id);
|
|
11608
|
+
wfState.dirtyFlag = true;
|
|
11609
|
+
}
|
|
11610
|
+
|
|
11611
|
+
// ── Builder: Build graph from step inputs (template references) ──
|
|
11612
|
+
function wfBuildGraph() {
|
|
11613
|
+
const def = wfState.activeWorkflow;
|
|
11614
|
+
if (!def) return;
|
|
11615
|
+
const graph = {};
|
|
11616
|
+
const stepIds = new Set(def.steps.map(s => s.id));
|
|
11617
|
+
for (const step of def.steps) {
|
|
11618
|
+
const deps = new Set();
|
|
11619
|
+
// Scan all input values for {{ stepId.xxx }} references
|
|
11620
|
+
for (const val of Object.values(step.inputs || {})) {
|
|
11621
|
+
const str = typeof val === 'string' ? val : JSON.stringify(val);
|
|
11622
|
+
const matches = str.matchAll(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\./g);
|
|
11623
|
+
for (const m of matches) {
|
|
11624
|
+
if (stepIds.has(m[1]) && m[1] !== step.id) deps.add(m[1]);
|
|
11625
|
+
}
|
|
11626
|
+
}
|
|
11627
|
+
graph[step.id] = Array.from(deps);
|
|
11628
|
+
}
|
|
11629
|
+
wfState.graph = graph;
|
|
11630
|
+
}
|
|
11631
|
+
|
|
11632
|
+
// ── Builder: Canvas drop (from palette drag) ──
|
|
11633
|
+
function wfCanvasDrop(e) {
|
|
11634
|
+
e.preventDefault();
|
|
11635
|
+
const tool = e.dataTransfer.getData('text/plain');
|
|
11636
|
+
if (!tool || !WF_NODE_META[tool]) return;
|
|
11637
|
+
if (!wfState.activeWorkflow) wfNewWorkflow();
|
|
11638
|
+
wfState.builderMode = true;
|
|
11639
|
+
const def = wfState.activeWorkflow;
|
|
11640
|
+
|
|
11641
|
+
// Generate unique ID
|
|
11642
|
+
let id = tool;
|
|
11643
|
+
let counter = 2;
|
|
11644
|
+
const existingIds = new Set(def.steps.map(s => s.id));
|
|
11645
|
+
while (existingIds.has(id)) { id = tool + '_' + counter; counter++; }
|
|
11646
|
+
|
|
11647
|
+
// Inputs
|
|
11648
|
+
const inputDefs = WF_INPUT_DEFS[tool] || [];
|
|
11649
|
+
const inputs = {};
|
|
11650
|
+
for (const d of inputDefs) { if (d.required) inputs[d.key] = ''; }
|
|
11651
|
+
|
|
11652
|
+
const meta = WF_NODE_META[tool] || {};
|
|
11653
|
+
const step = { id, name: meta.label || tool, tool, inputs };
|
|
11654
|
+
def.steps.push(step);
|
|
11655
|
+
|
|
11656
|
+
// Convert drop coordinates to SVG space
|
|
11657
|
+
const svg = document.getElementById('wf-canvas');
|
|
11658
|
+
const rect = svg.getBoundingClientRect();
|
|
11659
|
+
const svgX = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
|
|
11660
|
+
const svgY = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
|
|
11661
|
+
wfState.nodePositions[id] = { x: svgX, y: svgY };
|
|
11662
|
+
|
|
11663
|
+
wfBuildGraph();
|
|
11664
|
+
wfRefreshNodes();
|
|
11665
|
+
wfSelectNode(id);
|
|
11666
|
+
wfState.dirtyFlag = true;
|
|
11667
|
+
}
|
|
11668
|
+
|
|
11669
|
+
// ── Builder: Mutation helpers ──
|
|
11670
|
+
function wfEditStepField(stepId, field, value) {
|
|
11671
|
+
const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
|
|
11672
|
+
if (!step) return;
|
|
11673
|
+
step[field] = value;
|
|
11674
|
+
wfState.dirtyFlag = true;
|
|
11675
|
+
if (field === 'name') wfRefreshNodes();
|
|
11676
|
+
}
|
|
11677
|
+
|
|
11678
|
+
function wfEditStepInput(stepId, key, value) {
|
|
11679
|
+
const step = wfState.activeWorkflow?.steps.find(s => s.id === stepId);
|
|
11680
|
+
if (!step) return;
|
|
11681
|
+
if (!step.inputs) step.inputs = {};
|
|
11682
|
+
step.inputs[key] = value;
|
|
11683
|
+
wfState.dirtyFlag = true;
|
|
11684
|
+
// Rebuild graph in case template refs changed
|
|
11685
|
+
wfBuildGraph();
|
|
11686
|
+
wfRefreshNodes();
|
|
11687
|
+
}
|
|
11688
|
+
|
|
11689
|
+
function wfEditStepId(oldId, newId) {
|
|
11690
|
+
const def = wfState.activeWorkflow;
|
|
11691
|
+
if (!def) return;
|
|
11692
|
+
newId = newId.trim().replace(/[^a-zA-Z0-9_]/g, '_');
|
|
11693
|
+
if (!newId || newId === oldId) return;
|
|
11694
|
+
if (def.steps.some(s => s.id === newId)) return; // duplicate
|
|
11695
|
+
|
|
11696
|
+
const step = def.steps.find(s => s.id === oldId);
|
|
11697
|
+
if (!step) return;
|
|
11698
|
+
step.id = newId;
|
|
11699
|
+
|
|
11700
|
+
// Update position map
|
|
11701
|
+
if (wfState.nodePositions[oldId]) {
|
|
11702
|
+
wfState.nodePositions[newId] = wfState.nodePositions[oldId];
|
|
11703
|
+
delete wfState.nodePositions[oldId];
|
|
11704
|
+
}
|
|
11705
|
+
// Update template references in other steps
|
|
11706
|
+
for (const s of def.steps) {
|
|
11707
|
+
for (const [k, v] of Object.entries(s.inputs || {})) {
|
|
11708
|
+
if (typeof v === 'string' && v.includes('{{ ' + oldId + '.')) {
|
|
11709
|
+
s.inputs[k] = v.replaceAll('{{ ' + oldId + '.', '{{ ' + newId + '.');
|
|
11710
|
+
}
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11713
|
+
if (wfState.selectedNodeId === oldId) wfState.selectedNodeId = newId;
|
|
11714
|
+
wfState.dirtyFlag = true;
|
|
11715
|
+
wfBuildGraph();
|
|
11716
|
+
wfRefreshNodes();
|
|
11717
|
+
wfUpdateInspector();
|
|
11718
|
+
}
|
|
11719
|
+
|
|
11720
|
+
function wfDeleteStep(stepId) {
|
|
11721
|
+
const def = wfState.activeWorkflow;
|
|
11722
|
+
if (!def) return;
|
|
11723
|
+
def.steps = def.steps.filter(s => s.id !== stepId);
|
|
11724
|
+
delete wfState.nodePositions[stepId];
|
|
11725
|
+
if (wfState.selectedNodeId === stepId) wfState.selectedNodeId = null;
|
|
11726
|
+
wfState.dirtyFlag = true;
|
|
11727
|
+
wfBuildGraph();
|
|
11728
|
+
wfRefreshNodes();
|
|
11729
|
+
wfUpdateInspector();
|
|
11730
|
+
}
|
|
11731
|
+
|
|
11732
|
+
function wfEditWorkflowField(field, value) {
|
|
11733
|
+
if (!wfState.activeWorkflow) return;
|
|
11734
|
+
wfState.activeWorkflow[field] = value;
|
|
11735
|
+
wfState.dirtyFlag = true;
|
|
11736
|
+
}
|
|
11737
|
+
|
|
11738
|
+
// ── Builder: Validate ──
|
|
11739
|
+
async function wfValidateBuilder() {
|
|
11740
|
+
const def = wfState.activeWorkflow;
|
|
11741
|
+
if (!def) return null;
|
|
11742
|
+
try {
|
|
11743
|
+
const res = await fetch('/api/workflows/validate', {
|
|
11744
|
+
method: 'POST',
|
|
11745
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11746
|
+
body: JSON.stringify(def),
|
|
11747
|
+
});
|
|
11748
|
+
return await res.json();
|
|
11749
|
+
} catch (err) {
|
|
11750
|
+
return { valid: false, errors: [err.message] };
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
|
|
11754
|
+
// ── Builder: Edge drag ──
|
|
11755
|
+
function wfEdgeDragStart(fromId, fromX, fromY) {
|
|
11756
|
+
const svg = document.getElementById('wf-canvas');
|
|
11757
|
+
if (!svg) return;
|
|
11758
|
+
wfState.draggingEdge = { fromId, fromX, fromY };
|
|
11759
|
+
|
|
11760
|
+
// Create temp edge (dashed bezier), pointer-events: none so it doesn't block port hit-testing
|
|
11761
|
+
const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
11762
|
+
tempPath.setAttribute('id', 'wf-temp-edge');
|
|
11763
|
+
tempPath.setAttribute('fill', 'none');
|
|
11764
|
+
tempPath.setAttribute('stroke', 'var(--accent)');
|
|
11765
|
+
tempPath.setAttribute('stroke-width', '2');
|
|
11766
|
+
tempPath.setAttribute('stroke-dasharray', '6 4');
|
|
11767
|
+
tempPath.setAttribute('pointer-events', 'none');
|
|
11768
|
+
tempPath.setAttribute('d', `M${fromX},${fromY} L${fromX},${fromY}`);
|
|
11769
|
+
svg.appendChild(tempPath);
|
|
11770
|
+
|
|
11771
|
+
function onMove(e) {
|
|
11772
|
+
const rect = svg.getBoundingClientRect();
|
|
11773
|
+
const mx = (e.clientX - rect.left) / wfState.zoom + wfState.panX;
|
|
11774
|
+
const my = (e.clientY - rect.top) / wfState.zoom + wfState.panY;
|
|
11775
|
+
const dx = Math.abs(mx - fromX) * 0.5;
|
|
11776
|
+
tempPath.setAttribute('d', `M${fromX},${fromY} C${fromX + dx},${fromY} ${mx - dx},${my} ${mx},${my}`);
|
|
11777
|
+
}
|
|
11778
|
+
|
|
11779
|
+
function onUp() {
|
|
11780
|
+
document.removeEventListener('mousemove', onMove);
|
|
11781
|
+
document.removeEventListener('mouseup', onUp);
|
|
11782
|
+
const el = document.getElementById('wf-temp-edge');
|
|
11783
|
+
if (el) el.remove();
|
|
11784
|
+
wfState.draggingEdge = null;
|
|
11785
|
+
}
|
|
11786
|
+
|
|
11787
|
+
document.addEventListener('mousemove', onMove);
|
|
11788
|
+
document.addEventListener('mouseup', onUp);
|
|
11789
|
+
}
|
|
11790
|
+
|
|
11791
|
+
function wfEdgeDropOnInput(toId) {
|
|
11792
|
+
if (!wfState.draggingEdge) return;
|
|
11793
|
+
const fromId = wfState.draggingEdge.fromId;
|
|
11794
|
+
if (fromId === toId) return; // no self-connections
|
|
11795
|
+
|
|
11796
|
+
// Add template reference to the target step's first empty required input
|
|
11797
|
+
const def = wfState.activeWorkflow;
|
|
11798
|
+
if (!def) return;
|
|
11799
|
+
const targetStep = def.steps.find(s => s.id === toId);
|
|
11800
|
+
if (!targetStep) return;
|
|
11801
|
+
|
|
11802
|
+
const inputDefs = WF_INPUT_DEFS[targetStep.tool] || [];
|
|
11803
|
+
let connected = false;
|
|
11804
|
+
|
|
11805
|
+
// Try to fill the first empty required input with a template reference
|
|
11806
|
+
for (const d of inputDefs) {
|
|
11807
|
+
if (!targetStep.inputs) targetStep.inputs = {};
|
|
11808
|
+
const current = targetStep.inputs[d.key];
|
|
11809
|
+
if (!current || current === '') {
|
|
11810
|
+
targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
|
|
11811
|
+
connected = true;
|
|
11812
|
+
break;
|
|
11813
|
+
}
|
|
11814
|
+
}
|
|
11815
|
+
|
|
11816
|
+
// If no empty required input, try first empty optional input
|
|
11817
|
+
if (!connected) {
|
|
11818
|
+
for (const d of inputDefs) {
|
|
11819
|
+
const current = targetStep.inputs?.[d.key];
|
|
11820
|
+
if (!current || current === '') {
|
|
11821
|
+
if (!targetStep.inputs) targetStep.inputs = {};
|
|
11822
|
+
targetStep.inputs[d.key] = `{{ ${fromId}.output }}`;
|
|
11823
|
+
connected = true;
|
|
11824
|
+
break;
|
|
11825
|
+
}
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
|
|
11829
|
+
if (connected) {
|
|
11830
|
+
wfState.dirtyFlag = true;
|
|
11831
|
+
wfBuildGraph();
|
|
11832
|
+
wfRelayout();
|
|
11833
|
+
wfRefreshNodes();
|
|
11834
|
+
if (wfState.selectedNodeId === toId) wfUpdateInspector();
|
|
11835
|
+
}
|
|
11836
|
+
|
|
11837
|
+
// Clean up drag state
|
|
11838
|
+
wfState.draggingEdge = null;
|
|
11839
|
+
const el = document.getElementById('wf-temp-edge');
|
|
11840
|
+
if (el) el.remove();
|
|
11841
|
+
}
|
|
11842
|
+
|
|
11843
|
+
// ── Builder: Relayout via topological sort ──
|
|
11844
|
+
async function wfRelayout() {
|
|
11845
|
+
const def = wfState.activeWorkflow;
|
|
11846
|
+
if (!def || def.steps.length === 0) return;
|
|
11847
|
+
|
|
11848
|
+
try {
|
|
11849
|
+
const res = await fetch('/api/workflows/plan', {
|
|
11850
|
+
method: 'POST',
|
|
11851
|
+
headers: { 'Content-Type': 'application/json' },
|
|
11852
|
+
body: JSON.stringify(def),
|
|
11853
|
+
});
|
|
11854
|
+
const data = await res.json();
|
|
11855
|
+
if (data.layers && data.layers.length > 0) {
|
|
11856
|
+
wfState.layers = data.layers;
|
|
11857
|
+
// Reposition nodes based on layers
|
|
11858
|
+
const positions = {};
|
|
11859
|
+
data.layers.forEach((layer, li) => {
|
|
11860
|
+
layer.forEach((stepId, ni) => {
|
|
11861
|
+
positions[stepId] = {
|
|
11862
|
+
x: WF_PAD + li * WF_LAYER_GAP,
|
|
11863
|
+
y: WF_PAD + ni * (WF_NODE_H + WF_NODE_GAP),
|
|
11864
|
+
};
|
|
11865
|
+
});
|
|
11866
|
+
});
|
|
11867
|
+
// Keep orphan nodes (not in any layer) at their current position
|
|
11868
|
+
for (const step of def.steps) {
|
|
11869
|
+
if (!positions[step.id] && wfState.nodePositions[step.id]) {
|
|
11870
|
+
positions[step.id] = wfState.nodePositions[step.id];
|
|
11871
|
+
}
|
|
11872
|
+
}
|
|
11873
|
+
wfState.nodePositions = positions;
|
|
11874
|
+
}
|
|
11875
|
+
} catch (err) {
|
|
11876
|
+
console.warn('Relayout failed:', err.message);
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
|
|
11880
|
+
// ── Docs shortcut (F1) ──
|
|
11881
|
+
const DOCS_URLS = {
|
|
11882
|
+
embed: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
|
|
11883
|
+
compare: 'https://docs.vaicli.com/docs/commands/embeddings/similarity',
|
|
11884
|
+
search: 'https://docs.vaicli.com/docs/commands/embeddings/rerank',
|
|
11885
|
+
multimodal: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
|
|
11886
|
+
generate: 'https://docs.vaicli.com/docs/commands/project-setup/generate',
|
|
11887
|
+
chat: 'https://docs.vaicli.com/docs/commands/advanced/chat',
|
|
11888
|
+
workflows: 'https://docs.vaicli.com/docs/commands/advanced/workflow-run',
|
|
11889
|
+
benchmark: 'https://docs.vaicli.com/docs/commands/evaluation/benchmark',
|
|
11890
|
+
explore: 'https://docs.vaicli.com/docs/commands/tools-and-learning/explain',
|
|
11891
|
+
about: 'https://docs.vaicli.com/docs/',
|
|
11892
|
+
settings: 'https://docs.vaicli.com/docs/commands/tools-and-learning/config',
|
|
11893
|
+
};
|
|
11894
|
+
document.addEventListener('keydown', (e) => {
|
|
11895
|
+
if (e.key === 'F1') {
|
|
11896
|
+
e.preventDefault();
|
|
11897
|
+
const activeTab = document.querySelector('.tab-btn.active');
|
|
11898
|
+
const tabName = activeTab ? activeTab.dataset.tab : '';
|
|
11899
|
+
const url = DOCS_URLS[tabName] || 'https://docs.vaicli.com';
|
|
11900
|
+
window.open(url, '_blank', 'noopener');
|
|
11901
|
+
}
|
|
11902
|
+
});
|
|
11903
|
+
|
|
11904
|
+
// Keyboard shortcuts (when workflows tab is active)
|
|
11905
|
+
document.addEventListener('keydown', (e) => {
|
|
11906
|
+
const activeTab = document.querySelector('.tab-btn.active');
|
|
11907
|
+
if (!activeTab || activeTab.dataset.tab !== 'workflows') return;
|
|
11908
|
+
|
|
11909
|
+
// Ctrl/Cmd+S to save/export in builder mode (works even in inputs)
|
|
11910
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's' && wfState.builderMode) {
|
|
11911
|
+
e.preventDefault();
|
|
11912
|
+
wfExportJson();
|
|
11913
|
+
return;
|
|
11914
|
+
}
|
|
11915
|
+
|
|
11916
|
+
// Delete/Backspace to remove selected node in builder mode
|
|
11917
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && wfState.builderMode && wfState.selectedNodeId) {
|
|
11918
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
11919
|
+
e.preventDefault();
|
|
11920
|
+
wfDeleteStep(wfState.selectedNodeId);
|
|
11921
|
+
return;
|
|
11922
|
+
}
|
|
11923
|
+
|
|
11924
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
11925
|
+
|
|
11926
|
+
const PAN_STEP = 40;
|
|
11927
|
+
if (e.key === '+' || e.key === '=') { wfZoom(1); e.preventDefault(); }
|
|
11928
|
+
else if (e.key === '-') { wfZoom(-1); e.preventDefault(); }
|
|
11929
|
+
else if (e.key === '0') { wfFitToView(); e.preventDefault(); }
|
|
11930
|
+
else if (e.key === 'Escape') { wfDeselectNode(); e.preventDefault(); }
|
|
11931
|
+
else if (e.key === 'ArrowLeft') { wfState.panX -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
11932
|
+
else if (e.key === 'ArrowRight') { wfState.panX += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
11933
|
+
else if (e.key === 'ArrowUp') { wfState.panY -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
11934
|
+
else if (e.key === 'ArrowDown') { wfState.panY += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
11935
|
+
});
|
|
11936
|
+
|
|
11937
|
+
// ── Init ──
|
|
11938
|
+
function wfInit() {
|
|
11939
|
+
wfLoadLibrary();
|
|
11940
|
+
wfInitPan();
|
|
11941
|
+
}
|
|
11942
|
+
|
|
11943
|
+
// Auto-init: use MutationObserver on the panel to detect when it becomes visible
|
|
11944
|
+
let wfInitialized = false;
|
|
11945
|
+
(function() {
|
|
11946
|
+
const panel = document.getElementById('tab-workflows');
|
|
11947
|
+
if (!panel) return;
|
|
11948
|
+
const observer = new MutationObserver(() => {
|
|
11949
|
+
if (panel.classList.contains('active') && !wfInitialized) {
|
|
11950
|
+
wfInitialized = true;
|
|
11951
|
+
wfInit();
|
|
11952
|
+
}
|
|
11953
|
+
});
|
|
11954
|
+
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
|
|
11955
|
+
// Also init immediately if the tab is already active (shouldn't be, but safety)
|
|
11956
|
+
if (panel.classList.contains('active')) {
|
|
11957
|
+
wfInitialized = true;
|
|
11958
|
+
wfInit();
|
|
11959
|
+
}
|
|
11960
|
+
})();
|
|
11961
|
+
</script>
|
|
11962
|
+
|
|
8754
11963
|
<script>
|
|
8755
11964
|
(function() {
|
|
8756
11965
|
const BUG_API = 'https://vaicli.com/api/bugs';
|