voyageai-cli 1.26.0 → 1.27.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/chat.js +281 -78
- package/src/commands/doctor.js +157 -14
- package/src/commands/playground.js +233 -19
- package/src/lib/chat.js +170 -4
- package/src/lib/llm.js +304 -2
- package/src/lib/mongo.js +6 -6
- package/src/lib/prompt.js +60 -1
- package/src/lib/tool-registry.js +194 -0
- package/src/mcp/tools/embedding.js +55 -43
- package/src/mcp/tools/ingest.js +74 -67
- package/src/mcp/tools/management.js +60 -48
- package/src/mcp/tools/retrieval.js +181 -163
- package/src/mcp/tools/utility.js +171 -153
- 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 +2769 -27
|
@@ -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; }
|
|
@@ -1825,6 +1825,34 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
1825
1825
|
line-height: 1.5;
|
|
1826
1826
|
}
|
|
1827
1827
|
|
|
1828
|
+
/* ── Contextual docs link ── */
|
|
1829
|
+
.page-header { position: relative; }
|
|
1830
|
+
.page-header-docs {
|
|
1831
|
+
position: absolute; top: 0; right: 0;
|
|
1832
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
1833
|
+
font-size: 12px; font-weight: 500;
|
|
1834
|
+
color: var(--text-muted); text-decoration: none;
|
|
1835
|
+
padding: 4px 10px; border-radius: 6px;
|
|
1836
|
+
border: 1px solid var(--border);
|
|
1837
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
1838
|
+
}
|
|
1839
|
+
.page-header-docs:hover {
|
|
1840
|
+
color: var(--accent, #6c63ff); border-color: var(--accent, #6c63ff);
|
|
1841
|
+
background: rgba(108,99,255,0.06);
|
|
1842
|
+
}
|
|
1843
|
+
.page-header-docs svg { opacity: 0.6; }
|
|
1844
|
+
.page-header-docs:hover svg { opacity: 1; }
|
|
1845
|
+
.sidebar-docs-link {
|
|
1846
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
1847
|
+
font-size: 10px; color: var(--text-muted); text-decoration: none;
|
|
1848
|
+
padding: 2px 6px; border-radius: 4px;
|
|
1849
|
+
transition: color 0.15s, background 0.15s;
|
|
1850
|
+
}
|
|
1851
|
+
.sidebar-docs-link:hover {
|
|
1852
|
+
color: var(--accent, #6c63ff); background: rgba(108,99,255,0.08);
|
|
1853
|
+
}
|
|
1854
|
+
.sidebar-docs-link svg { width: 12px; height: 12px; }
|
|
1855
|
+
|
|
1828
1856
|
/* ── Settings page ── */
|
|
1829
1857
|
.settings-container { max-width: 640px; margin: 0 auto; }
|
|
1830
1858
|
.settings-section {
|
|
@@ -2052,6 +2080,34 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2052
2080
|
transition: opacity 0.3s;
|
|
2053
2081
|
}
|
|
2054
2082
|
.settings-saved.show { opacity: 1; }
|
|
2083
|
+
.doctor-check {
|
|
2084
|
+
display: flex;
|
|
2085
|
+
align-items: flex-start;
|
|
2086
|
+
gap: 10px;
|
|
2087
|
+
padding: 10px 14px;
|
|
2088
|
+
border-radius: var(--radius);
|
|
2089
|
+
background: var(--bg-input);
|
|
2090
|
+
border: 1px solid var(--border);
|
|
2091
|
+
}
|
|
2092
|
+
.doctor-check-icon { font-size: 15px; flex-shrink: 0; line-height: 1.4; }
|
|
2093
|
+
.doctor-check-body { flex: 1; min-width: 0; }
|
|
2094
|
+
.doctor-check-name { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
2095
|
+
.doctor-check-msg { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
|
|
2096
|
+
.doctor-check-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
|
|
2097
|
+
.doctor-check-badge {
|
|
2098
|
+
font-size: 10px;
|
|
2099
|
+
padding: 2px 8px;
|
|
2100
|
+
border-radius: 10px;
|
|
2101
|
+
font-weight: 600;
|
|
2102
|
+
text-transform: uppercase;
|
|
2103
|
+
letter-spacing: 0.5px;
|
|
2104
|
+
flex-shrink: 0;
|
|
2105
|
+
align-self: center;
|
|
2106
|
+
}
|
|
2107
|
+
.doctor-check-badge.pass { background: rgba(0,212,170,0.12); color: var(--success); }
|
|
2108
|
+
.doctor-check-badge.fail { background: rgba(255,105,96,0.12); color: var(--error); }
|
|
2109
|
+
.doctor-check-badge.warn { background: rgba(255,192,16,0.12); color: var(--warning); }
|
|
2110
|
+
.doctor-check-badge.optional { background: rgba(136,147,151,0.12); color: var(--text-muted); }
|
|
2055
2111
|
.settings-reset-btn {
|
|
2056
2112
|
background: transparent;
|
|
2057
2113
|
border: 1px solid var(--border);
|
|
@@ -2316,6 +2372,25 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2316
2372
|
background: var(--bg-card);
|
|
2317
2373
|
border: 1px solid var(--border);
|
|
2318
2374
|
border-bottom-left-radius: 4px;
|
|
2375
|
+
position: relative;
|
|
2376
|
+
}
|
|
2377
|
+
.chat-copy-btn {
|
|
2378
|
+
position: absolute; top: 6px; right: 6px;
|
|
2379
|
+
width: 28px; height: 28px; border-radius: 6px;
|
|
2380
|
+
border: 1px solid var(--border); background: var(--bg);
|
|
2381
|
+
color: var(--text-muted); cursor: pointer;
|
|
2382
|
+
display: flex; align-items: center; justify-content: center;
|
|
2383
|
+
opacity: 0; transition: opacity 0.15s, background 0.15s, color 0.15s;
|
|
2384
|
+
pointer-events: none; z-index: 1;
|
|
2385
|
+
}
|
|
2386
|
+
.chat-message.assistant:hover .chat-copy-btn {
|
|
2387
|
+
opacity: 1; pointer-events: auto;
|
|
2388
|
+
}
|
|
2389
|
+
.chat-copy-btn:hover {
|
|
2390
|
+
background: var(--bg-card); color: var(--text); border-color: var(--text-muted);
|
|
2391
|
+
}
|
|
2392
|
+
.chat-copy-btn.copied {
|
|
2393
|
+
color: #69F0AE; border-color: #69F0AE;
|
|
2319
2394
|
}
|
|
2320
2395
|
.chat-message.system-msg {
|
|
2321
2396
|
align-self: center;
|
|
@@ -2334,6 +2409,247 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2334
2409
|
.chat-sources summary { cursor: pointer; }
|
|
2335
2410
|
.chat-sources ul { margin: 4px 0 0 16px; padding: 0; }
|
|
2336
2411
|
.chat-sources li { margin: 2px 0; }
|
|
2412
|
+
/* Markdown rendering in assistant messages */
|
|
2413
|
+
.chat-message.assistant .chat-message-content.rendered {
|
|
2414
|
+
white-space: normal;
|
|
2415
|
+
}
|
|
2416
|
+
.chat-message.assistant .chat-message-content.rendered p {
|
|
2417
|
+
margin: 0 0 8px 0;
|
|
2418
|
+
}
|
|
2419
|
+
.chat-message.assistant .chat-message-content.rendered p:last-child {
|
|
2420
|
+
margin-bottom: 0;
|
|
2421
|
+
}
|
|
2422
|
+
.chat-message.assistant .chat-message-content.rendered h1,
|
|
2423
|
+
.chat-message.assistant .chat-message-content.rendered h2,
|
|
2424
|
+
.chat-message.assistant .chat-message-content.rendered h3,
|
|
2425
|
+
.chat-message.assistant .chat-message-content.rendered h4 {
|
|
2426
|
+
margin: 12px 0 6px 0;
|
|
2427
|
+
font-weight: 600;
|
|
2428
|
+
color: var(--accent-text, #fff);
|
|
2429
|
+
}
|
|
2430
|
+
.chat-message.assistant .chat-message-content.rendered h1 { font-size: 1.3em; }
|
|
2431
|
+
.chat-message.assistant .chat-message-content.rendered h2 { font-size: 1.15em; }
|
|
2432
|
+
.chat-message.assistant .chat-message-content.rendered h3 { font-size: 1.05em; }
|
|
2433
|
+
.chat-message.assistant .chat-message-content.rendered code {
|
|
2434
|
+
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
2435
|
+
font-size: 0.9em;
|
|
2436
|
+
background: var(--bg-input, #112733);
|
|
2437
|
+
padding: 2px 5px;
|
|
2438
|
+
border-radius: 4px;
|
|
2439
|
+
}
|
|
2440
|
+
.chat-message.assistant .chat-message-content.rendered pre {
|
|
2441
|
+
background: var(--bg-input, #112733);
|
|
2442
|
+
border: 1px solid var(--border);
|
|
2443
|
+
border-radius: 8px;
|
|
2444
|
+
padding: 12px;
|
|
2445
|
+
margin: 8px 0;
|
|
2446
|
+
overflow-x: auto;
|
|
2447
|
+
white-space: pre;
|
|
2448
|
+
}
|
|
2449
|
+
.chat-message.assistant .chat-message-content.rendered pre code {
|
|
2450
|
+
background: none;
|
|
2451
|
+
padding: 0;
|
|
2452
|
+
border-radius: 0;
|
|
2453
|
+
font-size: 13px;
|
|
2454
|
+
line-height: 1.5;
|
|
2455
|
+
}
|
|
2456
|
+
.chat-message.assistant .chat-message-content.rendered ul,
|
|
2457
|
+
.chat-message.assistant .chat-message-content.rendered ol {
|
|
2458
|
+
margin: 6px 0;
|
|
2459
|
+
padding-left: 20px;
|
|
2460
|
+
}
|
|
2461
|
+
.chat-message.assistant .chat-message-content.rendered li {
|
|
2462
|
+
margin: 3px 0;
|
|
2463
|
+
}
|
|
2464
|
+
.chat-message.assistant .chat-message-content.rendered blockquote {
|
|
2465
|
+
border-left: 3px solid var(--accent, #00D4AA);
|
|
2466
|
+
margin: 8px 0;
|
|
2467
|
+
padding: 4px 12px;
|
|
2468
|
+
color: var(--text-muted);
|
|
2469
|
+
}
|
|
2470
|
+
.chat-message.assistant .chat-message-content.rendered a {
|
|
2471
|
+
color: var(--accent, #00D4AA);
|
|
2472
|
+
text-decoration: none;
|
|
2473
|
+
}
|
|
2474
|
+
.chat-message.assistant .chat-message-content.rendered a:hover {
|
|
2475
|
+
text-decoration: underline;
|
|
2476
|
+
}
|
|
2477
|
+
.chat-message.assistant .chat-message-content.rendered table {
|
|
2478
|
+
border-collapse: collapse;
|
|
2479
|
+
margin: 8px 0;
|
|
2480
|
+
font-size: 13px;
|
|
2481
|
+
width: 100%;
|
|
2482
|
+
}
|
|
2483
|
+
.chat-message.assistant .chat-message-content.rendered th,
|
|
2484
|
+
.chat-message.assistant .chat-message-content.rendered td {
|
|
2485
|
+
border: 1px solid var(--border);
|
|
2486
|
+
padding: 6px 10px;
|
|
2487
|
+
text-align: left;
|
|
2488
|
+
}
|
|
2489
|
+
.chat-message.assistant .chat-message-content.rendered th {
|
|
2490
|
+
background: var(--bg-input, #112733);
|
|
2491
|
+
font-weight: 600;
|
|
2492
|
+
}
|
|
2493
|
+
.chat-message.assistant .chat-message-content.rendered hr {
|
|
2494
|
+
border: none;
|
|
2495
|
+
border-top: 1px solid var(--border);
|
|
2496
|
+
margin: 12px 0;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/* Agent thinking panel */
|
|
2500
|
+
.chat-thinking {
|
|
2501
|
+
align-self: flex-start;
|
|
2502
|
+
max-width: 85%;
|
|
2503
|
+
margin-bottom: 2px;
|
|
2504
|
+
font-size: 13px;
|
|
2505
|
+
}
|
|
2506
|
+
.chat-thinking summary {
|
|
2507
|
+
cursor: pointer;
|
|
2508
|
+
list-style: none;
|
|
2509
|
+
display: flex;
|
|
2510
|
+
align-items: center;
|
|
2511
|
+
gap: 6px;
|
|
2512
|
+
padding: 8px 14px;
|
|
2513
|
+
border-radius: 10px;
|
|
2514
|
+
background: var(--bg-card);
|
|
2515
|
+
border: 1px solid var(--border);
|
|
2516
|
+
color: var(--text-muted);
|
|
2517
|
+
font-size: 13px;
|
|
2518
|
+
transition: background 0.15s, border-color 0.15s;
|
|
2519
|
+
user-select: none;
|
|
2520
|
+
}
|
|
2521
|
+
.chat-thinking summary::-webkit-details-marker { display: none; }
|
|
2522
|
+
.chat-thinking summary:hover {
|
|
2523
|
+
background: var(--bg-surface);
|
|
2524
|
+
border-color: var(--accent-dim, #009E80);
|
|
2525
|
+
}
|
|
2526
|
+
.chat-thinking[open] summary {
|
|
2527
|
+
border-radius: 10px 10px 0 0;
|
|
2528
|
+
border-bottom-color: transparent;
|
|
2529
|
+
}
|
|
2530
|
+
.chat-thinking .thinking-icon {
|
|
2531
|
+
font-size: 14px;
|
|
2532
|
+
line-height: 1;
|
|
2533
|
+
}
|
|
2534
|
+
.chat-thinking .thinking-label {
|
|
2535
|
+
font-weight: 500;
|
|
2536
|
+
}
|
|
2537
|
+
.chat-thinking .thinking-chevron {
|
|
2538
|
+
margin-left: auto;
|
|
2539
|
+
font-size: 10px;
|
|
2540
|
+
opacity: 0.5;
|
|
2541
|
+
transition: transform 0.15s;
|
|
2542
|
+
}
|
|
2543
|
+
.chat-thinking[open] .thinking-chevron {
|
|
2544
|
+
transform: rotate(90deg);
|
|
2545
|
+
}
|
|
2546
|
+
.chat-thinking .thinking-count {
|
|
2547
|
+
background: var(--bg-input, #112733);
|
|
2548
|
+
color: var(--text-muted);
|
|
2549
|
+
font-size: 11px;
|
|
2550
|
+
padding: 1px 7px;
|
|
2551
|
+
border-radius: 10px;
|
|
2552
|
+
font-weight: 600;
|
|
2553
|
+
}
|
|
2554
|
+
.chat-thinking .thinking-elapsed {
|
|
2555
|
+
font-size: 11px;
|
|
2556
|
+
opacity: 0.45;
|
|
2557
|
+
}
|
|
2558
|
+
.chat-thinking .thinking-timeline {
|
|
2559
|
+
border: 1px solid var(--border);
|
|
2560
|
+
border-top: none;
|
|
2561
|
+
border-radius: 0 0 10px 10px;
|
|
2562
|
+
padding: 10px 14px;
|
|
2563
|
+
background: var(--bg-card);
|
|
2564
|
+
}
|
|
2565
|
+
.thinking-step {
|
|
2566
|
+
display: flex;
|
|
2567
|
+
gap: 10px;
|
|
2568
|
+
padding: 7px 0;
|
|
2569
|
+
position: relative;
|
|
2570
|
+
animation: thinkingSlideIn 0.25s ease-out;
|
|
2571
|
+
}
|
|
2572
|
+
@keyframes thinkingSlideIn {
|
|
2573
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
2574
|
+
to { opacity: 1; transform: translateY(0); }
|
|
2575
|
+
}
|
|
2576
|
+
.thinking-step + .thinking-step {
|
|
2577
|
+
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
|
2578
|
+
}
|
|
2579
|
+
.thinking-step-icon {
|
|
2580
|
+
width: 28px;
|
|
2581
|
+
height: 28px;
|
|
2582
|
+
border-radius: 50%;
|
|
2583
|
+
display: flex;
|
|
2584
|
+
align-items: center;
|
|
2585
|
+
justify-content: center;
|
|
2586
|
+
font-size: 14px;
|
|
2587
|
+
flex-shrink: 0;
|
|
2588
|
+
background: var(--bg-input, #112733);
|
|
2589
|
+
border: 1px solid var(--border);
|
|
2590
|
+
}
|
|
2591
|
+
.thinking-step.active .thinking-step-icon {
|
|
2592
|
+
border-color: var(--accent);
|
|
2593
|
+
animation: thinkingPulse 1.2s ease-in-out infinite;
|
|
2594
|
+
}
|
|
2595
|
+
@keyframes thinkingPulse {
|
|
2596
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0.25); }
|
|
2597
|
+
50% { box-shadow: 0 0 0 6px rgba(0, 212, 170, 0); }
|
|
2598
|
+
}
|
|
2599
|
+
.thinking-step.error .thinking-step-icon {
|
|
2600
|
+
border-color: #e74c3c;
|
|
2601
|
+
background: rgba(231, 76, 60, 0.08);
|
|
2602
|
+
}
|
|
2603
|
+
.thinking-step.done .thinking-step-icon {
|
|
2604
|
+
border-color: var(--accent-dim, #009E80);
|
|
2605
|
+
opacity: 0.7;
|
|
2606
|
+
}
|
|
2607
|
+
.thinking-step-body {
|
|
2608
|
+
flex: 1;
|
|
2609
|
+
min-width: 0;
|
|
2610
|
+
display: flex;
|
|
2611
|
+
flex-direction: column;
|
|
2612
|
+
justify-content: center;
|
|
2613
|
+
gap: 2px;
|
|
2614
|
+
}
|
|
2615
|
+
.thinking-step-header {
|
|
2616
|
+
display: flex;
|
|
2617
|
+
align-items: center;
|
|
2618
|
+
gap: 6px;
|
|
2619
|
+
}
|
|
2620
|
+
.thinking-step-name {
|
|
2621
|
+
font-weight: 600;
|
|
2622
|
+
color: var(--text);
|
|
2623
|
+
font-size: 12.5px;
|
|
2624
|
+
}
|
|
2625
|
+
.thinking-step-desc {
|
|
2626
|
+
font-size: 12px;
|
|
2627
|
+
color: var(--text-muted);
|
|
2628
|
+
opacity: 0.7;
|
|
2629
|
+
}
|
|
2630
|
+
.thinking-step-time {
|
|
2631
|
+
font-size: 11px;
|
|
2632
|
+
opacity: 0.45;
|
|
2633
|
+
margin-left: auto;
|
|
2634
|
+
white-space: nowrap;
|
|
2635
|
+
}
|
|
2636
|
+
.thinking-step-detail {
|
|
2637
|
+
font-size: 11.5px;
|
|
2638
|
+
color: var(--text-muted);
|
|
2639
|
+
margin-top: 2px;
|
|
2640
|
+
line-height: 1.4;
|
|
2641
|
+
overflow: hidden;
|
|
2642
|
+
text-overflow: ellipsis;
|
|
2643
|
+
display: -webkit-box;
|
|
2644
|
+
-webkit-line-clamp: 2;
|
|
2645
|
+
-webkit-box-orient: vertical;
|
|
2646
|
+
}
|
|
2647
|
+
.thinking-step-detail code {
|
|
2648
|
+
font-size: 11px;
|
|
2649
|
+
background: var(--bg-input, #112733);
|
|
2650
|
+
padding: 1px 4px;
|
|
2651
|
+
border-radius: 3px;
|
|
2652
|
+
}
|
|
2337
2653
|
.chat-input-area {
|
|
2338
2654
|
padding: 12px 16px;
|
|
2339
2655
|
border-top: 1px solid var(--border);
|
|
@@ -2648,6 +2964,450 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2648
2964
|
.settings-row { flex-direction: column; align-items: flex-start; gap: 8px; }
|
|
2649
2965
|
.settings-select, .settings-input { min-width: 100%; }
|
|
2650
2966
|
}
|
|
2967
|
+
|
|
2968
|
+
/* ========== WORKFLOW VISUALIZER ========== */
|
|
2969
|
+
/* Expand .main when workflows tab is active to fill viewport */
|
|
2970
|
+
.main:has(#tab-workflows.active) {
|
|
2971
|
+
max-width: none; padding: 0; overflow: hidden;
|
|
2972
|
+
}
|
|
2973
|
+
#tab-workflows.tab-panel.active {
|
|
2974
|
+
display: flex; flex-direction: column;
|
|
2975
|
+
height: 100%;
|
|
2976
|
+
/* Fill entire .main which is flex:1 inside the 100vh app-shell */
|
|
2977
|
+
}
|
|
2978
|
+
.wf-container {
|
|
2979
|
+
display: flex; flex: 1; gap: 0; overflow: hidden;
|
|
2980
|
+
border-radius: 0;
|
|
2981
|
+
background: var(--bg-card);
|
|
2982
|
+
min-height: 0;
|
|
2983
|
+
}
|
|
2984
|
+
.wf-library {
|
|
2985
|
+
width: 220px; min-width: 180px; flex-shrink: 0;
|
|
2986
|
+
border-right: 1px solid var(--border);
|
|
2987
|
+
display: flex; flex-direction: column;
|
|
2988
|
+
background: var(--bg);
|
|
2989
|
+
}
|
|
2990
|
+
.wf-library-header {
|
|
2991
|
+
padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
|
|
2992
|
+
color: var(--text); border-bottom: 1px solid var(--border);
|
|
2993
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
2994
|
+
}
|
|
2995
|
+
.wf-library-list {
|
|
2996
|
+
flex: 1; overflow-y: auto; padding: 8px;
|
|
2997
|
+
}
|
|
2998
|
+
.wf-library-footer {
|
|
2999
|
+
padding: 8px; border-top: 1px solid var(--border);
|
|
3000
|
+
}
|
|
3001
|
+
.wf-load-file-btn {
|
|
3002
|
+
width: 100%; padding: 7px 12px; border-radius: 6px;
|
|
3003
|
+
border: 1px dashed var(--border); background: transparent;
|
|
3004
|
+
color: var(--text-muted); cursor: pointer; font-size: 12px;
|
|
3005
|
+
display: flex; align-items: center; justify-content: center; gap: 6px;
|
|
3006
|
+
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
|
3007
|
+
}
|
|
3008
|
+
.wf-load-file-btn:hover {
|
|
3009
|
+
color: var(--text); border-color: var(--text-muted); background: var(--bg-card);
|
|
3010
|
+
}
|
|
3011
|
+
.wf-library-item {
|
|
3012
|
+
padding: 10px 12px; border-radius: 8px; cursor: pointer;
|
|
3013
|
+
margin-bottom: 4px; transition: background 0.15s;
|
|
3014
|
+
border: 1px solid transparent;
|
|
3015
|
+
}
|
|
3016
|
+
.wf-library-item:hover { background: var(--bg-card); }
|
|
3017
|
+
.wf-library-item.active {
|
|
3018
|
+
background: var(--bg-card); border-color: var(--accent, #6c63ff);
|
|
3019
|
+
}
|
|
3020
|
+
.wf-library-item-name {
|
|
3021
|
+
font-weight: 600; font-size: 13px; color: var(--text);
|
|
3022
|
+
margin-bottom: 2px;
|
|
3023
|
+
}
|
|
3024
|
+
.wf-library-item-desc {
|
|
3025
|
+
font-size: 11px; color: var(--text-muted);
|
|
3026
|
+
line-height: 1.3; display: -webkit-box;
|
|
3027
|
+
-webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
3028
|
+
}
|
|
3029
|
+
.wf-library-item-meta {
|
|
3030
|
+
font-size: 10px; color: var(--text-muted); margin-top: 4px;
|
|
3031
|
+
display: flex; gap: 8px;
|
|
3032
|
+
}
|
|
3033
|
+
.wf-canvas-area {
|
|
3034
|
+
flex: 1; position: relative; overflow: hidden;
|
|
3035
|
+
background: var(--bg);
|
|
3036
|
+
background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
|
|
3037
|
+
background-size: 20px 20px;
|
|
3038
|
+
}
|
|
3039
|
+
.wf-canvas-toolbar {
|
|
3040
|
+
position: absolute; top: 12px; right: 12px; z-index: 10;
|
|
3041
|
+
display: flex; gap: 4px;
|
|
3042
|
+
}
|
|
3043
|
+
.wf-canvas-toolbar button {
|
|
3044
|
+
width: 32px; height: 32px; border-radius: 8px;
|
|
3045
|
+
border: 1px solid var(--border); background: var(--bg-card);
|
|
3046
|
+
color: var(--text); cursor: pointer; font-size: 14px;
|
|
3047
|
+
display: flex; align-items: center; justify-content: center;
|
|
3048
|
+
transition: background 0.15s;
|
|
3049
|
+
}
|
|
3050
|
+
.wf-canvas-toolbar button:hover { background: var(--bg); border-color: var(--text-muted); }
|
|
3051
|
+
.wf-canvas-toolbar .wf-plan-btn {
|
|
3052
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3053
|
+
font-weight: 600; font-size: 12px;
|
|
3054
|
+
}
|
|
3055
|
+
.wf-canvas-toolbar .wf-plan-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3056
|
+
.wf-canvas-toolbar .wf-run-btn {
|
|
3057
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3058
|
+
background: var(--accent, #6c63ff); color: #fff; border-color: transparent;
|
|
3059
|
+
font-weight: 600; font-size: 12px;
|
|
3060
|
+
}
|
|
3061
|
+
.wf-canvas-toolbar .wf-run-btn:hover { opacity: 0.9; }
|
|
3062
|
+
.wf-canvas-toolbar .wf-run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3063
|
+
.wf-toolbar-sep {
|
|
3064
|
+
width: 1px; height: 20px; background: var(--border); margin: 0 2px;
|
|
3065
|
+
}
|
|
3066
|
+
/* Dry-run plan overlay */
|
|
3067
|
+
.wf-dryrun-overlay {
|
|
3068
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
|
3069
|
+
z-index: 8; background: rgba(0,0,0,0.5);
|
|
3070
|
+
display: flex; align-items: center; justify-content: center;
|
|
3071
|
+
animation: wf-modal-fade-in 0.15s ease;
|
|
3072
|
+
}
|
|
3073
|
+
.wf-dryrun-panel {
|
|
3074
|
+
width: 90%; max-width: 520px; max-height: 70vh;
|
|
3075
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
3076
|
+
border-radius: 12px; display: flex; flex-direction: column;
|
|
3077
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
3078
|
+
animation: wf-modal-slide-up 0.2s ease;
|
|
3079
|
+
}
|
|
3080
|
+
.wf-dryrun-header {
|
|
3081
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3082
|
+
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
|
3083
|
+
}
|
|
3084
|
+
.wf-dryrun-title { font-weight: 700; font-size: 14px; color: var(--text); }
|
|
3085
|
+
.wf-dryrun-close {
|
|
3086
|
+
border: none; background: none; color: var(--text-muted);
|
|
3087
|
+
cursor: pointer; font-size: 20px; padding: 2px 8px;
|
|
3088
|
+
}
|
|
3089
|
+
.wf-dryrun-close:hover { color: var(--text); }
|
|
3090
|
+
.wf-dryrun-body {
|
|
3091
|
+
flex: 1; overflow-y: auto; padding: 16px 20px;
|
|
3092
|
+
}
|
|
3093
|
+
.wf-dryrun-layer {
|
|
3094
|
+
margin-bottom: 16px;
|
|
3095
|
+
}
|
|
3096
|
+
.wf-dryrun-layer-title {
|
|
3097
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
3098
|
+
letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 8px;
|
|
3099
|
+
display: flex; align-items: center; gap: 8px;
|
|
3100
|
+
}
|
|
3101
|
+
.wf-dryrun-layer-badge {
|
|
3102
|
+
font-size: 10px; padding: 1px 6px; border-radius: 10px;
|
|
3103
|
+
background: var(--accent); color: #fff; font-weight: 600;
|
|
3104
|
+
}
|
|
3105
|
+
.wf-dryrun-step {
|
|
3106
|
+
padding: 8px 12px; border-radius: 6px; margin-bottom: 4px;
|
|
3107
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3108
|
+
display: flex; align-items: center; gap: 10px; font-size: 12px;
|
|
3109
|
+
}
|
|
3110
|
+
.wf-dryrun-step-icon { font-size: 16px; flex-shrink: 0; }
|
|
3111
|
+
.wf-dryrun-step-info { flex: 1; min-width: 0; }
|
|
3112
|
+
.wf-dryrun-step-name { font-weight: 600; color: var(--text); }
|
|
3113
|
+
.wf-dryrun-step-tool { color: var(--text-muted); font-size: 11px; }
|
|
3114
|
+
.wf-dryrun-step-cond {
|
|
3115
|
+
font-size: 10px; color: #FFD54F; margin-top: 2px;
|
|
3116
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
3117
|
+
}
|
|
3118
|
+
.wf-dryrun-summary {
|
|
3119
|
+
padding: 12px 16px; border-radius: 8px; margin-top: 8px;
|
|
3120
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3121
|
+
font-size: 12px; color: var(--text-muted);
|
|
3122
|
+
display: flex; gap: 16px;
|
|
3123
|
+
}
|
|
3124
|
+
.wf-dryrun-stat { text-align: center; }
|
|
3125
|
+
.wf-dryrun-stat-value { font-size: 20px; font-weight: 700; color: var(--text); }
|
|
3126
|
+
.wf-dryrun-stat-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
3127
|
+
[data-theme="light"] .wf-dryrun-panel { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
|
|
3128
|
+
[data-theme="light"] .wf-dryrun-step-cond { color: #944F01; }
|
|
3129
|
+
.wf-canvas-toolbar .wf-stop-btn {
|
|
3130
|
+
width: auto; padding: 0 12px; gap: 4px;
|
|
3131
|
+
background: #e74c3c; color: #fff; border-color: transparent;
|
|
3132
|
+
font-weight: 600; font-size: 12px;
|
|
3133
|
+
}
|
|
3134
|
+
.wf-canvas-toolbar .wf-stop-btn:hover { opacity: 0.9; background: #c0392b; }
|
|
3135
|
+
/* Execution status bar */
|
|
3136
|
+
.wf-exec-status {
|
|
3137
|
+
position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
|
|
3138
|
+
z-index: 10; display: flex; align-items: center; gap: 8px;
|
|
3139
|
+
padding: 6px 16px; border-radius: 20px;
|
|
3140
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3141
|
+
font-size: 12px; color: var(--text); box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
3142
|
+
}
|
|
3143
|
+
.wf-exec-status-dot {
|
|
3144
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
3145
|
+
background: #69F0AE; animation: wf-status-blink 1s ease-in-out infinite;
|
|
3146
|
+
}
|
|
3147
|
+
.wf-exec-status.error .wf-exec-status-dot { background: #e74c3c; animation: none; }
|
|
3148
|
+
.wf-exec-status.stopped .wf-exec-status-dot { background: #FFB74D; animation: none; }
|
|
3149
|
+
.wf-exec-status.done .wf-exec-status-dot { background: #69F0AE; animation: none; }
|
|
3150
|
+
@keyframes wf-status-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
3151
|
+
.wf-exec-status-time { color: var(--text-muted); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; }
|
|
3152
|
+
/* Output modal */
|
|
3153
|
+
.wf-output-modal-backdrop {
|
|
3154
|
+
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
3155
|
+
background: rgba(0,0,0,0.6); z-index: 9999;
|
|
3156
|
+
display: flex; align-items: center; justify-content: center;
|
|
3157
|
+
animation: wf-modal-fade-in 0.15s ease;
|
|
3158
|
+
}
|
|
3159
|
+
@keyframes wf-modal-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
3160
|
+
.wf-output-modal {
|
|
3161
|
+
width: 90%; max-width: 800px; max-height: 80vh;
|
|
3162
|
+
background: var(--bg); border: 1px solid var(--border);
|
|
3163
|
+
border-radius: 12px; display: flex; flex-direction: column;
|
|
3164
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
3165
|
+
animation: wf-modal-slide-up 0.2s ease;
|
|
3166
|
+
}
|
|
3167
|
+
@keyframes wf-modal-slide-up { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
3168
|
+
.wf-output-modal-header {
|
|
3169
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3170
|
+
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
|
3171
|
+
}
|
|
3172
|
+
.wf-output-modal-title { font-weight: 700; font-size: 14px; color: var(--text); }
|
|
3173
|
+
.wf-output-modal-actions { display: flex; align-items: center; gap: 6px; }
|
|
3174
|
+
.wf-output-modal-btn {
|
|
3175
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3176
|
+
padding: 5px 10px; border-radius: 6px;
|
|
3177
|
+
border: 1px solid var(--border); background: var(--bg-card);
|
|
3178
|
+
color: var(--text); cursor: pointer; font-size: 12px;
|
|
3179
|
+
transition: background 0.15s, border-color 0.15s;
|
|
3180
|
+
}
|
|
3181
|
+
.wf-output-modal-btn:hover { background: var(--bg); border-color: var(--text-muted); }
|
|
3182
|
+
.wf-output-modal-btn.close { border: none; font-size: 20px; padding: 2px 8px; }
|
|
3183
|
+
.wf-output-modal-body {
|
|
3184
|
+
flex: 1; overflow: auto; padding: 16px 20px;
|
|
3185
|
+
margin: 0; font-family: 'SF Mono', 'Fira Code', monospace;
|
|
3186
|
+
font-size: 12px; line-height: 1.5; color: var(--text);
|
|
3187
|
+
white-space: pre-wrap; word-break: break-word;
|
|
3188
|
+
}
|
|
3189
|
+
/* Expand button for output sections */
|
|
3190
|
+
.wf-output-expand-btn {
|
|
3191
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3192
|
+
padding: 3px 8px; border-radius: 4px; margin-top: 6px;
|
|
3193
|
+
border: 1px solid var(--border); background: transparent;
|
|
3194
|
+
color: var(--text-muted); cursor: pointer; font-size: 10px;
|
|
3195
|
+
transition: color 0.15s, border-color 0.15s;
|
|
3196
|
+
}
|
|
3197
|
+
.wf-output-expand-btn:hover { color: var(--text); border-color: var(--text-muted); }
|
|
3198
|
+
#wf-canvas {
|
|
3199
|
+
width: 100%; height: 100%; display: block;
|
|
3200
|
+
}
|
|
3201
|
+
/* SVG node styles */
|
|
3202
|
+
.wf-node { cursor: pointer; }
|
|
3203
|
+
.wf-node rect {
|
|
3204
|
+
rx: 10; ry: 10; stroke-width: 2;
|
|
3205
|
+
transition: stroke 0.2s, filter 0.2s;
|
|
3206
|
+
}
|
|
3207
|
+
.wf-node:hover rect { filter: brightness(1.1); }
|
|
3208
|
+
.wf-node.selected rect { stroke-width: 3; stroke: var(--accent, #6c63ff); }
|
|
3209
|
+
.wf-node-label {
|
|
3210
|
+
font-size: 12px; font-weight: 600; fill: #fff;
|
|
3211
|
+
pointer-events: none; text-anchor: middle; dominant-baseline: central;
|
|
3212
|
+
}
|
|
3213
|
+
.wf-node-icon {
|
|
3214
|
+
font-size: 16px; pointer-events: none;
|
|
3215
|
+
text-anchor: middle; dominant-baseline: central;
|
|
3216
|
+
}
|
|
3217
|
+
.wf-node-badge {
|
|
3218
|
+
font-size: 10px; fill: rgba(255,255,255,0.7);
|
|
3219
|
+
pointer-events: none; text-anchor: middle; dominant-baseline: central;
|
|
3220
|
+
}
|
|
3221
|
+
.wf-node-condition {
|
|
3222
|
+
font-size: 10px; fill: #FFD54F;
|
|
3223
|
+
pointer-events: none; text-anchor: end;
|
|
3224
|
+
}
|
|
3225
|
+
/* Execution states */
|
|
3226
|
+
.wf-node--pending rect { opacity: 0.5; }
|
|
3227
|
+
.wf-node--running rect {
|
|
3228
|
+
animation: wf-pulse 1.2s ease-in-out infinite;
|
|
3229
|
+
}
|
|
3230
|
+
@keyframes wf-pulse {
|
|
3231
|
+
0%, 100% { filter: drop-shadow(0 0 4px rgba(108,99,255,0.4)); }
|
|
3232
|
+
50% { filter: drop-shadow(0 0 12px rgba(108,99,255,0.8)); }
|
|
3233
|
+
}
|
|
3234
|
+
.wf-node--completed rect { opacity: 1; }
|
|
3235
|
+
.wf-node--skipped rect { opacity: 0.3; }
|
|
3236
|
+
.wf-node--error rect { stroke: #e74c3c !important; stroke-width: 3; }
|
|
3237
|
+
.wf-node-status {
|
|
3238
|
+
font-size: 12px; pointer-events: none; fill: #fff;
|
|
3239
|
+
text-anchor: middle; dominant-baseline: central;
|
|
3240
|
+
}
|
|
3241
|
+
.wf-node--error .wf-node-status { fill: #e74c3c; }
|
|
3242
|
+
.wf-node--skipped .wf-node-status { fill: rgba(255,255,255,0.5); }
|
|
3243
|
+
.wf-node-time {
|
|
3244
|
+
font-size: 9px; fill: rgba(255,255,255,0.6);
|
|
3245
|
+
pointer-events: none; text-anchor: middle;
|
|
3246
|
+
}
|
|
3247
|
+
/* Port styles */
|
|
3248
|
+
.wf-port { pointer-events: none; transition: fill 0.2s, stroke 0.2s; }
|
|
3249
|
+
.wf-port-in { fill: var(--bg-card); }
|
|
3250
|
+
.wf-port-out { fill: currentColor; stroke: rgba(255,255,255,0.8); }
|
|
3251
|
+
.wf-node:hover .wf-port-out { filter: brightness(1.3); }
|
|
3252
|
+
.wf-node--completed .wf-port-out { fill: #69F0AE; stroke: #fff; }
|
|
3253
|
+
.wf-node--error .wf-port-in { fill: #e74c3c; }
|
|
3254
|
+
/* Edge styles */
|
|
3255
|
+
.wf-edge {
|
|
3256
|
+
fill: none; stroke: #6b7b8d; stroke-width: 2;
|
|
3257
|
+
opacity: 0.5; transition: opacity 0.3s, stroke 0.3s;
|
|
3258
|
+
stroke-linecap: round;
|
|
3259
|
+
}
|
|
3260
|
+
.wf-edge--backward {
|
|
3261
|
+
stroke-dasharray: 6 4; opacity: 0.35;
|
|
3262
|
+
}
|
|
3263
|
+
.wf-edge--active {
|
|
3264
|
+
stroke: var(--accent, #6c63ff); opacity: 0.85;
|
|
3265
|
+
stroke-dasharray: 8 4;
|
|
3266
|
+
animation: wf-flow 0.6s linear infinite;
|
|
3267
|
+
}
|
|
3268
|
+
@keyframes wf-flow { to { stroke-dashoffset: -12; } }
|
|
3269
|
+
.wf-edge--complete { stroke: var(--accent, #6c63ff); opacity: 0.6; stroke-dasharray: none; }
|
|
3270
|
+
/* Inspector */
|
|
3271
|
+
.wf-inspector {
|
|
3272
|
+
flex-shrink: 0; position: relative;
|
|
3273
|
+
display: flex; flex-direction: row;
|
|
3274
|
+
background: var(--bg);
|
|
3275
|
+
transition: width 0.25s ease;
|
|
3276
|
+
width: 300px;
|
|
3277
|
+
overflow: hidden;
|
|
3278
|
+
align-self: stretch;
|
|
3279
|
+
}
|
|
3280
|
+
.wf-inspector.collapsed {
|
|
3281
|
+
width: 28px;
|
|
3282
|
+
}
|
|
3283
|
+
.wf-inspector-toggle {
|
|
3284
|
+
width: 28px; flex-shrink: 0; align-self: stretch;
|
|
3285
|
+
border: none; border-left: 1px solid var(--border);
|
|
3286
|
+
background: var(--bg); color: var(--text-muted);
|
|
3287
|
+
cursor: pointer; font-size: 14px; padding: 0;
|
|
3288
|
+
display: flex; align-items: center; justify-content: center;
|
|
3289
|
+
transition: color 0.15s, background 0.15s;
|
|
3290
|
+
}
|
|
3291
|
+
.wf-inspector-toggle:hover {
|
|
3292
|
+
color: var(--text); background: var(--bg-card);
|
|
3293
|
+
}
|
|
3294
|
+
.wf-inspector.collapsed .wf-inspector-toggle {
|
|
3295
|
+
border-left: 1px solid var(--border);
|
|
3296
|
+
}
|
|
3297
|
+
.wf-inspector.collapsed .wf-inspector-content {
|
|
3298
|
+
display: none;
|
|
3299
|
+
}
|
|
3300
|
+
.wf-inspector-content {
|
|
3301
|
+
flex: 1; display: flex; flex-direction: column;
|
|
3302
|
+
border-left: 1px solid var(--border);
|
|
3303
|
+
overflow-y: auto; min-width: 0; height: 100%;
|
|
3304
|
+
}
|
|
3305
|
+
.wf-inspector-header {
|
|
3306
|
+
padding: 14px 16px 10px; font-weight: 700; font-size: 13px;
|
|
3307
|
+
color: var(--text); border-bottom: 1px solid var(--border);
|
|
3308
|
+
}
|
|
3309
|
+
.wf-inspector-body {
|
|
3310
|
+
padding: 16px; flex: 1; overflow-y: auto;
|
|
3311
|
+
}
|
|
3312
|
+
.wf-inspector-empty {
|
|
3313
|
+
color: var(--text-muted); font-size: 13px; text-align: center;
|
|
3314
|
+
padding: 40px 16px;
|
|
3315
|
+
}
|
|
3316
|
+
.wf-inspector-section {
|
|
3317
|
+
margin-bottom: 16px;
|
|
3318
|
+
}
|
|
3319
|
+
.wf-inspector-section-title {
|
|
3320
|
+
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
|
3321
|
+
letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 6px;
|
|
3322
|
+
}
|
|
3323
|
+
.wf-inspector-field {
|
|
3324
|
+
font-size: 12px; color: var(--text); margin-bottom: 4px;
|
|
3325
|
+
display: flex; gap: 6px;
|
|
3326
|
+
}
|
|
3327
|
+
.wf-inspector-field-label {
|
|
3328
|
+
font-weight: 600; min-width: 60px; color: var(--text-muted);
|
|
3329
|
+
}
|
|
3330
|
+
.wf-inspector-field-value {
|
|
3331
|
+
word-break: break-all;
|
|
3332
|
+
}
|
|
3333
|
+
.wf-inspector-code {
|
|
3334
|
+
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px;
|
|
3335
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3336
|
+
border-radius: 6px; padding: 8px; overflow-x: auto;
|
|
3337
|
+
white-space: pre-wrap; color: var(--text); margin-top: 4px;
|
|
3338
|
+
}
|
|
3339
|
+
.wf-tool-badge {
|
|
3340
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
3341
|
+
padding: 2px 8px; border-radius: 10px; font-size: 11px;
|
|
3342
|
+
font-weight: 600; color: #fff;
|
|
3343
|
+
}
|
|
3344
|
+
.wf-inspector-input {
|
|
3345
|
+
width: 100%; padding: 6px 10px; border-radius: 6px;
|
|
3346
|
+
border: 1px solid var(--border); background: var(--bg-card);
|
|
3347
|
+
color: var(--text); font-size: 12px; margin-top: 4px;
|
|
3348
|
+
}
|
|
3349
|
+
.wf-inspector-input:focus {
|
|
3350
|
+
outline: none; border-color: var(--accent, #6c63ff);
|
|
3351
|
+
}
|
|
3352
|
+
.wf-inspector-btn {
|
|
3353
|
+
width: 100%; padding: 8px 16px; border-radius: 8px;
|
|
3354
|
+
border: none; background: var(--accent, #6c63ff);
|
|
3355
|
+
color: #fff; font-weight: 600; font-size: 13px;
|
|
3356
|
+
cursor: pointer; margin-top: 12px;
|
|
3357
|
+
}
|
|
3358
|
+
.wf-inspector-btn:hover { opacity: 0.9; }
|
|
3359
|
+
.wf-inspector-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
3360
|
+
.wf-inspector-result {
|
|
3361
|
+
margin-top: 8px; padding: 8px; border-radius: 6px;
|
|
3362
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
3363
|
+
font-size: 11px; max-height: 200px; overflow-y: auto;
|
|
3364
|
+
}
|
|
3365
|
+
.wf-inspector-result.success { border-color: #69F0AE; }
|
|
3366
|
+
.wf-inspector-result.error { border-color: #e74c3c; }
|
|
3367
|
+
/* Responsive */
|
|
3368
|
+
@media (max-width: 900px) {
|
|
3369
|
+
.wf-library { display: none; }
|
|
3370
|
+
.wf-inspector:not(.collapsed) { width: 240px; }
|
|
3371
|
+
}
|
|
3372
|
+
@media (max-width: 600px) {
|
|
3373
|
+
.wf-inspector { display: none; }
|
|
3374
|
+
}
|
|
3375
|
+
/* Workflow visualizer light mode */
|
|
3376
|
+
[data-theme="light"] .wf-node-label { fill: #001E2B; }
|
|
3377
|
+
[data-theme="light"] .wf-node-icon { fill: #001E2B; }
|
|
3378
|
+
[data-theme="light"] .wf-node-badge { fill: rgba(0,30,43,0.55); }
|
|
3379
|
+
[data-theme="light"] .wf-node-time { fill: rgba(0,30,43,0.5); }
|
|
3380
|
+
[data-theme="light"] .wf-node-condition { fill: #944F01; }
|
|
3381
|
+
[data-theme="light"] .wf-node-status { fill: #001E2B; }
|
|
3382
|
+
[data-theme="light"] .wf-node--skipped .wf-node-status { fill: rgba(0,30,43,0.4); }
|
|
3383
|
+
[data-theme="light"] .wf-node--error .wf-node-status { fill: #DB3030; }
|
|
3384
|
+
[data-theme="light"] .wf-node rect { stroke-opacity: 0.7; }
|
|
3385
|
+
[data-theme="light"] .wf-node.selected rect { stroke: var(--accent); }
|
|
3386
|
+
[data-theme="light"] .wf-edge { stroke: #889397; opacity: 0.45; }
|
|
3387
|
+
[data-theme="light"] .wf-edge--backward { opacity: 0.3; }
|
|
3388
|
+
[data-theme="light"] .wf-edge--active { stroke: var(--accent); opacity: 0.7; }
|
|
3389
|
+
[data-theme="light"] .wf-edge--complete { stroke: var(--accent); opacity: 0.5; }
|
|
3390
|
+
[data-theme="light"] .wf-port-in { fill: var(--bg); }
|
|
3391
|
+
[data-theme="light"] .wf-port-out { stroke: rgba(0,30,43,0.3); }
|
|
3392
|
+
[data-theme="light"] .wf-node--completed .wf-port-out { fill: #009E80; stroke: var(--bg); }
|
|
3393
|
+
[data-theme="light"] .wf-canvas-area { background: var(--bg-surface); }
|
|
3394
|
+
[data-theme="light"] .wf-inspector-result.success { border-color: #009E80; }
|
|
3395
|
+
[data-theme="light"] .wf-inspector-result.error { border-color: #DB3030; }
|
|
3396
|
+
[data-theme="light"] .wf-exec-status { box-shadow: 0 2px 8px rgba(0,30,43,0.08); }
|
|
3397
|
+
[data-theme="light"] .wf-run-btn { background: var(--accent); }
|
|
3398
|
+
[data-theme="light"] .wf-output-modal { box-shadow: 0 8px 32px rgba(0,30,43,0.15); }
|
|
3399
|
+
@keyframes wf-pulse-light {
|
|
3400
|
+
0%, 100% { filter: drop-shadow(0 0 4px rgba(0,158,128,0.3)); }
|
|
3401
|
+
50% { filter: drop-shadow(0 0 12px rgba(0,158,128,0.6)); }
|
|
3402
|
+
}
|
|
3403
|
+
[data-theme="light"] .wf-node--running rect { animation-name: wf-pulse-light; }
|
|
3404
|
+
/* Empty canvas state */
|
|
3405
|
+
.wf-canvas-empty {
|
|
3406
|
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
3407
|
+
text-align: center; color: var(--text-muted); pointer-events: none;
|
|
3408
|
+
}
|
|
3409
|
+
.wf-canvas-empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; }
|
|
3410
|
+
.wf-canvas-empty-text { font-size: 14px; }
|
|
2651
3411
|
</style>
|
|
2652
3412
|
</head>
|
|
2653
3413
|
<body>
|
|
@@ -2699,6 +3459,9 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2699
3459
|
<symbol id="lg-shield" viewBox="0 0 16 16">
|
|
2700
3460
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.35 1.18a.75.75 0 0 0-.7 0l-5 2.7A.75.75 0 0 0 2.25 4.5V8c0 2.9 2.1 5.5 5.5 6.95a.75.75 0 0 0 .5 0C11.65 13.5 13.75 10.9 13.75 8V4.5a.75.75 0 0 0-.4-.62l-5-2.7zM8 3.2 3.75 5.5V8c0 2.2 1.6 4.2 4.25 5.45C10.65 12.2 12.25 10.2 12.25 8V5.5L8 3.2z" fill="currentColor"/>
|
|
2701
3461
|
</symbol>
|
|
3462
|
+
<symbol id="lg-pulse" viewBox="0 0 16 16">
|
|
3463
|
+
<path d="M1 8h3l2-5 2 10 2-5h5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3464
|
+
</symbol>
|
|
2702
3465
|
</svg>
|
|
2703
3466
|
|
|
2704
3467
|
<div class="app-shell">
|
|
@@ -2719,6 +3482,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2719
3482
|
<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>
|
|
2720
3483
|
<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>
|
|
2721
3484
|
<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 width="16" height="16" viewBox="0 0 16 16"><path d="M2 2h12v9H5l-3 3V2z" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></span><span>Chat</span></button>
|
|
3485
|
+
<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 width="16" height="16" viewBox="0 0 16 16"><circle cx="3" cy="8" r="2" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="13" cy="4" r="2" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="13" cy="12" r="2" fill="none" stroke="currentColor" stroke-width="1.3"/><line x1="5" y1="7" x2="11" y2="4.5" stroke="currentColor" stroke-width="1.3"/><line x1="5" y1="9" x2="11" y2="11.5" stroke="currentColor" stroke-width="1.3"/></svg></span><span>Workflows</span></button>
|
|
2722
3486
|
</div>
|
|
2723
3487
|
<div class="sidebar-nav-divider"></div>
|
|
2724
3488
|
<div class="sidebar-nav-group" role="tablist" aria-label="Learn">
|
|
@@ -2738,10 +3502,16 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2738
3502
|
</div>
|
|
2739
3503
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
2740
3504
|
<div id="appVersionLabel" style="font-size:10px;color:var(--text-muted);"></div>
|
|
2741
|
-
<
|
|
2742
|
-
<
|
|
2743
|
-
|
|
2744
|
-
|
|
3505
|
+
<div style="display:flex;align-items:center;gap:6px;">
|
|
3506
|
+
<a class="sidebar-docs-link" href="https://docs.vaicli.com" target="_blank" rel="noopener" title="Documentation (F1)">
|
|
3507
|
+
<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>
|
|
3508
|
+
<span>Docs</span>
|
|
3509
|
+
</a>
|
|
3510
|
+
<button class="sidebar-bug-link" id="bugButton" title="Report a Bug">
|
|
3511
|
+
<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>
|
|
3512
|
+
<span class="sidebar-bug-label">Bug</span>
|
|
3513
|
+
</button>
|
|
3514
|
+
</div>
|
|
2745
3515
|
</div>
|
|
2746
3516
|
</div>
|
|
2747
3517
|
</aside>
|
|
@@ -2771,6 +3541,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2771
3541
|
<h2 class="page-header-title">Embed</h2>
|
|
2772
3542
|
<p class="page-header-subtitle">Generate vector embeddings for text</p>
|
|
2773
3543
|
<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>
|
|
3544
|
+
<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>
|
|
2774
3545
|
</div>
|
|
2775
3546
|
<div class="card">
|
|
2776
3547
|
<div class="card-title">Input Text</div>
|
|
@@ -2834,7 +3605,8 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2834
3605
|
<div class="page-header">
|
|
2835
3606
|
<h2 class="page-header-title">Compare</h2>
|
|
2836
3607
|
<p class="page-header-subtitle">Visualize similarity between text pairs</p>
|
|
2837
|
-
<p class="page-header-hint">Enter two texts and compare their embeddings
|
|
3608
|
+
<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>
|
|
3609
|
+
<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>
|
|
2838
3610
|
</div>
|
|
2839
3611
|
<div class="compare-grid">
|
|
2840
3612
|
<div class="card">
|
|
@@ -2888,7 +3660,8 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
2888
3660
|
<div class="page-header">
|
|
2889
3661
|
<h2 class="page-header-title">Rerank</h2>
|
|
2890
3662
|
<p class="page-header-subtitle">Re-order documents by relevance to a query</p>
|
|
2891
|
-
<p class="page-header-hint">Enter a search query and a set of documents
|
|
3663
|
+
<p class="page-header-hint">Enter a search query and a set of documents: the reranker scores and sorts them by semantic relevance.</p>
|
|
3664
|
+
<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>
|
|
2892
3665
|
</div>
|
|
2893
3666
|
<div class="card">
|
|
2894
3667
|
<div class="card-title">Query</div>
|
|
@@ -2939,7 +3712,8 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
2939
3712
|
<div class="page-header">
|
|
2940
3713
|
<h2 class="page-header-title">Multimodal</h2>
|
|
2941
3714
|
<p class="page-header-subtitle">Compare images and text in the same vector space</p>
|
|
2942
|
-
<p class="page-header-hint">Voyage AI's multimodal models embed images and text into a unified vector space
|
|
3715
|
+
<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>
|
|
3716
|
+
<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>
|
|
2943
3717
|
</div>
|
|
2944
3718
|
|
|
2945
3719
|
<!-- Section A: Image ↔ Text Similarity -->
|
|
@@ -2949,7 +3723,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
2949
3723
|
<div class="mm-drop-zone" id="mmDropZone">
|
|
2950
3724
|
<div class="mm-drop-icon">🖼️</div>
|
|
2951
3725
|
<div class="mm-drop-text">Drop an image here or click to browse</div>
|
|
2952
|
-
<div class="mm-drop-hint">PNG, JPEG, WebP, GIF
|
|
3726
|
+
<div class="mm-drop-hint">PNG, JPEG, WebP, GIF, max 20 MB. Paste from clipboard (⌘V)</div>
|
|
2953
3727
|
</div>
|
|
2954
3728
|
<input type="file" id="mmFileInput" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none">
|
|
2955
3729
|
<div class="mm-preview" id="mmPreview">
|
|
@@ -3055,6 +3829,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
3055
3829
|
<h2 class="page-header-title">Benchmark</h2>
|
|
3056
3830
|
<p class="page-header-subtitle">Compare model speed, cost, and quality</p>
|
|
3057
3831
|
<p class="page-header-hint">Run latency tests, compare ranking accuracy, analyze quantization trade-offs, and estimate costs across models.</p>
|
|
3832
|
+
<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>
|
|
3058
3833
|
</div>
|
|
3059
3834
|
|
|
3060
3835
|
<!-- Sub-panel switcher -->
|
|
@@ -3562,6 +4337,80 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3562
4337
|
</div>
|
|
3563
4338
|
</div>
|
|
3564
4339
|
|
|
4340
|
+
<!-- ========== WORKFLOWS TAB ========== -->
|
|
4341
|
+
<div class="tab-panel" id="tab-workflows" role="tabpanel" aria-labelledby="tab-btn-workflows" tabindex="0">
|
|
4342
|
+
<div class="wf-container">
|
|
4343
|
+
<div class="wf-library">
|
|
4344
|
+
<div class="wf-library-header">
|
|
4345
|
+
<span>Workflows</span>
|
|
4346
|
+
</div>
|
|
4347
|
+
<div class="wf-library-list" id="wfLibraryList">
|
|
4348
|
+
<div style="padding: 16px; color: var(--text-muted); font-size: 12px;">Loading...</div>
|
|
4349
|
+
</div>
|
|
4350
|
+
<div class="wf-library-footer">
|
|
4351
|
+
<button class="wf-load-file-btn" onclick="wfLoadFromFile()" title="Load workflow JSON from file">
|
|
4352
|
+
<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>
|
|
4353
|
+
Load File
|
|
4354
|
+
</button>
|
|
4355
|
+
<input type="file" id="wfFileInput" accept=".json" style="display:none;" onchange="wfHandleFileLoad(event)">
|
|
4356
|
+
</div>
|
|
4357
|
+
</div>
|
|
4358
|
+
<div class="wf-canvas-area">
|
|
4359
|
+
<div class="wf-canvas-toolbar" id="wfToolbar">
|
|
4360
|
+
<button onclick="wfZoom(1)" title="Zoom in">+</button>
|
|
4361
|
+
<button onclick="wfZoom(-1)" title="Zoom out">−</button>
|
|
4362
|
+
<button onclick="wfFitToView()" title="Fit to view">⊞</button>
|
|
4363
|
+
<button onclick="wfResetExecution()" title="Reset">↻</button>
|
|
4364
|
+
<span class="wf-toolbar-sep"></span>
|
|
4365
|
+
<button class="wf-plan-btn" onclick="wfDryRun()" id="wfDryRunBtn" disabled title="Dry run: show execution plan">⚙ Plan</button>
|
|
4366
|
+
<button class="wf-run-btn" id="wfRunBtn" onclick="wfExecute()" disabled title="Run workflow">▶ Run</button>
|
|
4367
|
+
<button class="wf-stop-btn" id="wfStopBtn" onclick="wfStopExecution()" style="display:none;" title="Stop workflow">■ Stop</button>
|
|
4368
|
+
<span class="wf-toolbar-sep"></span>
|
|
4369
|
+
<button onclick="wfExportJson()" id="wfExportBtn" disabled title="Export workflow JSON">
|
|
4370
|
+
<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>
|
|
4371
|
+
</button>
|
|
4372
|
+
</div>
|
|
4373
|
+
<!-- Execution status bar -->
|
|
4374
|
+
<div class="wf-exec-status" id="wfExecStatus" style="display:none;">
|
|
4375
|
+
<span class="wf-exec-status-dot"></span>
|
|
4376
|
+
<span class="wf-exec-status-text" id="wfExecStatusText">Running...</span>
|
|
4377
|
+
<span class="wf-exec-status-time" id="wfExecStatusTime"></span>
|
|
4378
|
+
</div>
|
|
4379
|
+
<div class="wf-canvas-empty" id="wfCanvasEmpty">
|
|
4380
|
+
<div class="wf-canvas-empty-icon">⚙</div>
|
|
4381
|
+
<div class="wf-canvas-empty-text">Select a workflow from the library</div>
|
|
4382
|
+
</div>
|
|
4383
|
+
<svg id="wf-canvas" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
4384
|
+
</div>
|
|
4385
|
+
<div class="wf-inspector collapsed" id="wfInspector">
|
|
4386
|
+
<button class="wf-inspector-toggle" id="wfInspectorToggle" onclick="wfToggleInspector()" title="Toggle inspector">‹</button>
|
|
4387
|
+
<div class="wf-inspector-content">
|
|
4388
|
+
<div class="wf-inspector-header" id="wfInspectorHeader">Inspector</div>
|
|
4389
|
+
<div class="wf-inspector-body" id="wfInspectorBody">
|
|
4390
|
+
<div class="wf-inspector-empty">Click a node to inspect</div>
|
|
4391
|
+
</div>
|
|
4392
|
+
</div>
|
|
4393
|
+
</div>
|
|
4394
|
+
</div>
|
|
4395
|
+
</div>
|
|
4396
|
+
|
|
4397
|
+
<!-- ── Workflow Output Modal ── -->
|
|
4398
|
+
<div class="wf-output-modal-backdrop" id="wfOutputModalBackdrop" style="display:none;" onclick="wfCloseOutputModal()">
|
|
4399
|
+
<div class="wf-output-modal" onclick="event.stopPropagation()">
|
|
4400
|
+
<div class="wf-output-modal-header">
|
|
4401
|
+
<span class="wf-output-modal-title" id="wfOutputModalTitle">Output</span>
|
|
4402
|
+
<div class="wf-output-modal-actions">
|
|
4403
|
+
<button class="wf-output-modal-btn" onclick="wfCopyOutput()" title="Copy to clipboard" id="wfOutputCopyBtn">
|
|
4404
|
+
<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>
|
|
4405
|
+
<span id="wfOutputCopyLabel">Copy</span>
|
|
4406
|
+
</button>
|
|
4407
|
+
<button class="wf-output-modal-btn close" onclick="wfCloseOutputModal()" title="Close">×</button>
|
|
4408
|
+
</div>
|
|
4409
|
+
</div>
|
|
4410
|
+
<pre class="wf-output-modal-body" id="wfOutputModalBody"></pre>
|
|
4411
|
+
</div>
|
|
4412
|
+
</div>
|
|
4413
|
+
|
|
3565
4414
|
<!-- ========== ABOUT TAB ========== -->
|
|
3566
4415
|
<div class="tab-panel" id="tab-about" role="tabpanel" aria-labelledby="tab-btn-about" tabindex="0">
|
|
3567
4416
|
<div class="about-container">
|
|
@@ -3607,8 +4456,11 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3607
4456
|
<strong>⚖️ Compare</strong> — Measure similarity with cosine, dot product & euclidean distance<br>
|
|
3608
4457
|
<strong>🔍 Search</strong> — Semantic search with optional reranking<br>
|
|
3609
4458
|
<strong>🔮 Multimodal</strong> — Compare images and text in the same vector space with voyage-multimodal-3.5<br>
|
|
4459
|
+
<strong>🛠️ Generate</strong> — Generate code snippets and scaffold full projects with templates<br>
|
|
4460
|
+
<strong>💬 Chat</strong> — RAG-powered chat with your documents, configurable system prompts<br>
|
|
4461
|
+
<strong>🔄 Workflows</strong> — Multi-step agent workflows with thinking panels and tool orchestration<br>
|
|
3610
4462
|
<strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
|
|
3611
|
-
<strong>📚 Explore</strong> —
|
|
4463
|
+
<strong>📚 Explore</strong> — 22 interactive concepts covering embeddings, vector search, multimodal, RAG, and more
|
|
3612
4464
|
</div>
|
|
3613
4465
|
</div>
|
|
3614
4466
|
|
|
@@ -3639,9 +4491,12 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3639
4491
|
<div class="about-section" style="padding-bottom:0;">
|
|
3640
4492
|
<div class="about-section-title">What's New</div>
|
|
3641
4493
|
<div class="about-text" style="font-size:13px;">
|
|
3642
|
-
<strong>v1.
|
|
3643
|
-
|
|
3644
|
-
|
|
4494
|
+
<strong>v1.26</strong> — Agent workflows with thinking panel & markdown rendering, multi-step tool orchestration<br>
|
|
4495
|
+
<strong>v1.25</strong> — Code generation & project scaffolding tabs<br>
|
|
4496
|
+
<strong>v1.24</strong> — MCP server install/uninstall/status commands, Electron app v1.5<br>
|
|
4497
|
+
<strong>v1.23</strong> — MCP server (expose vai tools to AI agents), HTTP transport, bearer auth, 71+ MCP tests<br>
|
|
4498
|
+
<strong>v1.22</strong> — RAG chat with smart source labels, configurable system prompts, streaming responses<br>
|
|
4499
|
+
<strong>v1.2</strong> — Multimodal tab, 4 new Explore concepts, auto-update, hidden easter egg 🕹️
|
|
3645
4500
|
</div>
|
|
3646
4501
|
</div>
|
|
3647
4502
|
</div>
|
|
@@ -3658,6 +4513,7 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3658
4513
|
<div class="page-header">
|
|
3659
4514
|
<h2 class="page-header-title">Generate</h2>
|
|
3660
4515
|
<p class="page-header-subtitle">Generate code and scaffold projects</p>
|
|
4516
|
+
<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>
|
|
3661
4517
|
</div>
|
|
3662
4518
|
|
|
3663
4519
|
<!-- Mode Toggle -->
|
|
@@ -3831,7 +4687,8 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3831
4687
|
<div class="page-header">
|
|
3832
4688
|
<h2 class="page-header-title">Explore</h2>
|
|
3833
4689
|
<p class="page-header-subtitle">Learn embedding and vector search concepts</p>
|
|
3834
|
-
<p class="page-header-hint">Browse interactive explanations of key topics
|
|
4690
|
+
<p class="page-header-hint">Browse interactive explanations of key topics, from cosine similarity to quantization to RAG pipelines.</p>
|
|
4691
|
+
<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>
|
|
3835
4692
|
</div>
|
|
3836
4693
|
<div style="margin-bottom:16px;">
|
|
3837
4694
|
<input type="text" id="exploreSearch" placeholder="🔍 Search concepts..." oninput="filterExplore()" style="max-width:400px;" aria-label="Search concepts">
|
|
@@ -3888,6 +4745,10 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
3888
4745
|
<svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-shield"/></svg>
|
|
3889
4746
|
<span>Data & Privacy</span>
|
|
3890
4747
|
</button>
|
|
4748
|
+
<button class="settings-nav-item" data-settings-section="health">
|
|
4749
|
+
<svg width="16" height="16" viewBox="0 0 16 16"><use href="#lg-pulse"/></svg>
|
|
4750
|
+
<span>Health Check</span>
|
|
4751
|
+
</button>
|
|
3891
4752
|
</nav>
|
|
3892
4753
|
|
|
3893
4754
|
<!-- Settings content panels -->
|
|
@@ -4046,6 +4907,39 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4046
4907
|
</select>
|
|
4047
4908
|
</div>
|
|
4048
4909
|
</div>
|
|
4910
|
+
<div class="settings-row" id="chatApiKeyRow" style="display:none">
|
|
4911
|
+
<div class="settings-label">
|
|
4912
|
+
<span class="settings-label-text">API Key</span>
|
|
4913
|
+
<span class="settings-label-hint">Provider API key (stored securely in OS keychain)</span>
|
|
4914
|
+
</div>
|
|
4915
|
+
<div class="settings-control" style="display:flex;gap:8px;align-items:center">
|
|
4916
|
+
<input
|
|
4917
|
+
type="password"
|
|
4918
|
+
class="settings-input"
|
|
4919
|
+
id="chatApiKey"
|
|
4920
|
+
placeholder="Enter your API key"
|
|
4921
|
+
style="flex:1;font-family:monospace;font-size:13px"
|
|
4922
|
+
>
|
|
4923
|
+
<button
|
|
4924
|
+
class="btn btn-secondary"
|
|
4925
|
+
id="chatApiKeyToggle"
|
|
4926
|
+
onclick="toggleChatApiKeyVisibility()"
|
|
4927
|
+
title="Show/hide API key"
|
|
4928
|
+
style="padding:8px 12px;min-width:auto"
|
|
4929
|
+
>
|
|
4930
|
+
👁️
|
|
4931
|
+
</button>
|
|
4932
|
+
<button
|
|
4933
|
+
class="btn"
|
|
4934
|
+
id="chatApiKeySave"
|
|
4935
|
+
onclick="saveChatApiKey()"
|
|
4936
|
+
title="Save API key"
|
|
4937
|
+
style="padding:8px 12px;min-width:auto"
|
|
4938
|
+
>
|
|
4939
|
+
💾
|
|
4940
|
+
</button>
|
|
4941
|
+
</div>
|
|
4942
|
+
</div>
|
|
4049
4943
|
</div>
|
|
4050
4944
|
<div class="settings-section">
|
|
4051
4945
|
<div class="settings-section-title">Knowledge Base</div>
|
|
@@ -4085,6 +4979,18 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4085
4979
|
<button class="settings-toggle active" id="chatRerank" type="button"></button>
|
|
4086
4980
|
</div>
|
|
4087
4981
|
</div>
|
|
4982
|
+
<div class="settings-row">
|
|
4983
|
+
<div class="settings-label">
|
|
4984
|
+
<span class="settings-label-text">Chat Mode</span>
|
|
4985
|
+
<span class="settings-label-hint">Pipeline: fixed RAG retrieval. Agent: LLM chooses which tools to call.</span>
|
|
4986
|
+
</div>
|
|
4987
|
+
<div class="settings-control">
|
|
4988
|
+
<select class="settings-select" id="chatMode">
|
|
4989
|
+
<option value="pipeline">Pipeline (fixed RAG)</option>
|
|
4990
|
+
<option value="agent">Agent (tool-calling)</option>
|
|
4991
|
+
</select>
|
|
4992
|
+
</div>
|
|
4993
|
+
</div>
|
|
4088
4994
|
</div>
|
|
4089
4995
|
<div class="settings-section">
|
|
4090
4996
|
<div class="settings-section-title">Custom Instructions</div>
|
|
@@ -4202,6 +5108,30 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
4202
5108
|
</div>
|
|
4203
5109
|
</div>
|
|
4204
5110
|
|
|
5111
|
+
<!-- ── Health Check ── -->
|
|
5112
|
+
<div class="settings-panel" id="settings-health">
|
|
5113
|
+
<div class="settings-panel-header">
|
|
5114
|
+
<h3 class="settings-panel-title">Health Check</h3>
|
|
5115
|
+
<p class="settings-panel-subtitle">Validate your vai setup and connectivity</p>
|
|
5116
|
+
</div>
|
|
5117
|
+
<div class="settings-section">
|
|
5118
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
|
5119
|
+
<span class="settings-section-title" style="margin:0;">Diagnostics</span>
|
|
5120
|
+
<button class="btn" id="doctorRunBtn" style="padding:6px 16px;font-size:13px;">Run Health Check</button>
|
|
5121
|
+
</div>
|
|
5122
|
+
<div id="doctorResults" style="display:none;">
|
|
5123
|
+
<div id="doctorCheckList" style="display:flex;flex-direction:column;gap:6px;"></div>
|
|
5124
|
+
<div id="doctorSummary" style="margin-top:16px;padding:12px 16px;border-radius:var(--radius);font-size:13px;"></div>
|
|
5125
|
+
</div>
|
|
5126
|
+
<div id="doctorLoading" style="display:none;text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">
|
|
5127
|
+
Running health checks...
|
|
5128
|
+
</div>
|
|
5129
|
+
<div id="doctorEmpty" style="text-align:center;padding:24px 0;color:var(--text-muted);font-size:13px;">
|
|
5130
|
+
Click "Run Health Check" to validate your setup.
|
|
5131
|
+
</div>
|
|
5132
|
+
</div>
|
|
5133
|
+
</div>
|
|
5134
|
+
|
|
4205
5135
|
<div style="text-align:center;padding:8px 0;">
|
|
4206
5136
|
<span class="settings-saved" id="settingsSavedMsg">✓ Saved</span>
|
|
4207
5137
|
</div>
|
|
@@ -5169,7 +6099,9 @@ async function loadConcepts() {
|
|
|
5169
6099
|
}
|
|
5170
6100
|
|
|
5171
6101
|
function escapeHtml(str) {
|
|
5172
|
-
|
|
6102
|
+
if (str == null) return '';
|
|
6103
|
+
const s = typeof str === 'string' ? str : String(str);
|
|
6104
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
5173
6105
|
}
|
|
5174
6106
|
|
|
5175
6107
|
function buildExploreCards() {
|
|
@@ -6497,6 +7429,8 @@ function initSettings() {
|
|
|
6497
7429
|
}
|
|
6498
7430
|
const chatModel = document.getElementById('chatModel');
|
|
6499
7431
|
if (chatModel) chatModel.addEventListener('change', saveChatSettings);
|
|
7432
|
+
const chatMode = document.getElementById('chatMode');
|
|
7433
|
+
if (chatMode) chatMode.addEventListener('change', saveChatSettings);
|
|
6500
7434
|
const chatMaxDocs = document.getElementById('chatMaxDocs');
|
|
6501
7435
|
if (chatMaxDocs) chatMaxDocs.addEventListener('change', saveChatSettings);
|
|
6502
7436
|
|
|
@@ -6508,11 +7442,117 @@ function initSettings() {
|
|
|
6508
7442
|
const chatSystemPrompt = document.getElementById('chatSystemPrompt');
|
|
6509
7443
|
if (chatSystemPrompt) chatSystemPrompt.addEventListener('input', saveChatSettingsDebounced);
|
|
6510
7444
|
|
|
7445
|
+
// API key input listeners
|
|
7446
|
+
const chatApiKey = document.getElementById('chatApiKey');
|
|
7447
|
+
if (chatApiKey) {
|
|
7448
|
+
// Mark as modified when user types
|
|
7449
|
+
chatApiKey.addEventListener('input', () => {
|
|
7450
|
+
delete chatApiKey.dataset.isMasked;
|
|
7451
|
+
delete chatApiKey.dataset.actualKey;
|
|
7452
|
+
});
|
|
7453
|
+
// Save on Enter key
|
|
7454
|
+
chatApiKey.addEventListener('keydown', (e) => {
|
|
7455
|
+
if (e.key === 'Enter') {
|
|
7456
|
+
e.preventDefault();
|
|
7457
|
+
saveChatApiKey();
|
|
7458
|
+
}
|
|
7459
|
+
});
|
|
7460
|
+
}
|
|
7461
|
+
|
|
6511
7462
|
// Set up settings sub-navigation
|
|
6512
7463
|
setupSettingsNav();
|
|
7464
|
+
|
|
7465
|
+
// Set up doctor health check button
|
|
7466
|
+
setupDoctorPanel();
|
|
6513
7467
|
}
|
|
6514
7468
|
|
|
6515
|
-
// ──
|
|
7469
|
+
// ── Doctor Health Check ──
|
|
7470
|
+
function setupDoctorPanel() {
|
|
7471
|
+
const runBtn = document.getElementById('doctorRunBtn');
|
|
7472
|
+
if (!runBtn) return;
|
|
7473
|
+
runBtn.addEventListener('click', runDoctorCheck);
|
|
7474
|
+
}
|
|
7475
|
+
|
|
7476
|
+
async function runDoctorCheck() {
|
|
7477
|
+
const btn = document.getElementById('doctorRunBtn');
|
|
7478
|
+
const loading = document.getElementById('doctorLoading');
|
|
7479
|
+
const results = document.getElementById('doctorResults');
|
|
7480
|
+
const empty = document.getElementById('doctorEmpty');
|
|
7481
|
+
const checkList = document.getElementById('doctorCheckList');
|
|
7482
|
+
const summary = document.getElementById('doctorSummary');
|
|
7483
|
+
|
|
7484
|
+
btn.disabled = true;
|
|
7485
|
+
btn.textContent = 'Running...';
|
|
7486
|
+
empty.style.display = 'none';
|
|
7487
|
+
results.style.display = 'none';
|
|
7488
|
+
loading.style.display = 'block';
|
|
7489
|
+
|
|
7490
|
+
try {
|
|
7491
|
+
const res = await fetch('/api/doctor');
|
|
7492
|
+
const data = await res.json();
|
|
7493
|
+
|
|
7494
|
+
checkList.innerHTML = '';
|
|
7495
|
+
let hasError = false;
|
|
7496
|
+
let hasWarning = false;
|
|
7497
|
+
|
|
7498
|
+
for (const [key, check] of Object.entries(data)) {
|
|
7499
|
+
let icon, badgeClass, badgeText;
|
|
7500
|
+
if (check.ok === true) {
|
|
7501
|
+
icon = '✓';
|
|
7502
|
+
badgeClass = 'pass';
|
|
7503
|
+
badgeText = 'pass';
|
|
7504
|
+
} else if (check.ok === false) {
|
|
7505
|
+
icon = '✗';
|
|
7506
|
+
if (check.required) { hasError = true; badgeClass = 'fail'; badgeText = 'fail'; }
|
|
7507
|
+
else { hasWarning = true; badgeClass = 'warn'; badgeText = 'warn'; }
|
|
7508
|
+
} else {
|
|
7509
|
+
icon = '⚠';
|
|
7510
|
+
hasWarning = true;
|
|
7511
|
+
badgeClass = 'optional';
|
|
7512
|
+
badgeText = 'optional';
|
|
7513
|
+
}
|
|
7514
|
+
|
|
7515
|
+
const el = document.createElement('div');
|
|
7516
|
+
el.className = 'doctor-check';
|
|
7517
|
+
el.innerHTML = `
|
|
7518
|
+
<span class="doctor-check-icon" style="color:var(--${badgeClass === 'pass' ? 'success' : badgeClass === 'fail' ? 'error' : 'warning'})">${icon}</span>
|
|
7519
|
+
<div class="doctor-check-body">
|
|
7520
|
+
<div class="doctor-check-name">${check.name}</div>
|
|
7521
|
+
<div class="doctor-check-msg">${check.message || ''}</div>
|
|
7522
|
+
${check.hint ? `<div class="doctor-check-hint">${check.hint}</div>` : ''}
|
|
7523
|
+
</div>
|
|
7524
|
+
<span class="doctor-check-badge ${badgeClass}">${badgeText}</span>
|
|
7525
|
+
`;
|
|
7526
|
+
checkList.appendChild(el);
|
|
7527
|
+
}
|
|
7528
|
+
|
|
7529
|
+
if (hasError) {
|
|
7530
|
+
summary.style.background = 'rgba(255,105,96,0.08)';
|
|
7531
|
+
summary.style.color = 'var(--error)';
|
|
7532
|
+
summary.textContent = 'Some required checks failed. Fix the issues above to use vai.';
|
|
7533
|
+
} else if (hasWarning) {
|
|
7534
|
+
summary.style.background = 'rgba(255,192,16,0.08)';
|
|
7535
|
+
summary.style.color = 'var(--warning)';
|
|
7536
|
+
summary.textContent = 'All required checks passed. Some optional features are not configured.';
|
|
7537
|
+
} else {
|
|
7538
|
+
summary.style.background = 'rgba(0,212,170,0.08)';
|
|
7539
|
+
summary.style.color = 'var(--success)';
|
|
7540
|
+
summary.textContent = 'All checks passed. vai is ready to use!';
|
|
7541
|
+
}
|
|
7542
|
+
|
|
7543
|
+
loading.style.display = 'none';
|
|
7544
|
+
results.style.display = 'block';
|
|
7545
|
+
} catch (err) {
|
|
7546
|
+
loading.style.display = 'none';
|
|
7547
|
+
empty.style.display = 'block';
|
|
7548
|
+
empty.textContent = 'Health check failed: ' + err.message;
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
btn.disabled = false;
|
|
7552
|
+
btn.textContent = 'Run Health Check';
|
|
7553
|
+
}
|
|
7554
|
+
|
|
7555
|
+
// ── Update Checker ──
|
|
6516
7556
|
function checkForAppUpdate() {
|
|
6517
7557
|
if (!window.vai || !window.vai.isElectron) return;
|
|
6518
7558
|
|
|
@@ -6701,6 +7741,13 @@ function initOnboarding() {
|
|
|
6701
7741
|
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.',
|
|
6702
7742
|
arrow: 'left',
|
|
6703
7743
|
},
|
|
7744
|
+
{
|
|
7745
|
+
target: '[data-tab="workflows"]',
|
|
7746
|
+
icon: '\u2699\uFE0F',
|
|
7747
|
+
title: 'Workflow Visualizer',
|
|
7748
|
+
body: '<strong>Visualize and execute workflows</strong> as interactive DAGs. Browse built-in templates, inspect step configuration, and watch execution animate in real time.',
|
|
7749
|
+
arrow: 'left',
|
|
7750
|
+
},
|
|
6704
7751
|
{
|
|
6705
7752
|
target: '[data-tab="benchmark"]',
|
|
6706
7753
|
icon: '⏱️',
|
|
@@ -6881,10 +7928,29 @@ function initMultimodal() {
|
|
|
6881
7928
|
const dropZone = document.getElementById('mmDropZone');
|
|
6882
7929
|
const fileInput = document.getElementById('mmFileInput');
|
|
6883
7930
|
|
|
6884
|
-
// Click to browse
|
|
6885
|
-
|
|
7931
|
+
// Click to browse — in Electron, use native dialog (more reliable);
|
|
7932
|
+
// in browser, use <input type="file"> with a re-trigger guard.
|
|
7933
|
+
let fileDialogOpen = false;
|
|
7934
|
+
dropZone.addEventListener('click', async () => {
|
|
7935
|
+
if (fileDialogOpen) return;
|
|
7936
|
+
fileDialogOpen = true;
|
|
7937
|
+
try {
|
|
7938
|
+
if (window.vai && window.vai.isElectron && window.vai.openImageDialog) {
|
|
7939
|
+
const result = await window.vai.openImageDialog();
|
|
7940
|
+
if (!result.canceled && result.dataUrl) {
|
|
7941
|
+
handleMultimodalImageFromData(result.dataUrl, result.name, result.size);
|
|
7942
|
+
}
|
|
7943
|
+
} else {
|
|
7944
|
+
fileInput.click();
|
|
7945
|
+
}
|
|
7946
|
+
} finally {
|
|
7947
|
+
setTimeout(() => { fileDialogOpen = false; }, 300);
|
|
7948
|
+
}
|
|
7949
|
+
});
|
|
6886
7950
|
fileInput.addEventListener('change', (e) => {
|
|
7951
|
+
fileDialogOpen = false;
|
|
6887
7952
|
if (e.target.files && e.target.files[0]) handleMultimodalImage(e.target.files[0]);
|
|
7953
|
+
fileInput.value = '';
|
|
6888
7954
|
});
|
|
6889
7955
|
|
|
6890
7956
|
// Drag and drop
|
|
@@ -6936,6 +8002,23 @@ function initMultimodal() {
|
|
|
6936
8002
|
});
|
|
6937
8003
|
}
|
|
6938
8004
|
|
|
8005
|
+
// Handle image from Electron native dialog (already has dataUrl)
|
|
8006
|
+
function handleMultimodalImageFromData(dataUrl, name, size) {
|
|
8007
|
+
mmImageData = dataUrl;
|
|
8008
|
+
const img = document.getElementById('mmPreviewImg');
|
|
8009
|
+
img.src = mmImageData;
|
|
8010
|
+
img.onload = () => {
|
|
8011
|
+
const info = document.getElementById('mmFileInfo');
|
|
8012
|
+
const sizeStr = size > 1024 * 1024
|
|
8013
|
+
? (size / (1024 * 1024)).toFixed(1) + ' MB'
|
|
8014
|
+
: (size / 1024).toFixed(0) + ' KB';
|
|
8015
|
+
info.textContent = `${name} · ${img.naturalWidth}×${img.naturalHeight} · ${sizeStr}`;
|
|
8016
|
+
};
|
|
8017
|
+
document.getElementById('mmDropZone').style.display = 'none';
|
|
8018
|
+
document.getElementById('mmPreview').classList.add('visible');
|
|
8019
|
+
hideError('mmError');
|
|
8020
|
+
}
|
|
8021
|
+
|
|
6939
8022
|
function handleMultimodalImage(file) {
|
|
6940
8023
|
const VALID_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
6941
8024
|
if (!VALID_TYPES.includes(file.type)) {
|
|
@@ -7073,13 +8156,33 @@ function renderGalleryGrid() {
|
|
|
7073
8156
|
const addSlot = document.createElement('div');
|
|
7074
8157
|
addSlot.className = 'mm-gallery-slot';
|
|
7075
8158
|
addSlot.innerHTML = '<span class="mm-slot-add">+</span>';
|
|
7076
|
-
addSlot.addEventListener('click', () => {
|
|
7077
|
-
|
|
8159
|
+
addSlot.addEventListener('click', async () => {
|
|
8160
|
+
if (window._galleryDialogOpen) return;
|
|
8161
|
+
window._galleryDialogOpen = true;
|
|
8162
|
+
try {
|
|
8163
|
+
if (window.vai && window.vai.isElectron && window.vai.openImageDialog) {
|
|
8164
|
+
const result = await window.vai.openImageDialog();
|
|
8165
|
+
if (!result.canceled && result.dataUrl) {
|
|
8166
|
+
addGalleryImageFromData(result.dataUrl, result.name, result.size);
|
|
8167
|
+
}
|
|
8168
|
+
} else {
|
|
8169
|
+
document.getElementById('mmGalleryFileInput').click();
|
|
8170
|
+
}
|
|
8171
|
+
} finally {
|
|
8172
|
+
setTimeout(() => { window._galleryDialogOpen = false; }, 300);
|
|
8173
|
+
}
|
|
7078
8174
|
});
|
|
7079
8175
|
grid.appendChild(addSlot);
|
|
7080
8176
|
}
|
|
7081
8177
|
}
|
|
7082
8178
|
|
|
8179
|
+
// Add gallery image from Electron native dialog (already has dataUrl)
|
|
8180
|
+
function addGalleryImageFromData(dataUrl, name, size) {
|
|
8181
|
+
if (mmGalleryImages.length >= 6) return;
|
|
8182
|
+
mmGalleryImages.push({ dataUrl, name, size });
|
|
8183
|
+
renderGalleryGrid();
|
|
8184
|
+
}
|
|
8185
|
+
|
|
7083
8186
|
function addGalleryImage(file) {
|
|
7084
8187
|
const VALID_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
7085
8188
|
if (!VALID_TYPES.includes(file.type)) return;
|
|
@@ -7731,6 +8834,7 @@ function saveChatSettings() {
|
|
|
7731
8834
|
maxDocs: parseInt(document.getElementById('chatMaxDocs').value) || 5,
|
|
7732
8835
|
rerank: document.getElementById('chatRerank').classList.contains('active'),
|
|
7733
8836
|
systemPrompt: document.getElementById('chatSystemPrompt').value.trim(),
|
|
8837
|
+
mode: document.getElementById('chatMode').value,
|
|
7734
8838
|
};
|
|
7735
8839
|
fetch('/api/chat/config', {
|
|
7736
8840
|
method: 'POST',
|
|
@@ -7779,6 +8883,7 @@ async function loadChatConfig() {
|
|
|
7779
8883
|
if (data.chat?.maxContextDocs) document.getElementById('chatMaxDocs').value = data.chat.maxContextDocs;
|
|
7780
8884
|
if (data.chat?.rerank === false) document.getElementById('chatRerank').classList.remove('active');
|
|
7781
8885
|
if (data.chat?.systemPrompt) document.getElementById('chatSystemPrompt').value = data.chat.systemPrompt;
|
|
8886
|
+
if (data.mode || data.chat?.mode) document.getElementById('chatMode').value = data.mode || data.chat?.mode || 'pipeline';
|
|
7782
8887
|
updateChatStatus();
|
|
7783
8888
|
// Show not-configured banner if incomplete
|
|
7784
8889
|
const notConfigured = document.getElementById('chatNotConfigured');
|
|
@@ -7792,23 +8897,137 @@ async function loadChatConfig() {
|
|
|
7792
8897
|
}
|
|
7793
8898
|
}
|
|
7794
8899
|
|
|
8900
|
+
// ── Chat API Key Management ──
|
|
8901
|
+
|
|
8902
|
+
async function loadChatApiKey() {
|
|
8903
|
+
if (!window.vai?.llmKey) return;
|
|
8904
|
+
const apiKeyInput = document.getElementById('chatApiKey');
|
|
8905
|
+
if (!apiKeyInput) return;
|
|
8906
|
+
|
|
8907
|
+
try {
|
|
8908
|
+
const key = await window.vai.llmKey.get();
|
|
8909
|
+
if (key) {
|
|
8910
|
+
// Mask the key by default (show first 4 + last 4)
|
|
8911
|
+
const masked = key.slice(0, 4) + '•'.repeat(Math.max(0, key.length - 8)) + key.slice(-4);
|
|
8912
|
+
apiKeyInput.value = masked;
|
|
8913
|
+
apiKeyInput.dataset.actualKey = key;
|
|
8914
|
+
apiKeyInput.dataset.isMasked = 'true';
|
|
8915
|
+
apiKeyInput.type = 'password';
|
|
8916
|
+
} else {
|
|
8917
|
+
apiKeyInput.value = '';
|
|
8918
|
+
delete apiKeyInput.dataset.actualKey;
|
|
8919
|
+
delete apiKeyInput.dataset.isMasked;
|
|
8920
|
+
}
|
|
8921
|
+
} catch (err) {
|
|
8922
|
+
console.error('Failed to load LLM API key:', err);
|
|
8923
|
+
}
|
|
8924
|
+
}
|
|
8925
|
+
|
|
8926
|
+
function toggleChatApiKeyVisibility() {
|
|
8927
|
+
const apiKeyInput = document.getElementById('chatApiKey');
|
|
8928
|
+
const toggleBtn = document.getElementById('chatApiKeyToggle');
|
|
8929
|
+
if (!apiKeyInput || !toggleBtn) return;
|
|
8930
|
+
|
|
8931
|
+
if (apiKeyInput.dataset.isMasked === 'true' && apiKeyInput.dataset.actualKey) {
|
|
8932
|
+
// Show actual key
|
|
8933
|
+
apiKeyInput.value = apiKeyInput.dataset.actualKey;
|
|
8934
|
+
apiKeyInput.type = 'text';
|
|
8935
|
+
apiKeyInput.dataset.isMasked = 'false';
|
|
8936
|
+
toggleBtn.textContent = '🙈';
|
|
8937
|
+
toggleBtn.title = 'Hide API key';
|
|
8938
|
+
} else if (apiKeyInput.dataset.actualKey) {
|
|
8939
|
+
// Re-mask the key
|
|
8940
|
+
const key = apiKeyInput.dataset.actualKey;
|
|
8941
|
+
const masked = key.slice(0, 4) + '•'.repeat(Math.max(0, key.length - 8)) + key.slice(-4);
|
|
8942
|
+
apiKeyInput.value = masked;
|
|
8943
|
+
apiKeyInput.type = 'password';
|
|
8944
|
+
apiKeyInput.dataset.isMasked = 'true';
|
|
8945
|
+
toggleBtn.textContent = '👁️';
|
|
8946
|
+
toggleBtn.title = 'Show API key';
|
|
8947
|
+
} else {
|
|
8948
|
+
// No stored key, just toggle between text/password for new input
|
|
8949
|
+
apiKeyInput.type = apiKeyInput.type === 'password' ? 'text' : 'password';
|
|
8950
|
+
toggleBtn.textContent = apiKeyInput.type === 'password' ? '👁️' : '🙈';
|
|
8951
|
+
toggleBtn.title = apiKeyInput.type === 'password' ? 'Show API key' : 'Hide API key';
|
|
8952
|
+
}
|
|
8953
|
+
}
|
|
8954
|
+
|
|
8955
|
+
async function saveChatApiKey() {
|
|
8956
|
+
if (!window.vai?.llmKey) return;
|
|
8957
|
+
const apiKeyInput = document.getElementById('chatApiKey');
|
|
8958
|
+
const toggleBtn = document.getElementById('chatApiKeyToggle');
|
|
8959
|
+
if (!apiKeyInput) return;
|
|
8960
|
+
|
|
8961
|
+
let key = apiKeyInput.value.trim();
|
|
8962
|
+
|
|
8963
|
+
// If showing masked value and it hasn't been modified, no need to save
|
|
8964
|
+
if (apiKeyInput.dataset.isMasked === 'true' && apiKeyInput.dataset.actualKey) {
|
|
8965
|
+
const currentMasked = apiKeyInput.dataset.actualKey.slice(0, 4) + '•'.repeat(Math.max(0, apiKeyInput.dataset.actualKey.length - 8)) + apiKeyInput.dataset.actualKey.slice(-4);
|
|
8966
|
+
if (key === currentMasked) {
|
|
8967
|
+
flashSaved();
|
|
8968
|
+
return;
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
|
|
8972
|
+
if (!key) {
|
|
8973
|
+
// Delete the key if empty
|
|
8974
|
+
try {
|
|
8975
|
+
await window.vai.llmKey.delete();
|
|
8976
|
+
delete apiKeyInput.dataset.actualKey;
|
|
8977
|
+
delete apiKeyInput.dataset.isMasked;
|
|
8978
|
+
flashSaved();
|
|
8979
|
+
} catch (err) {
|
|
8980
|
+
console.error('Failed to delete LLM API key:', err);
|
|
8981
|
+
alert('Failed to delete API key: ' + err.message);
|
|
8982
|
+
}
|
|
8983
|
+
return;
|
|
8984
|
+
}
|
|
8985
|
+
|
|
8986
|
+
try {
|
|
8987
|
+
await window.vai.llmKey.set(key);
|
|
8988
|
+
// Mask the newly saved key
|
|
8989
|
+
const masked = key.slice(0, 4) + '•'.repeat(Math.max(0, key.length - 8)) + key.slice(-4);
|
|
8990
|
+
apiKeyInput.value = masked;
|
|
8991
|
+
apiKeyInput.dataset.actualKey = key;
|
|
8992
|
+
apiKeyInput.dataset.isMasked = 'true';
|
|
8993
|
+
apiKeyInput.type = 'password';
|
|
8994
|
+
toggleBtn.textContent = '👁️';
|
|
8995
|
+
toggleBtn.title = 'Show API key';
|
|
8996
|
+
flashSaved();
|
|
8997
|
+
} catch (err) {
|
|
8998
|
+
console.error('Failed to save LLM API key:', err);
|
|
8999
|
+
alert('Failed to save API key: ' + err.message);
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
|
|
7795
9003
|
function updateChatStatus() {
|
|
7796
9004
|
const provider = document.getElementById('chatProvider');
|
|
7797
9005
|
const db = document.getElementById('chatDb').value;
|
|
7798
9006
|
const collection = document.getElementById('chatCollection').value;
|
|
9007
|
+
const mode = document.getElementById('chatMode')?.value || 'pipeline';
|
|
7799
9008
|
const providerLabel = provider.value
|
|
7800
9009
|
? provider.options[provider.selectedIndex].text
|
|
7801
9010
|
: 'No provider';
|
|
7802
|
-
|
|
9011
|
+
const modeLabel = mode === 'agent' ? 'Agent' : 'Pipeline';
|
|
9012
|
+
document.getElementById('chatStatusProvider').textContent = `${providerLabel} (${modeLabel})`;
|
|
7803
9013
|
document.getElementById('chatStatusDb').textContent =
|
|
7804
|
-
(db && collection) ? `${db}.${collection}` : 'No database';
|
|
9014
|
+
(db && collection) ? `${db}.${collection}` : (mode === 'agent' ? 'Agent discovers' : 'No database');
|
|
7805
9015
|
}
|
|
7806
9016
|
|
|
7807
9017
|
async function chatProviderChanged() {
|
|
7808
9018
|
updateChatStatus();
|
|
7809
9019
|
const provider = document.getElementById('chatProvider').value;
|
|
7810
9020
|
const modelSelect = document.getElementById('chatModel');
|
|
7811
|
-
|
|
9021
|
+
const apiKeyRow = document.getElementById('chatApiKeyRow');
|
|
9022
|
+
|
|
9023
|
+
// Show/hide API key row based on provider (not needed for Ollama)
|
|
9024
|
+
if (apiKeyRow) {
|
|
9025
|
+
apiKeyRow.style.display = (provider === 'anthropic' || provider === 'openai') ? 'flex' : 'none';
|
|
9026
|
+
if (provider === 'anthropic' || provider === 'openai') {
|
|
9027
|
+
await loadChatApiKey();
|
|
9028
|
+
}
|
|
9029
|
+
}
|
|
9030
|
+
|
|
7812
9031
|
if (!provider) {
|
|
7813
9032
|
modelSelect.innerHTML = '<option value="">Select provider first</option>';
|
|
7814
9033
|
return;
|
|
@@ -7886,6 +9105,262 @@ function chatInputKeydown(e) {
|
|
|
7886
9105
|
}
|
|
7887
9106
|
}
|
|
7888
9107
|
|
|
9108
|
+
/**
|
|
9109
|
+
* Lightweight markdown-to-HTML renderer for assistant chat messages.
|
|
9110
|
+
* Handles: fenced code blocks, inline code, headings, bold, italic,
|
|
9111
|
+
* links, images, unordered/ordered lists, blockquotes, tables, and <hr>.
|
|
9112
|
+
* No external dependencies.
|
|
9113
|
+
*/
|
|
9114
|
+
function renderMarkdown(md) {
|
|
9115
|
+
// Escape HTML to prevent XSS, then selectively render markdown
|
|
9116
|
+
function esc(s) {
|
|
9117
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9118
|
+
}
|
|
9119
|
+
|
|
9120
|
+
// Extract fenced code blocks first (preserve content as-is)
|
|
9121
|
+
const codeBlocks = [];
|
|
9122
|
+
let text = md.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
|
|
9123
|
+
const idx = codeBlocks.length;
|
|
9124
|
+
codeBlocks.push(`<pre><code${lang ? ' data-lang="' + esc(lang) + '"' : ''}>${esc(code.replace(/\n$/, ''))}</code></pre>`);
|
|
9125
|
+
return '\x00CB' + idx + '\x00';
|
|
9126
|
+
});
|
|
9127
|
+
|
|
9128
|
+
// Escape remaining HTML
|
|
9129
|
+
text = esc(text);
|
|
9130
|
+
|
|
9131
|
+
// Restore code block placeholders
|
|
9132
|
+
text = text.replace(/\x00CB(\d+)\x00/g, (_, i) => codeBlocks[i]);
|
|
9133
|
+
|
|
9134
|
+
// Inline code (must come after escape but before other inline formatting)
|
|
9135
|
+
text = text.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
9136
|
+
|
|
9137
|
+
// Headings (must be at line start)
|
|
9138
|
+
text = text.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
|
9139
|
+
text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
9140
|
+
text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
9141
|
+
text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
9142
|
+
|
|
9143
|
+
// Horizontal rule
|
|
9144
|
+
text = text.replace(/^---+$/gm, '<hr>');
|
|
9145
|
+
|
|
9146
|
+
// Bold and italic
|
|
9147
|
+
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
9148
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
9149
|
+
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
9150
|
+
|
|
9151
|
+
// Links and images
|
|
9152
|
+
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" style="max-width:100%;border-radius:4px;">');
|
|
9153
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
9154
|
+
|
|
9155
|
+
// Tables (pipe-delimited)
|
|
9156
|
+
text = text.replace(/((?:^\|.+\|$\n?)+)/gm, (block) => {
|
|
9157
|
+
const rows = block.trim().split('\n').filter(r => r.trim());
|
|
9158
|
+
if (rows.length < 2) return block;
|
|
9159
|
+
// Check if second row is separator
|
|
9160
|
+
const sep = rows[1];
|
|
9161
|
+
if (!/^\|[\s:-]+\|$/.test(sep.trim().replace(/\|/g, '|'))) return block;
|
|
9162
|
+
const headerCells = rows[0].split('|').filter(c => c.trim() !== '').map(c => c.trim());
|
|
9163
|
+
let html = '<table><thead><tr>' + headerCells.map(c => '<th>' + c + '</th>').join('') + '</tr></thead><tbody>';
|
|
9164
|
+
for (let i = 2; i < rows.length; i++) {
|
|
9165
|
+
const cells = rows[i].split('|').filter(c => c.trim() !== '').map(c => c.trim());
|
|
9166
|
+
html += '<tr>' + cells.map(c => '<td>' + c + '</td>').join('') + '</tr>';
|
|
9167
|
+
}
|
|
9168
|
+
html += '</tbody></table>';
|
|
9169
|
+
return html;
|
|
9170
|
+
});
|
|
9171
|
+
|
|
9172
|
+
// Blockquotes
|
|
9173
|
+
text = text.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
|
|
9174
|
+
// Merge adjacent blockquotes
|
|
9175
|
+
text = text.replace(/<\/blockquote>\n<blockquote>/g, '\n');
|
|
9176
|
+
|
|
9177
|
+
// Unordered lists (lines starting with - or *)
|
|
9178
|
+
text = text.replace(/((?:^[\s]*[-*] .+$\n?)+)/gm, (block) => {
|
|
9179
|
+
const items = block.trim().split('\n').map(line => {
|
|
9180
|
+
const content = line.replace(/^[\s]*[-*] /, '');
|
|
9181
|
+
return '<li>' + content + '</li>';
|
|
9182
|
+
});
|
|
9183
|
+
return '<ul>' + items.join('') + '</ul>';
|
|
9184
|
+
});
|
|
9185
|
+
|
|
9186
|
+
// Ordered lists (lines starting with number.)
|
|
9187
|
+
text = text.replace(/((?:^[\s]*\d+\. .+$\n?)+)/gm, (block) => {
|
|
9188
|
+
const items = block.trim().split('\n').map(line => {
|
|
9189
|
+
const content = line.replace(/^[\s]*\d+\. /, '');
|
|
9190
|
+
return '<li>' + content + '</li>';
|
|
9191
|
+
});
|
|
9192
|
+
return '<ol>' + items.join('') + '</ol>';
|
|
9193
|
+
});
|
|
9194
|
+
|
|
9195
|
+
// Paragraphs: wrap remaining text lines that aren't already wrapped in block elements
|
|
9196
|
+
const blockTags = ['<h1', '<h2', '<h3', '<h4', '<hr', '<pre', '<ul', '<ol', '<table', '<blockquote'];
|
|
9197
|
+
const lines = text.split('\n');
|
|
9198
|
+
const result = [];
|
|
9199
|
+
let paraLines = [];
|
|
9200
|
+
|
|
9201
|
+
function flushPara() {
|
|
9202
|
+
if (paraLines.length > 0) {
|
|
9203
|
+
const content = paraLines.join('\n').trim();
|
|
9204
|
+
if (content) result.push('<p>' + content + '</p>');
|
|
9205
|
+
paraLines = [];
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
|
|
9209
|
+
for (const line of lines) {
|
|
9210
|
+
const trimmed = line.trim();
|
|
9211
|
+
if (trimmed === '') {
|
|
9212
|
+
flushPara();
|
|
9213
|
+
} else if (blockTags.some(tag => trimmed.startsWith(tag))) {
|
|
9214
|
+
flushPara();
|
|
9215
|
+
result.push(line);
|
|
9216
|
+
} else {
|
|
9217
|
+
paraLines.push(line);
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
9220
|
+
flushPara();
|
|
9221
|
+
|
|
9222
|
+
return result.join('\n');
|
|
9223
|
+
}
|
|
9224
|
+
|
|
9225
|
+
/**
|
|
9226
|
+
* Tool metadata: icon, label, and a function to summarize the call for the thinking panel.
|
|
9227
|
+
*/
|
|
9228
|
+
const TOOL_META = {
|
|
9229
|
+
vai_query: { icon: '\uD83D\uDD0D', label: 'RAG Query', verb: 'Searching', descFn: a => a.query ? `"${a.query}"` : '' },
|
|
9230
|
+
vai_search: { icon: '\uD83D\uDD0E', label: 'Vector Search', verb: 'Searching vectors', descFn: a => a.query ? `"${a.query}"` : '' },
|
|
9231
|
+
vai_rerank: { icon: '\u2195\uFE0F', label: 'Rerank', verb: 'Reranking', descFn: a => a.query ? `${a.documents?.length || '?'} docs for "${a.query}"` : '' },
|
|
9232
|
+
vai_embed: { icon: '\uD83E\uDDE0', label: 'Embed', verb: 'Embedding', descFn: a => a.text ? `"${a.text.slice(0, 60)}${a.text.length > 60 ? '...' : ''}"` : '' },
|
|
9233
|
+
vai_similarity: { icon: '\uD83C\uDFAF', label: 'Similarity', verb: 'Comparing', descFn: a => a.text1 ? `two texts` : '' },
|
|
9234
|
+
vai_collections: { icon: '\uD83D\uDDC4\uFE0F', label: 'Collections', verb: 'Discovering', descFn: a => a.db ? `in ${a.db}` : 'available databases' },
|
|
9235
|
+
vai_models: { icon: '\uD83E\uDD16', label: 'Models', verb: 'Listing', descFn: () => 'available models' },
|
|
9236
|
+
vai_topics: { icon: '\uD83D\uDCDA', label: 'Topics', verb: 'Browsing', descFn: () => 'educational topics' },
|
|
9237
|
+
vai_explain: { icon: '\uD83D\uDCA1', label: 'Explain', verb: 'Explaining', descFn: a => a.topic || '' },
|
|
9238
|
+
vai_estimate: { icon: '\uD83D\uDCB0', label: 'Cost Estimate', verb: 'Estimating', descFn: a => a.docs ? `${a.docs} docs` : '' },
|
|
9239
|
+
vai_ingest: { icon: '\uD83D\uDCE5', label: 'Ingest', verb: 'Ingesting', descFn: a => a.source || 'document' },
|
|
9240
|
+
};
|
|
9241
|
+
const DEFAULT_TOOL_META = { icon: '\u2699\uFE0F', label: '', verb: 'Running', descFn: () => '' };
|
|
9242
|
+
|
|
9243
|
+
/**
|
|
9244
|
+
* Create the thinking panel <details> element.
|
|
9245
|
+
* Returns { panel, timeline, addStep, finalize }.
|
|
9246
|
+
*/
|
|
9247
|
+
function createThinkingPanel() {
|
|
9248
|
+
const panel = document.createElement('details');
|
|
9249
|
+
panel.className = 'chat-thinking';
|
|
9250
|
+
panel.open = true;
|
|
9251
|
+
|
|
9252
|
+
const summary = document.createElement('summary');
|
|
9253
|
+
summary.innerHTML =
|
|
9254
|
+
'<span class="thinking-icon">\uD83E\uDDE0</span>' +
|
|
9255
|
+
'<span class="thinking-label">Thinking</span>' +
|
|
9256
|
+
'<span class="thinking-count">0</span>' +
|
|
9257
|
+
'<span class="thinking-elapsed"></span>' +
|
|
9258
|
+
'<span class="thinking-chevron">\u25B6</span>';
|
|
9259
|
+
panel.appendChild(summary);
|
|
9260
|
+
|
|
9261
|
+
const timeline = document.createElement('div');
|
|
9262
|
+
timeline.className = 'thinking-timeline';
|
|
9263
|
+
panel.appendChild(timeline);
|
|
9264
|
+
|
|
9265
|
+
let stepCount = 0;
|
|
9266
|
+
let activeStep = null;
|
|
9267
|
+
const startTime = Date.now();
|
|
9268
|
+
let elapsedTimer = null;
|
|
9269
|
+
|
|
9270
|
+
// Update the elapsed time in the summary
|
|
9271
|
+
function tickElapsed() {
|
|
9272
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
9273
|
+
summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
|
|
9274
|
+
}
|
|
9275
|
+
elapsedTimer = setInterval(tickElapsed, 200);
|
|
9276
|
+
tickElapsed();
|
|
9277
|
+
|
|
9278
|
+
function addStep(data) {
|
|
9279
|
+
const meta = TOOL_META[data.name] || DEFAULT_TOOL_META;
|
|
9280
|
+
const desc = meta.descFn(data.args || {});
|
|
9281
|
+
|
|
9282
|
+
// Mark previous active step as done
|
|
9283
|
+
if (activeStep) {
|
|
9284
|
+
activeStep.classList.remove('active');
|
|
9285
|
+
activeStep.classList.add('done');
|
|
9286
|
+
}
|
|
9287
|
+
|
|
9288
|
+
stepCount++;
|
|
9289
|
+
summary.querySelector('.thinking-count').textContent = stepCount;
|
|
9290
|
+
|
|
9291
|
+
const step = document.createElement('div');
|
|
9292
|
+
step.className = 'thinking-step active';
|
|
9293
|
+
if (data.error) step.className = 'thinking-step error';
|
|
9294
|
+
|
|
9295
|
+
const iconDiv = document.createElement('div');
|
|
9296
|
+
iconDiv.className = 'thinking-step-icon';
|
|
9297
|
+
iconDiv.textContent = meta.icon;
|
|
9298
|
+
step.appendChild(iconDiv);
|
|
9299
|
+
|
|
9300
|
+
const body = document.createElement('div');
|
|
9301
|
+
body.className = 'thinking-step-body';
|
|
9302
|
+
|
|
9303
|
+
const header = document.createElement('div');
|
|
9304
|
+
header.className = 'thinking-step-header';
|
|
9305
|
+
const nameSpan = document.createElement('span');
|
|
9306
|
+
nameSpan.className = 'thinking-step-name';
|
|
9307
|
+
nameSpan.textContent = meta.verb + (desc ? ' ' : '') + (meta.label || data.name);
|
|
9308
|
+
header.appendChild(nameSpan);
|
|
9309
|
+
|
|
9310
|
+
if (data.timeMs !== undefined) {
|
|
9311
|
+
const timeSpan = document.createElement('span');
|
|
9312
|
+
timeSpan.className = 'thinking-step-time';
|
|
9313
|
+
timeSpan.textContent = data.timeMs + 'ms';
|
|
9314
|
+
header.appendChild(timeSpan);
|
|
9315
|
+
}
|
|
9316
|
+
body.appendChild(header);
|
|
9317
|
+
|
|
9318
|
+
if (desc) {
|
|
9319
|
+
const descDiv = document.createElement('div');
|
|
9320
|
+
descDiv.className = 'thinking-step-desc';
|
|
9321
|
+
descDiv.textContent = desc;
|
|
9322
|
+
body.appendChild(descDiv);
|
|
9323
|
+
}
|
|
9324
|
+
|
|
9325
|
+
if (data.error) {
|
|
9326
|
+
const errDiv = document.createElement('div');
|
|
9327
|
+
errDiv.className = 'thinking-step-detail';
|
|
9328
|
+
errDiv.style.color = '#e74c3c';
|
|
9329
|
+
errDiv.textContent = data.error;
|
|
9330
|
+
body.appendChild(errDiv);
|
|
9331
|
+
} else if (data.resultSummary) {
|
|
9332
|
+
const detailDiv = document.createElement('div');
|
|
9333
|
+
detailDiv.className = 'thinking-step-detail';
|
|
9334
|
+
detailDiv.innerHTML = data.resultSummary;
|
|
9335
|
+
body.appendChild(detailDiv);
|
|
9336
|
+
}
|
|
9337
|
+
|
|
9338
|
+
step.appendChild(body);
|
|
9339
|
+
timeline.appendChild(step);
|
|
9340
|
+
|
|
9341
|
+
if (!data.error) activeStep = step;
|
|
9342
|
+
|
|
9343
|
+
return step;
|
|
9344
|
+
}
|
|
9345
|
+
|
|
9346
|
+
function finalize() {
|
|
9347
|
+
clearInterval(elapsedTimer);
|
|
9348
|
+
// Final elapsed
|
|
9349
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
9350
|
+
summary.querySelector('.thinking-elapsed').textContent = elapsed + 's';
|
|
9351
|
+
summary.querySelector('.thinking-label').textContent = 'Thought for ' + elapsed + 's';
|
|
9352
|
+
summary.querySelector('.thinking-icon').textContent = '\u2728';
|
|
9353
|
+
// Mark last step done and collapse
|
|
9354
|
+
if (activeStep) {
|
|
9355
|
+
activeStep.classList.remove('active');
|
|
9356
|
+
activeStep.classList.add('done');
|
|
9357
|
+
}
|
|
9358
|
+
panel.open = false;
|
|
9359
|
+
}
|
|
9360
|
+
|
|
9361
|
+
return { panel, addStep, finalize };
|
|
9362
|
+
}
|
|
9363
|
+
|
|
7889
9364
|
function addChatMessage(role, content, sources) {
|
|
7890
9365
|
const container = document.getElementById('chatMessages');
|
|
7891
9366
|
const div = document.createElement('div');
|
|
@@ -7895,6 +9370,26 @@ function addChatMessage(role, content, sources) {
|
|
|
7895
9370
|
contentSpan.textContent = content;
|
|
7896
9371
|
div.appendChild(contentSpan);
|
|
7897
9372
|
|
|
9373
|
+
// Add copy button for assistant messages
|
|
9374
|
+
if (role === 'assistant') {
|
|
9375
|
+
const copyBtn = document.createElement('button');
|
|
9376
|
+
copyBtn.className = 'chat-copy-btn';
|
|
9377
|
+
copyBtn.title = 'Copy to clipboard';
|
|
9378
|
+
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>';
|
|
9379
|
+
copyBtn.addEventListener('click', () => {
|
|
9380
|
+
const text = contentSpan.textContent || contentSpan.innerText;
|
|
9381
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
9382
|
+
copyBtn.classList.add('copied');
|
|
9383
|
+
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>';
|
|
9384
|
+
setTimeout(() => {
|
|
9385
|
+
copyBtn.classList.remove('copied');
|
|
9386
|
+
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>';
|
|
9387
|
+
}, 2000);
|
|
9388
|
+
});
|
|
9389
|
+
});
|
|
9390
|
+
div.appendChild(copyBtn);
|
|
9391
|
+
}
|
|
9392
|
+
|
|
7898
9393
|
if (sources && sources.length > 0) {
|
|
7899
9394
|
const details = document.createElement('details');
|
|
7900
9395
|
details.className = 'chat-sources';
|
|
@@ -7929,12 +9424,14 @@ async function sendChatMessage() {
|
|
|
7929
9424
|
const maxDocs = parseInt(document.getElementById('chatMaxDocs').value) || 5;
|
|
7930
9425
|
const rerank = document.getElementById('chatRerank').classList.contains('active');
|
|
7931
9426
|
const systemPrompt = document.getElementById('chatSystemPrompt').value.trim() || undefined;
|
|
9427
|
+
const mode = document.getElementById('chatMode')?.value || 'pipeline';
|
|
9428
|
+
const isAgent = mode === 'agent';
|
|
7932
9429
|
|
|
7933
9430
|
if (!provider) {
|
|
7934
9431
|
addChatMessage('system-msg', 'Please select an LLM provider in <a href="#" onclick="openChatSettings();return false;">Chat Settings</a>.');
|
|
7935
9432
|
return;
|
|
7936
9433
|
}
|
|
7937
|
-
if (!db || !collection) {
|
|
9434
|
+
if (!isAgent && (!db || !collection)) {
|
|
7938
9435
|
addChatMessage('system-msg', 'Please configure a database and collection in <a href="#" onclick="openChatSettings();return false;">Chat Settings</a>.');
|
|
7939
9436
|
return;
|
|
7940
9437
|
}
|
|
@@ -7947,7 +9444,7 @@ async function sendChatMessage() {
|
|
|
7947
9444
|
// Show typing indicator
|
|
7948
9445
|
const typing = document.createElement('div');
|
|
7949
9446
|
typing.className = 'chat-typing';
|
|
7950
|
-
typing.textContent = 'Thinking';
|
|
9447
|
+
typing.textContent = isAgent ? 'Agent working' : 'Thinking';
|
|
7951
9448
|
document.getElementById('chatMessages').appendChild(typing);
|
|
7952
9449
|
|
|
7953
9450
|
// Disable input
|
|
@@ -7959,7 +9456,7 @@ async function sendChatMessage() {
|
|
|
7959
9456
|
const res = await fetch('/api/chat/message', {
|
|
7960
9457
|
method: 'POST',
|
|
7961
9458
|
headers: { 'Content-Type': 'application/json' },
|
|
7962
|
-
body: JSON.stringify({ query, db, collection, provider, model, maxDocs, rerank, systemPrompt }),
|
|
9459
|
+
body: JSON.stringify({ query, db, collection, provider, model, maxDocs, rerank, systemPrompt, mode }),
|
|
7963
9460
|
});
|
|
7964
9461
|
|
|
7965
9462
|
if (!res.ok) {
|
|
@@ -7976,6 +9473,7 @@ async function sendChatMessage() {
|
|
|
7976
9473
|
let assistantDiv = null;
|
|
7977
9474
|
let fullText = '';
|
|
7978
9475
|
let sources = [];
|
|
9476
|
+
let thinkingPanel = null;
|
|
7979
9477
|
|
|
7980
9478
|
while (true) {
|
|
7981
9479
|
const { done, value } = await reader.read();
|
|
@@ -7998,6 +9496,17 @@ async function sendChatMessage() {
|
|
|
7998
9496
|
typing.textContent = `Retrieved ${data.docs?.length || 0} docs (${data.timeMs}ms)`;
|
|
7999
9497
|
}
|
|
8000
9498
|
|
|
9499
|
+
if (currentEvent === 'tool_call') {
|
|
9500
|
+
// Create thinking panel on first tool call
|
|
9501
|
+
if (!thinkingPanel) {
|
|
9502
|
+
typing.remove();
|
|
9503
|
+
thinkingPanel = createThinkingPanel();
|
|
9504
|
+
document.getElementById('chatMessages').appendChild(thinkingPanel.panel);
|
|
9505
|
+
}
|
|
9506
|
+
thinkingPanel.addStep(data);
|
|
9507
|
+
document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
|
|
9508
|
+
}
|
|
9509
|
+
|
|
8001
9510
|
if (currentEvent === 'chunk') {
|
|
8002
9511
|
if (!assistantDiv) {
|
|
8003
9512
|
typing.remove();
|
|
@@ -8009,6 +9518,14 @@ async function sendChatMessage() {
|
|
|
8009
9518
|
}
|
|
8010
9519
|
|
|
8011
9520
|
if (currentEvent === 'done') {
|
|
9521
|
+
// Finalize the thinking panel (collapse, show elapsed)
|
|
9522
|
+
if (thinkingPanel) thinkingPanel.finalize();
|
|
9523
|
+
// Render accumulated text as markdown for assistant messages
|
|
9524
|
+
if (assistantDiv && fullText) {
|
|
9525
|
+
const contentEl = assistantDiv.querySelector('.chat-message-content');
|
|
9526
|
+
contentEl.innerHTML = renderMarkdown(fullText);
|
|
9527
|
+
contentEl.classList.add('rendered');
|
|
9528
|
+
}
|
|
8012
9529
|
sources = data.sources || [];
|
|
8013
9530
|
if (sources.length > 0 && assistantDiv) {
|
|
8014
9531
|
const details = document.createElement('details');
|
|
@@ -8042,6 +9559,7 @@ async function sendChatMessage() {
|
|
|
8042
9559
|
|
|
8043
9560
|
} catch (err) {
|
|
8044
9561
|
if (typing.parentNode) typing.remove();
|
|
9562
|
+
if (thinkingPanel) thinkingPanel.finalize();
|
|
8045
9563
|
addChatMessage('system-msg', `Error: ${err.message}`);
|
|
8046
9564
|
} finally {
|
|
8047
9565
|
sendBtn.disabled = false;
|
|
@@ -8074,6 +9592,9 @@ window.switchSettingsSection = switchSettingsSection;
|
|
|
8074
9592
|
window.updateChatStatus = updateChatStatus;
|
|
8075
9593
|
window.chatInputKeydown = chatInputKeydown;
|
|
8076
9594
|
window.chatProviderChanged = chatProviderChanged;
|
|
9595
|
+
window.loadChatApiKey = loadChatApiKey;
|
|
9596
|
+
window.toggleChatApiKeyVisibility = toggleChatApiKeyVisibility;
|
|
9597
|
+
window.saveChatApiKey = saveChatApiKey;
|
|
8077
9598
|
|
|
8078
9599
|
// ── Start ──
|
|
8079
9600
|
init();
|
|
@@ -8253,6 +9774,1227 @@ init();
|
|
|
8253
9774
|
</div>
|
|
8254
9775
|
</div>
|
|
8255
9776
|
|
|
9777
|
+
<!-- ========== WORKFLOW VISUALIZER JS ========== -->
|
|
9778
|
+
<script>
|
|
9779
|
+
// ── Workflow Visualizer ──
|
|
9780
|
+
|
|
9781
|
+
// escapeHtml is defined in the main IIFE scope and not accessible here — redeclare it
|
|
9782
|
+
function escapeHtml(str) {
|
|
9783
|
+
if (str == null) return '';
|
|
9784
|
+
const s = typeof str === 'string' ? str : String(str);
|
|
9785
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9786
|
+
}
|
|
9787
|
+
|
|
9788
|
+
const WF_NODE_META = {
|
|
9789
|
+
query: { icon: '\u{1F50D}', label: 'RAG Query', color: '#40E0FF', category: 'retrieval' },
|
|
9790
|
+
search: { icon: '\u{1F50E}', label: 'Vector Search', color: '#40E0FF', category: 'retrieval' },
|
|
9791
|
+
rerank: { icon: '\u{1F3C6}', label: 'Rerank', color: '#40E0FF', category: 'retrieval' },
|
|
9792
|
+
ingest: { icon: '\u{1F4E5}', label: 'Ingest', color: '#40E0FF', category: 'retrieval' },
|
|
9793
|
+
embed: { icon: '\u{1F4D0}', label: 'Embed', color: '#B388FF', category: 'embedding' },
|
|
9794
|
+
similarity: { icon: '\u{1F517}', label: 'Similarity', color: '#B388FF', category: 'embedding' },
|
|
9795
|
+
collections: { icon: '\u{1F5C4}', label: 'Collections', color: '#00D4AA', category: 'management' },
|
|
9796
|
+
models: { icon: '\u{1F4CB}', label: 'Models', color: '#00D4AA', category: 'management' },
|
|
9797
|
+
estimate: { icon: '\u{1F4B0}', label: 'Cost Estimate', color: '#FFB74D', category: 'utility' },
|
|
9798
|
+
explain: { icon: '\u{1F4D6}', label: 'Explain', color: '#FFB74D', category: 'utility' },
|
|
9799
|
+
topics: { icon: '\u{1F5C2}', label: 'Topics', color: '#FFB74D', category: 'utility' },
|
|
9800
|
+
merge: { icon: '\u{1F500}', label: 'Merge', color: '#90A4AE', category: 'control' },
|
|
9801
|
+
filter: { icon: '\u{1F9F9}', label: 'Filter', color: '#90A4AE', category: 'control' },
|
|
9802
|
+
transform: { icon: '\u{1F504}', label: 'Transform', color: '#90A4AE', category: 'control' },
|
|
9803
|
+
generate: { icon: '\u2728', label: 'Generate', color: '#69F0AE', category: 'generation' },
|
|
9804
|
+
};
|
|
9805
|
+
|
|
9806
|
+
const WF_NODE_W = 180;
|
|
9807
|
+
const WF_NODE_H = 64;
|
|
9808
|
+
const WF_LAYER_GAP = 260;
|
|
9809
|
+
const WF_NODE_GAP = 100;
|
|
9810
|
+
const WF_PAD = 80;
|
|
9811
|
+
const WF_PORT_R = 5; // Port circle radius
|
|
9812
|
+
|
|
9813
|
+
let wfState = {
|
|
9814
|
+
workflows: [],
|
|
9815
|
+
activeWorkflow: null,
|
|
9816
|
+
selectedNodeId: null,
|
|
9817
|
+
executionState: {},
|
|
9818
|
+
executionResults: {},
|
|
9819
|
+
nodePositions: {},
|
|
9820
|
+
layers: [],
|
|
9821
|
+
graph: {},
|
|
9822
|
+
zoom: 1,
|
|
9823
|
+
panX: 0,
|
|
9824
|
+
panY: 0,
|
|
9825
|
+
isPanning: false,
|
|
9826
|
+
panStart: { x: 0, y: 0 },
|
|
9827
|
+
executing: false,
|
|
9828
|
+
};
|
|
9829
|
+
|
|
9830
|
+
// ── Library ──
|
|
9831
|
+
async function wfLoadLibrary() {
|
|
9832
|
+
try {
|
|
9833
|
+
const res = await fetch('/api/workflows');
|
|
9834
|
+
const data = await res.json();
|
|
9835
|
+
wfState.workflows = data.workflows || [];
|
|
9836
|
+
wfRenderLibrary();
|
|
9837
|
+
} catch (err) {
|
|
9838
|
+
const list = document.getElementById('wfLibraryList');
|
|
9839
|
+
if (list) list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">Failed to load workflows</div>';
|
|
9840
|
+
}
|
|
9841
|
+
}
|
|
9842
|
+
|
|
9843
|
+
function wfRenderLibrary() {
|
|
9844
|
+
const list = document.getElementById('wfLibraryList');
|
|
9845
|
+
if (!list) return;
|
|
9846
|
+
if (wfState.workflows.length === 0) {
|
|
9847
|
+
list.innerHTML = '<div style="padding:16px;color:var(--text-muted);font-size:12px;">No workflows found</div>';
|
|
9848
|
+
return;
|
|
9849
|
+
}
|
|
9850
|
+
list.innerHTML = wfState.workflows.map(w => {
|
|
9851
|
+
// w.name is the file stem (e.g., "multi-collection-search")
|
|
9852
|
+
// w.description comes from the workflow JSON's description field
|
|
9853
|
+
const displayName = w.name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
9854
|
+
return `<div class="wf-library-item" data-wf-name="${w.name}" onclick="wfSelectWorkflow('${w.name}')">
|
|
9855
|
+
<div class="wf-library-item-name">${displayName}</div>
|
|
9856
|
+
<div class="wf-library-item-desc">${w.description || ''}</div>
|
|
9857
|
+
</div>`;
|
|
9858
|
+
}).join('');
|
|
9859
|
+
}
|
|
9860
|
+
|
|
9861
|
+
async function wfSelectWorkflow(name) {
|
|
9862
|
+
// Highlight in library
|
|
9863
|
+
document.querySelectorAll('.wf-library-item').forEach(el => {
|
|
9864
|
+
el.classList.toggle('active', el.dataset.wfName === name);
|
|
9865
|
+
});
|
|
9866
|
+
|
|
9867
|
+
try {
|
|
9868
|
+
const res = await fetch('/api/workflows/' + encodeURIComponent(name));
|
|
9869
|
+
const data = await res.json();
|
|
9870
|
+
wfState.activeWorkflow = data.definition;
|
|
9871
|
+
wfState.selectedNodeId = null;
|
|
9872
|
+
wfState.executionState = {};
|
|
9873
|
+
wfState.executionResults = {};
|
|
9874
|
+
wfSetToolbarEnabled(true);
|
|
9875
|
+
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
9876
|
+
await wfRenderWorkflow(data.definition);
|
|
9877
|
+
wfOpenInspector();
|
|
9878
|
+
wfUpdateInspector();
|
|
9879
|
+
} catch (err) {
|
|
9880
|
+
console.error('Failed to load workflow:', err);
|
|
9881
|
+
}
|
|
9882
|
+
}
|
|
9883
|
+
|
|
9884
|
+
// ── Load workflow from file ──
|
|
9885
|
+
function wfLoadFromFile() {
|
|
9886
|
+
document.getElementById('wfFileInput').click();
|
|
9887
|
+
}
|
|
9888
|
+
|
|
9889
|
+
function wfHandleFileLoad(event) {
|
|
9890
|
+
const file = event.target.files[0];
|
|
9891
|
+
if (!file) return;
|
|
9892
|
+
const reader = new FileReader();
|
|
9893
|
+
reader.onload = async (e) => {
|
|
9894
|
+
try {
|
|
9895
|
+
const definition = JSON.parse(e.target.result);
|
|
9896
|
+
if (!definition.steps || !Array.isArray(definition.steps)) {
|
|
9897
|
+
alert('Invalid workflow: missing "steps" array');
|
|
9898
|
+
return;
|
|
9899
|
+
}
|
|
9900
|
+
// Validate via server
|
|
9901
|
+
const valRes = await fetch('/api/workflows/validate', {
|
|
9902
|
+
method: 'POST',
|
|
9903
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9904
|
+
body: JSON.stringify({ definition }),
|
|
9905
|
+
});
|
|
9906
|
+
const valData = await valRes.json();
|
|
9907
|
+
if (!valData.valid) {
|
|
9908
|
+
alert('Workflow validation failed:\n' + (valData.errors || []).join('\n'));
|
|
9909
|
+
return;
|
|
9910
|
+
}
|
|
9911
|
+
// Deselect any library item
|
|
9912
|
+
document.querySelectorAll('.wf-library-item.active').forEach(el => el.classList.remove('active'));
|
|
9913
|
+
// Load the workflow
|
|
9914
|
+
wfState.activeWorkflow = definition;
|
|
9915
|
+
wfState.selectedNodeId = null;
|
|
9916
|
+
wfState.executionState = {};
|
|
9917
|
+
wfState.executionResults = {};
|
|
9918
|
+
wfSetToolbarEnabled(true);
|
|
9919
|
+
document.getElementById('wfCanvasEmpty').style.display = 'none';
|
|
9920
|
+
wfHideExecStatus();
|
|
9921
|
+
await wfRenderWorkflow(definition);
|
|
9922
|
+
wfOpenInspector();
|
|
9923
|
+
wfUpdateInspector();
|
|
9924
|
+
} catch (err) {
|
|
9925
|
+
alert('Failed to parse workflow file: ' + err.message);
|
|
9926
|
+
}
|
|
9927
|
+
};
|
|
9928
|
+
reader.readAsText(file);
|
|
9929
|
+
// Reset file input so the same file can be re-loaded
|
|
9930
|
+
event.target.value = '';
|
|
9931
|
+
}
|
|
9932
|
+
|
|
9933
|
+
// ── DAG Layout + SVG Rendering ──
|
|
9934
|
+
async function wfRenderWorkflow(definition) {
|
|
9935
|
+
const svg = document.getElementById('wf-canvas');
|
|
9936
|
+
// Clear previous nodes and edges (keep defs)
|
|
9937
|
+
svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
|
|
9938
|
+
|
|
9939
|
+
// Get execution plan from server
|
|
9940
|
+
let layers, graph;
|
|
9941
|
+
try {
|
|
9942
|
+
const res = await fetch('/api/workflows/plan', {
|
|
9943
|
+
method: 'POST',
|
|
9944
|
+
headers: { 'Content-Type': 'application/json' },
|
|
9945
|
+
body: JSON.stringify({ definition }),
|
|
9946
|
+
});
|
|
9947
|
+
const data = await res.json();
|
|
9948
|
+
layers = data.layers;
|
|
9949
|
+
graph = data.graph;
|
|
9950
|
+
} catch (err) {
|
|
9951
|
+
console.error('Failed to get execution plan:', err);
|
|
9952
|
+
return;
|
|
9953
|
+
}
|
|
9954
|
+
|
|
9955
|
+
wfState.layers = layers;
|
|
9956
|
+
wfState.graph = graph;
|
|
9957
|
+
|
|
9958
|
+
// Build step lookup
|
|
9959
|
+
const stepMap = {};
|
|
9960
|
+
definition.steps.forEach(s => { stepMap[s.id] = s; });
|
|
9961
|
+
|
|
9962
|
+
// Calculate positions
|
|
9963
|
+
const positions = {};
|
|
9964
|
+
const maxLayerSize = Math.max(...layers.map(l => l.length));
|
|
9965
|
+
const totalW = layers.length * WF_LAYER_GAP;
|
|
9966
|
+
const totalH = maxLayerSize * (WF_NODE_H + WF_NODE_GAP);
|
|
9967
|
+
|
|
9968
|
+
layers.forEach((layer, li) => {
|
|
9969
|
+
const x = WF_PAD + li * WF_LAYER_GAP;
|
|
9970
|
+
const layerH = layer.length * WF_NODE_H + (layer.length - 1) * WF_NODE_GAP;
|
|
9971
|
+
const startY = WF_PAD + (totalH - layerH) / 2;
|
|
9972
|
+
layer.forEach((stepId, ni) => {
|
|
9973
|
+
positions[stepId] = {
|
|
9974
|
+
x,
|
|
9975
|
+
y: startY + ni * (WF_NODE_H + WF_NODE_GAP),
|
|
9976
|
+
};
|
|
9977
|
+
});
|
|
9978
|
+
});
|
|
9979
|
+
wfState.nodePositions = positions;
|
|
9980
|
+
|
|
9981
|
+
// Build port-visibility maps: which nodes have input deps, which have dependents
|
|
9982
|
+
const nodeHasDeps = {}; // node has incoming edges (show input port)
|
|
9983
|
+
const nodeHasDependents = {}; // node has outgoing edges (show output port)
|
|
9984
|
+
for (const [stepId, deps] of Object.entries(graph)) {
|
|
9985
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
9986
|
+
deps.forEach(rawDepId => {
|
|
9987
|
+
const depId = rawDepId.replace(/^!/, '');
|
|
9988
|
+
if (positions[depId] && positions[stepId]) {
|
|
9989
|
+
nodeHasDeps[stepId] = true;
|
|
9990
|
+
nodeHasDependents[depId] = true;
|
|
9991
|
+
}
|
|
9992
|
+
});
|
|
9993
|
+
}
|
|
9994
|
+
|
|
9995
|
+
// Draw edges first (behind nodes)
|
|
9996
|
+
const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
9997
|
+
edgeGroup.classList.add('wf-edge-group');
|
|
9998
|
+
for (const [stepId, deps] of Object.entries(graph)) {
|
|
9999
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
10000
|
+
deps.forEach(rawDepId => {
|
|
10001
|
+
// Strip negation prefix (e.g., "!similarity_check" -> "similarity_check")
|
|
10002
|
+
const depId = rawDepId.replace(/^!/, '');
|
|
10003
|
+
if (positions[depId] && positions[stepId]) {
|
|
10004
|
+
const edge = wfDrawEdge(depId, stepId, positions);
|
|
10005
|
+
edgeGroup.appendChild(edge);
|
|
10006
|
+
}
|
|
10007
|
+
});
|
|
10008
|
+
}
|
|
10009
|
+
svg.appendChild(edgeGroup);
|
|
10010
|
+
|
|
10011
|
+
// Draw nodes
|
|
10012
|
+
for (const step of definition.steps) {
|
|
10013
|
+
const pos = positions[step.id];
|
|
10014
|
+
if (!pos) continue;
|
|
10015
|
+
const state = wfState.executionState[step.id] || 'idle';
|
|
10016
|
+
const hasDeps = !!nodeHasDeps[step.id];
|
|
10017
|
+
const hasDependents = !!nodeHasDependents[step.id];
|
|
10018
|
+
const nodeGroup = wfDrawNode(step, pos.x, pos.y, state, hasDeps, hasDependents);
|
|
10019
|
+
svg.appendChild(nodeGroup);
|
|
10020
|
+
}
|
|
10021
|
+
|
|
10022
|
+
// Set viewBox to fit
|
|
10023
|
+
wfFitToView();
|
|
10024
|
+
}
|
|
10025
|
+
|
|
10026
|
+
function wfDrawNode(step, x, y, state, hasDeps, hasDependents) {
|
|
10027
|
+
const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666', category: 'unknown' };
|
|
10028
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10029
|
+
g.classList.add('wf-node');
|
|
10030
|
+
if (state !== 'idle') g.classList.add('wf-node--' + state);
|
|
10031
|
+
if (wfState.selectedNodeId === step.id) g.classList.add('selected');
|
|
10032
|
+
g.dataset.stepId = step.id;
|
|
10033
|
+
g.setAttribute('transform', `translate(${x}, ${y})`);
|
|
10034
|
+
g.addEventListener('click', (e) => {
|
|
10035
|
+
e.stopPropagation();
|
|
10036
|
+
wfSelectNode(step.id);
|
|
10037
|
+
});
|
|
10038
|
+
|
|
10039
|
+
// Background rect
|
|
10040
|
+
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
10041
|
+
rect.setAttribute('width', WF_NODE_W);
|
|
10042
|
+
rect.setAttribute('height', WF_NODE_H);
|
|
10043
|
+
rect.setAttribute('fill', meta.color);
|
|
10044
|
+
rect.setAttribute('stroke', meta.color);
|
|
10045
|
+
rect.setAttribute('opacity', '0.85');
|
|
10046
|
+
g.appendChild(rect);
|
|
10047
|
+
|
|
10048
|
+
// Input port (left side): only if this node has dependencies
|
|
10049
|
+
if (hasDeps) {
|
|
10050
|
+
const inPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
10051
|
+
inPort.classList.add('wf-port', 'wf-port-in');
|
|
10052
|
+
inPort.setAttribute('cx', 0);
|
|
10053
|
+
inPort.setAttribute('cy', WF_NODE_H / 2);
|
|
10054
|
+
inPort.setAttribute('r', WF_PORT_R);
|
|
10055
|
+
inPort.setAttribute('stroke', meta.color);
|
|
10056
|
+
inPort.setAttribute('stroke-width', '2');
|
|
10057
|
+
g.appendChild(inPort);
|
|
10058
|
+
}
|
|
10059
|
+
|
|
10060
|
+
// Output port (right side): only if other nodes depend on this one
|
|
10061
|
+
if (hasDependents) {
|
|
10062
|
+
const outPort = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
10063
|
+
outPort.classList.add('wf-port', 'wf-port-out');
|
|
10064
|
+
outPort.setAttribute('cx', WF_NODE_W);
|
|
10065
|
+
outPort.setAttribute('cy', WF_NODE_H / 2);
|
|
10066
|
+
outPort.setAttribute('r', WF_PORT_R);
|
|
10067
|
+
outPort.setAttribute('fill', meta.color);
|
|
10068
|
+
outPort.setAttribute('stroke-width', '2');
|
|
10069
|
+
g.appendChild(outPort);
|
|
10070
|
+
}
|
|
10071
|
+
|
|
10072
|
+
// Icon
|
|
10073
|
+
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10074
|
+
icon.classList.add('wf-node-icon');
|
|
10075
|
+
icon.setAttribute('x', 22);
|
|
10076
|
+
icon.setAttribute('y', WF_NODE_H / 2);
|
|
10077
|
+
icon.textContent = meta.icon;
|
|
10078
|
+
g.appendChild(icon);
|
|
10079
|
+
|
|
10080
|
+
// Label (step name, truncated)
|
|
10081
|
+
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10082
|
+
label.classList.add('wf-node-label');
|
|
10083
|
+
label.setAttribute('x', WF_NODE_W / 2 + 10);
|
|
10084
|
+
label.setAttribute('y', WF_NODE_H / 2 - 7);
|
|
10085
|
+
const maxChars = 20;
|
|
10086
|
+
const displayName = (step.name || step.id).length > maxChars
|
|
10087
|
+
? (step.name || step.id).slice(0, maxChars - 2) + '..'
|
|
10088
|
+
: (step.name || step.id);
|
|
10089
|
+
label.textContent = displayName;
|
|
10090
|
+
g.appendChild(label);
|
|
10091
|
+
|
|
10092
|
+
// Tool badge
|
|
10093
|
+
const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10094
|
+
badge.classList.add('wf-node-badge');
|
|
10095
|
+
badge.setAttribute('x', WF_NODE_W / 2 + 10);
|
|
10096
|
+
badge.setAttribute('y', WF_NODE_H / 2 + 10);
|
|
10097
|
+
badge.textContent = meta.label;
|
|
10098
|
+
g.appendChild(badge);
|
|
10099
|
+
|
|
10100
|
+
// Condition indicator
|
|
10101
|
+
if (step.condition) {
|
|
10102
|
+
const cond = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10103
|
+
cond.classList.add('wf-node-condition');
|
|
10104
|
+
cond.setAttribute('x', WF_NODE_W - 8);
|
|
10105
|
+
cond.setAttribute('y', 14);
|
|
10106
|
+
cond.textContent = '\u26A1';
|
|
10107
|
+
g.appendChild(cond);
|
|
10108
|
+
}
|
|
10109
|
+
|
|
10110
|
+
// Status overlay (for execution)
|
|
10111
|
+
if (state === 'completed') {
|
|
10112
|
+
const check = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10113
|
+
check.classList.add('wf-node-status');
|
|
10114
|
+
check.setAttribute('x', WF_NODE_W - 18);
|
|
10115
|
+
check.setAttribute('y', WF_NODE_H / 2);
|
|
10116
|
+
check.textContent = '\u2713';
|
|
10117
|
+
g.appendChild(check);
|
|
10118
|
+
} else if (state === 'error') {
|
|
10119
|
+
const errIcon = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10120
|
+
errIcon.classList.add('wf-node-status');
|
|
10121
|
+
errIcon.setAttribute('x', WF_NODE_W - 18);
|
|
10122
|
+
errIcon.setAttribute('y', WF_NODE_H / 2);
|
|
10123
|
+
errIcon.textContent = '\u2717';
|
|
10124
|
+
g.appendChild(errIcon);
|
|
10125
|
+
} else if (state === 'skipped') {
|
|
10126
|
+
const skip = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10127
|
+
skip.classList.add('wf-node-status');
|
|
10128
|
+
skip.setAttribute('x', WF_NODE_W - 18);
|
|
10129
|
+
skip.setAttribute('y', WF_NODE_H / 2);
|
|
10130
|
+
skip.textContent = '\u2500';
|
|
10131
|
+
g.appendChild(skip);
|
|
10132
|
+
}
|
|
10133
|
+
|
|
10134
|
+
// Time badge (if completed)
|
|
10135
|
+
const result = wfState.executionResults[step.id];
|
|
10136
|
+
if (result && result.timeMs !== undefined) {
|
|
10137
|
+
const time = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
10138
|
+
time.classList.add('wf-node-time');
|
|
10139
|
+
time.setAttribute('x', WF_NODE_W / 2);
|
|
10140
|
+
time.setAttribute('y', WF_NODE_H + 16);
|
|
10141
|
+
time.textContent = result.timeMs + 'ms';
|
|
10142
|
+
g.appendChild(time);
|
|
10143
|
+
}
|
|
10144
|
+
|
|
10145
|
+
return g;
|
|
10146
|
+
}
|
|
10147
|
+
|
|
10148
|
+
function wfDrawEdge(fromId, toId, positions) {
|
|
10149
|
+
const from = positions[fromId];
|
|
10150
|
+
const to = positions[toId];
|
|
10151
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10152
|
+
g.classList.add('wf-edge-group-item');
|
|
10153
|
+
g.dataset.from = fromId;
|
|
10154
|
+
g.dataset.to = toId;
|
|
10155
|
+
|
|
10156
|
+
// Port positions: output port on right side of source, input port on left side of target
|
|
10157
|
+
const x1 = from.x + WF_NODE_W;
|
|
10158
|
+
const y1 = from.y + WF_NODE_H / 2;
|
|
10159
|
+
const x2 = to.x;
|
|
10160
|
+
const y2 = to.y + WF_NODE_H / 2;
|
|
10161
|
+
|
|
10162
|
+
// Detect backward edges (target is same column or to the left)
|
|
10163
|
+
const isBackward = x2 <= x1;
|
|
10164
|
+
|
|
10165
|
+
let d;
|
|
10166
|
+
if (isBackward) {
|
|
10167
|
+
// Route backward edges: go down from output, loop under/over, come in from left of target
|
|
10168
|
+
const midY = Math.max(from.y + WF_NODE_H, to.y + WF_NODE_H) + 40;
|
|
10169
|
+
d = `M ${x1} ${y1} C ${x1 + 50} ${y1}, ${x1 + 50} ${midY}, ${(x1 + x2) / 2} ${midY} C ${x2 - 50} ${midY}, ${x2 - 50} ${y2}, ${x2} ${y2}`;
|
|
10170
|
+
} else {
|
|
10171
|
+
// Normal left-to-right bezier
|
|
10172
|
+
const dx = Math.abs(x2 - x1);
|
|
10173
|
+
const tension = Math.max(dx * 0.4, 40);
|
|
10174
|
+
d = `M ${x1} ${y1} C ${x1 + tension} ${y1}, ${x2 - tension} ${y2}, ${x2} ${y2}`;
|
|
10175
|
+
}
|
|
10176
|
+
|
|
10177
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
10178
|
+
path.classList.add('wf-edge');
|
|
10179
|
+
if (isBackward) path.classList.add('wf-edge--backward');
|
|
10180
|
+
path.setAttribute('d', d);
|
|
10181
|
+
|
|
10182
|
+
// Set state-based classes
|
|
10183
|
+
const fromState = wfState.executionState[fromId];
|
|
10184
|
+
const toState = wfState.executionState[toId];
|
|
10185
|
+
if (toState === 'running') path.classList.add('wf-edge--active');
|
|
10186
|
+
else if (fromState === 'completed' && (toState === 'completed' || toState === 'skipped')) path.classList.add('wf-edge--complete');
|
|
10187
|
+
|
|
10188
|
+
g.appendChild(path);
|
|
10189
|
+
return g;
|
|
10190
|
+
}
|
|
10191
|
+
|
|
10192
|
+
// ── Inspector Toggle ──
|
|
10193
|
+
function wfToggleInspector() {
|
|
10194
|
+
const panel = document.getElementById('wfInspector');
|
|
10195
|
+
const btn = document.getElementById('wfInspectorToggle');
|
|
10196
|
+
if (!panel) return;
|
|
10197
|
+
panel.classList.toggle('collapsed');
|
|
10198
|
+
if (btn) btn.innerHTML = panel.classList.contains('collapsed') ? '‹' : '›';
|
|
10199
|
+
}
|
|
10200
|
+
|
|
10201
|
+
function wfOpenInspector() {
|
|
10202
|
+
const panel = document.getElementById('wfInspector');
|
|
10203
|
+
const btn = document.getElementById('wfInspectorToggle');
|
|
10204
|
+
if (!panel || !panel.classList.contains('collapsed')) return;
|
|
10205
|
+
panel.classList.remove('collapsed');
|
|
10206
|
+
if (btn) btn.innerHTML = '›';
|
|
10207
|
+
}
|
|
10208
|
+
|
|
10209
|
+
// ── Node Selection ──
|
|
10210
|
+
function wfSelectNode(stepId) {
|
|
10211
|
+
wfState.selectedNodeId = stepId;
|
|
10212
|
+
// Update visual selection
|
|
10213
|
+
document.querySelectorAll('.wf-node').forEach(n => {
|
|
10214
|
+
n.classList.toggle('selected', n.dataset.stepId === stepId);
|
|
10215
|
+
});
|
|
10216
|
+
wfOpenInspector();
|
|
10217
|
+
wfUpdateInspector();
|
|
10218
|
+
}
|
|
10219
|
+
|
|
10220
|
+
function wfDeselectNode() {
|
|
10221
|
+
wfState.selectedNodeId = null;
|
|
10222
|
+
document.querySelectorAll('.wf-node.selected').forEach(n => n.classList.remove('selected'));
|
|
10223
|
+
wfUpdateInspector();
|
|
10224
|
+
}
|
|
10225
|
+
|
|
10226
|
+
// ── Inspector ──
|
|
10227
|
+
function wfUpdateInspector() {
|
|
10228
|
+
const body = document.getElementById('wfInspectorBody');
|
|
10229
|
+
const header = document.getElementById('wfInspectorHeader');
|
|
10230
|
+
if (!body || !header) return;
|
|
10231
|
+
|
|
10232
|
+
const def = wfState.activeWorkflow;
|
|
10233
|
+
if (!def) {
|
|
10234
|
+
header.textContent = 'Inspector';
|
|
10235
|
+
body.innerHTML = '<div class="wf-inspector-empty">Select a workflow to view details</div>';
|
|
10236
|
+
return;
|
|
10237
|
+
}
|
|
10238
|
+
|
|
10239
|
+
try {
|
|
10240
|
+
if (!wfState.selectedNodeId) {
|
|
10241
|
+
// Show workflow-level info
|
|
10242
|
+
header.textContent = def.name || 'Workflow';
|
|
10243
|
+
let html = '';
|
|
10244
|
+
|
|
10245
|
+
// Description
|
|
10246
|
+
if (def.description) {
|
|
10247
|
+
html += `<div class="wf-inspector-section">
|
|
10248
|
+
<div class="wf-inspector-section-title">Description</div>
|
|
10249
|
+
<div style="font-size:12px;color:var(--text);line-height:1.4;">${escapeHtml(def.description)}</div>
|
|
10250
|
+
</div>`;
|
|
10251
|
+
}
|
|
10252
|
+
|
|
10253
|
+
// Inputs
|
|
10254
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
10255
|
+
html += '<div class="wf-inspector-section"><div class="wf-inspector-section-title">Inputs</div>';
|
|
10256
|
+
for (const [key, spec] of Object.entries(def.inputs)) {
|
|
10257
|
+
const req = spec.required ? ' <span style="color:#e74c3c">*</span>' : '';
|
|
10258
|
+
const defVal = spec.default !== undefined ? ` (default: ${spec.default})` : '';
|
|
10259
|
+
html += `<div style="margin-bottom:8px;">
|
|
10260
|
+
<div style="font-size:12px;font-weight:600;color:var(--text);">${escapeHtml(key)}${req}</div>
|
|
10261
|
+
<div style="font-size:11px;color:var(--text-muted);">${escapeHtml(spec.description || spec.type || '')}${defVal}</div>
|
|
10262
|
+
<input class="wf-inspector-input" id="wf-input-${key}" placeholder="${escapeHtml(key)}" value="${spec.default !== undefined ? spec.default : ''}">
|
|
10263
|
+
</div>`;
|
|
10264
|
+
}
|
|
10265
|
+
html += '</div>';
|
|
10266
|
+
}
|
|
10267
|
+
|
|
10268
|
+
// Steps summary
|
|
10269
|
+
html += `<div class="wf-inspector-section">
|
|
10270
|
+
<div class="wf-inspector-section-title">Steps</div>
|
|
10271
|
+
<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>
|
|
10272
|
+
</div>`;
|
|
10273
|
+
|
|
10274
|
+
// Output mapping
|
|
10275
|
+
if (def.output) {
|
|
10276
|
+
html += `<div class="wf-inspector-section">
|
|
10277
|
+
<div class="wf-inspector-section-title">Output</div>
|
|
10278
|
+
<div class="wf-inspector-code">${escapeHtml(JSON.stringify(def.output, null, 2))}</div>
|
|
10279
|
+
</div>`;
|
|
10280
|
+
}
|
|
10281
|
+
|
|
10282
|
+
// Execution result
|
|
10283
|
+
if (wfState.executionResults._done) {
|
|
10284
|
+
const r = wfState.executionResults._done;
|
|
10285
|
+
const doneJson = JSON.stringify(r.output, null, 2);
|
|
10286
|
+
html += `<div class="wf-inspector-section">
|
|
10287
|
+
<div class="wf-inspector-section-title">Result</div>
|
|
10288
|
+
<div class="wf-inspector-result success">
|
|
10289
|
+
<div style="font-weight:600;margin-bottom:4px;">Completed in ${r.totalTimeMs}ms</div>
|
|
10290
|
+
<div class="wf-inspector-code" style="max-height:150px;overflow:auto;">${escapeHtml(doneJson)}</div>
|
|
10291
|
+
</div>
|
|
10292
|
+
<button class="wf-output-expand-btn" data-expand-step="_done">⤢ Expand</button>
|
|
10293
|
+
</div>`;
|
|
10294
|
+
}
|
|
10295
|
+
|
|
10296
|
+
body.innerHTML = html;
|
|
10297
|
+
wfBindExpandButtons(body);
|
|
10298
|
+
return;
|
|
10299
|
+
}
|
|
10300
|
+
|
|
10301
|
+
// Show step details
|
|
10302
|
+
const step = def.steps.find(s => s.id === wfState.selectedNodeId);
|
|
10303
|
+
if (!step) {
|
|
10304
|
+
body.innerHTML = '<div class="wf-inspector-empty">Step not found: ' + escapeHtml(wfState.selectedNodeId) + '</div>';
|
|
10305
|
+
return;
|
|
10306
|
+
}
|
|
10307
|
+
|
|
10308
|
+
const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666' };
|
|
10309
|
+
header.textContent = step.name || step.id;
|
|
10310
|
+
|
|
10311
|
+
let html = '';
|
|
10312
|
+
|
|
10313
|
+
// Tool badge
|
|
10314
|
+
html += `<div class="wf-inspector-section">
|
|
10315
|
+
<div class="wf-inspector-section-title">Tool</div>
|
|
10316
|
+
<span class="wf-tool-badge" style="background:${meta.color}">${meta.icon} ${meta.label}</span>
|
|
10317
|
+
</div>`;
|
|
10318
|
+
|
|
10319
|
+
// Step ID
|
|
10320
|
+
html += `<div class="wf-inspector-section">
|
|
10321
|
+
<div class="wf-inspector-section-title">ID</div>
|
|
10322
|
+
<div style="font-size:12px;color:var(--text);font-family:monospace;">${escapeHtml(step.id)}</div>
|
|
10323
|
+
</div>`;
|
|
10324
|
+
|
|
10325
|
+
// Inputs
|
|
10326
|
+
if (step.inputs) {
|
|
10327
|
+
html += `<div class="wf-inspector-section">
|
|
10328
|
+
<div class="wf-inspector-section-title">Inputs</div>`;
|
|
10329
|
+
for (const [key, val] of Object.entries(step.inputs)) {
|
|
10330
|
+
const display = typeof val === 'string' ? val : JSON.stringify(val);
|
|
10331
|
+
html += `<div class="wf-inspector-field">
|
|
10332
|
+
<span class="wf-inspector-field-label">${escapeHtml(key)}</span>
|
|
10333
|
+
<span class="wf-inspector-field-value" style="font-family:monospace;font-size:11px;">${escapeHtml(display)}</span>
|
|
10334
|
+
</div>`;
|
|
10335
|
+
}
|
|
10336
|
+
html += '</div>';
|
|
10337
|
+
}
|
|
10338
|
+
|
|
10339
|
+
// Condition
|
|
10340
|
+
if (step.condition) {
|
|
10341
|
+
html += `<div class="wf-inspector-section">
|
|
10342
|
+
<div class="wf-inspector-section-title">Condition \u26A1</div>
|
|
10343
|
+
<div class="wf-inspector-code">${escapeHtml(step.condition)}</div>
|
|
10344
|
+
</div>`;
|
|
10345
|
+
}
|
|
10346
|
+
|
|
10347
|
+
// ForEach
|
|
10348
|
+
if (step.forEach) {
|
|
10349
|
+
html += `<div class="wf-inspector-section">
|
|
10350
|
+
<div class="wf-inspector-section-title">ForEach</div>
|
|
10351
|
+
<div class="wf-inspector-code">${escapeHtml(JSON.stringify(step.forEach, null, 2))}</div>
|
|
10352
|
+
</div>`;
|
|
10353
|
+
}
|
|
10354
|
+
|
|
10355
|
+
// Execution result for this step
|
|
10356
|
+
const result = wfState.executionResults[step.id];
|
|
10357
|
+
const state = wfState.executionState[step.id];
|
|
10358
|
+
if (state === 'completed' && result) {
|
|
10359
|
+
const outputJson = result.output ? JSON.stringify(result.output, null, 2) : '';
|
|
10360
|
+
const stepTitle = step.name || step.id;
|
|
10361
|
+
html += `<div class="wf-inspector-section">
|
|
10362
|
+
<div class="wf-inspector-section-title">Result</div>
|
|
10363
|
+
<div class="wf-inspector-result success">
|
|
10364
|
+
<div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">${result.timeMs}ms${result.summary ? ', ' + result.summary : ''}</div>
|
|
10365
|
+
${outputJson ? '<div class="wf-inspector-code" style="max-height:120px;overflow:auto;">' + escapeHtml(outputJson) + '</div>' : ''}
|
|
10366
|
+
</div>
|
|
10367
|
+
${outputJson ? '<button class="wf-output-expand-btn" data-expand-step="' + escapeHtml(step.id) + '">⤢ Expand</button>' : ''}
|
|
10368
|
+
</div>`;
|
|
10369
|
+
} else if (state === 'error' && result) {
|
|
10370
|
+
html += `<div class="wf-inspector-section">
|
|
10371
|
+
<div class="wf-inspector-section-title">Error</div>
|
|
10372
|
+
<div class="wf-inspector-result error">
|
|
10373
|
+
<div style="font-size:12px;margin-bottom:4px;">${escapeHtml(result.error || 'Unknown error')}</div>
|
|
10374
|
+
</div>
|
|
10375
|
+
</div>`;
|
|
10376
|
+
} else if (state === 'skipped' && result) {
|
|
10377
|
+
html += `<div class="wf-inspector-section">
|
|
10378
|
+
<div class="wf-inspector-section-title">Skipped</div>
|
|
10379
|
+
<div style="font-size:12px;color:var(--text-muted);">${escapeHtml(result.reason || 'Condition not met')}</div>
|
|
10380
|
+
</div>`;
|
|
10381
|
+
}
|
|
10382
|
+
|
|
10383
|
+
body.innerHTML = html;
|
|
10384
|
+
wfBindExpandButtons(body);
|
|
10385
|
+
} catch (err) {
|
|
10386
|
+
console.error('Inspector render error:', err);
|
|
10387
|
+
body.innerHTML = '<div class="wf-inspector-empty" style="color:#e74c3c;">Error rendering inspector: ' + escapeHtml(err.message) + '</div>';
|
|
10388
|
+
}
|
|
10389
|
+
}
|
|
10390
|
+
|
|
10391
|
+
function wfBindExpandButtons(container) {
|
|
10392
|
+
container.querySelectorAll('.wf-output-expand-btn[data-expand-step]').forEach(btn => {
|
|
10393
|
+
btn.addEventListener('click', () => {
|
|
10394
|
+
const stepId = btn.dataset.expandStep;
|
|
10395
|
+
const result = wfState.executionResults[stepId];
|
|
10396
|
+
if (!result) return;
|
|
10397
|
+
let title, content;
|
|
10398
|
+
if (stepId === '_done') {
|
|
10399
|
+
title = 'Workflow Output';
|
|
10400
|
+
content = JSON.stringify(result.output, null, 2);
|
|
10401
|
+
} else {
|
|
10402
|
+
const def = wfState.activeWorkflow;
|
|
10403
|
+
const step = def ? def.steps.find(s => s.id === stepId) : null;
|
|
10404
|
+
title = (step ? step.name || step.id : stepId) + ' Output';
|
|
10405
|
+
content = JSON.stringify(result.output, null, 2);
|
|
10406
|
+
}
|
|
10407
|
+
wfOpenOutputModal(title, content);
|
|
10408
|
+
});
|
|
10409
|
+
});
|
|
10410
|
+
}
|
|
10411
|
+
|
|
10412
|
+
// escapeHtml is already defined globally — reuse it
|
|
10413
|
+
|
|
10414
|
+
// ── Toolbar helpers ──
|
|
10415
|
+
function wfSetToolbarEnabled(enabled) {
|
|
10416
|
+
document.getElementById('wfRunBtn').disabled = !enabled;
|
|
10417
|
+
document.getElementById('wfDryRunBtn').disabled = !enabled;
|
|
10418
|
+
document.getElementById('wfExportBtn').disabled = !enabled;
|
|
10419
|
+
}
|
|
10420
|
+
|
|
10421
|
+
// ── Export workflow JSON ──
|
|
10422
|
+
function wfExportJson() {
|
|
10423
|
+
const def = wfState.activeWorkflow;
|
|
10424
|
+
if (!def) return;
|
|
10425
|
+
const json = JSON.stringify(def, null, 2);
|
|
10426
|
+
const name = (def.name || 'workflow').replace(/\s+/g, '-').toLowerCase();
|
|
10427
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
10428
|
+
const url = URL.createObjectURL(blob);
|
|
10429
|
+
const a = document.createElement('a');
|
|
10430
|
+
a.href = url;
|
|
10431
|
+
a.download = name + '.vai-workflow.json';
|
|
10432
|
+
document.body.appendChild(a);
|
|
10433
|
+
a.click();
|
|
10434
|
+
document.body.removeChild(a);
|
|
10435
|
+
URL.revokeObjectURL(url);
|
|
10436
|
+
}
|
|
10437
|
+
|
|
10438
|
+
// ── Dry Run ──
|
|
10439
|
+
function wfDryRun() {
|
|
10440
|
+
const def = wfState.activeWorkflow;
|
|
10441
|
+
if (!def || !wfState.layers) return;
|
|
10442
|
+
|
|
10443
|
+
const layers = wfState.layers;
|
|
10444
|
+
const stepMap = {};
|
|
10445
|
+
def.steps.forEach(s => { stepMap[s.id] = s; });
|
|
10446
|
+
|
|
10447
|
+
// Build layer HTML
|
|
10448
|
+
let layersHtml = '';
|
|
10449
|
+
let totalSteps = 0;
|
|
10450
|
+
let conditionalSteps = 0;
|
|
10451
|
+
|
|
10452
|
+
layers.forEach((layer, i) => {
|
|
10453
|
+
const parallel = layer.length > 1 ? ' (parallel)' : '';
|
|
10454
|
+
layersHtml += '<div class="wf-dryrun-layer">';
|
|
10455
|
+
layersHtml += '<div class="wf-dryrun-layer-title">';
|
|
10456
|
+
layersHtml += '<span class="wf-dryrun-layer-badge">' + (i + 1) + '</span>';
|
|
10457
|
+
layersHtml += ' Layer ' + (i + 1) + parallel;
|
|
10458
|
+
layersHtml += '</div>';
|
|
10459
|
+
|
|
10460
|
+
layer.forEach(stepId => {
|
|
10461
|
+
const step = stepMap[stepId];
|
|
10462
|
+
if (!step) return;
|
|
10463
|
+
totalSteps++;
|
|
10464
|
+
const meta = WF_NODE_META[step.tool] || { icon: '\u2699', label: step.tool, color: '#666' };
|
|
10465
|
+
layersHtml += '<div class="wf-dryrun-step">';
|
|
10466
|
+
layersHtml += '<span class="wf-dryrun-step-icon">' + meta.icon + '</span>';
|
|
10467
|
+
layersHtml += '<div class="wf-dryrun-step-info">';
|
|
10468
|
+
layersHtml += '<div class="wf-dryrun-step-name">' + escapeHtml(step.name || step.id) + '</div>';
|
|
10469
|
+
layersHtml += '<div class="wf-dryrun-step-tool">' + escapeHtml(meta.label) + ' (' + escapeHtml(step.id) + ')</div>';
|
|
10470
|
+
if (step.condition) {
|
|
10471
|
+
conditionalSteps++;
|
|
10472
|
+
layersHtml += '<div class="wf-dryrun-step-cond">\u26A1 ' + escapeHtml(step.condition) + '</div>';
|
|
10473
|
+
}
|
|
10474
|
+
layersHtml += '</div></div>';
|
|
10475
|
+
});
|
|
10476
|
+
|
|
10477
|
+
layersHtml += '</div>';
|
|
10478
|
+
});
|
|
10479
|
+
|
|
10480
|
+
// Summary stats
|
|
10481
|
+
const parallelLayers = layers.filter(l => l.length > 1).length;
|
|
10482
|
+
let summaryHtml = '<div class="wf-dryrun-summary">';
|
|
10483
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + totalSteps + '</div><div class="wf-dryrun-stat-label">Steps</div></div>';
|
|
10484
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + layers.length + '</div><div class="wf-dryrun-stat-label">Layers</div></div>';
|
|
10485
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + parallelLayers + '</div><div class="wf-dryrun-stat-label">Parallel</div></div>';
|
|
10486
|
+
if (conditionalSteps > 0) {
|
|
10487
|
+
summaryHtml += '<div class="wf-dryrun-stat"><div class="wf-dryrun-stat-value">' + conditionalSteps + '</div><div class="wf-dryrun-stat-label">Conditional</div></div>';
|
|
10488
|
+
}
|
|
10489
|
+
summaryHtml += '</div>';
|
|
10490
|
+
|
|
10491
|
+
// Inputs summary
|
|
10492
|
+
let inputsHtml = '';
|
|
10493
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
10494
|
+
inputsHtml += '<div style="margin-bottom:12px;font-size:12px;">';
|
|
10495
|
+
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>';
|
|
10496
|
+
for (const [key, spec] of Object.entries(def.inputs)) {
|
|
10497
|
+
const req = spec.required ? ' *' : '';
|
|
10498
|
+
inputsHtml += '<div style="margin-bottom:2px;"><span style="font-weight:600;color:var(--text);">' + escapeHtml(key) + req + '</span>';
|
|
10499
|
+
inputsHtml += ' <span style="color:var(--text-muted);">(' + escapeHtml(spec.type || 'string') + ')</span></div>';
|
|
10500
|
+
}
|
|
10501
|
+
inputsHtml += '</div>';
|
|
10502
|
+
}
|
|
10503
|
+
|
|
10504
|
+
// Create overlay
|
|
10505
|
+
const canvasArea = document.querySelector('.wf-canvas-area');
|
|
10506
|
+
// Remove any existing overlay
|
|
10507
|
+
canvasArea.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
|
|
10508
|
+
|
|
10509
|
+
const overlay = document.createElement('div');
|
|
10510
|
+
overlay.className = 'wf-dryrun-overlay';
|
|
10511
|
+
overlay.innerHTML = '<div class="wf-dryrun-panel">' +
|
|
10512
|
+
'<div class="wf-dryrun-header">' +
|
|
10513
|
+
'<span class="wf-dryrun-title">Execution Plan: ' + escapeHtml(def.name || 'Workflow') + '</span>' +
|
|
10514
|
+
'<button class="wf-dryrun-close" onclick="wfCloseDryRun()" title="Close">×</button>' +
|
|
10515
|
+
'</div>' +
|
|
10516
|
+
'<div class="wf-dryrun-body">' +
|
|
10517
|
+
inputsHtml +
|
|
10518
|
+
summaryHtml +
|
|
10519
|
+
'<div style="margin-top:16px;">' + layersHtml + '</div>' +
|
|
10520
|
+
'</div>' +
|
|
10521
|
+
'</div>';
|
|
10522
|
+
|
|
10523
|
+
// Click backdrop to close
|
|
10524
|
+
overlay.addEventListener('click', (e) => {
|
|
10525
|
+
if (e.target === overlay) wfCloseDryRun();
|
|
10526
|
+
});
|
|
10527
|
+
|
|
10528
|
+
canvasArea.appendChild(overlay);
|
|
10529
|
+
}
|
|
10530
|
+
|
|
10531
|
+
function wfCloseDryRun() {
|
|
10532
|
+
document.querySelectorAll('.wf-dryrun-overlay').forEach(el => el.remove());
|
|
10533
|
+
}
|
|
10534
|
+
|
|
10535
|
+
// ── Execution ──
|
|
10536
|
+
let wfAbortController = null;
|
|
10537
|
+
let wfExecTimer = null;
|
|
10538
|
+
let wfExecStartTime = 0;
|
|
10539
|
+
const WF_EXEC_TIMEOUT_MS = 120000; // 2 minute total timeout
|
|
10540
|
+
|
|
10541
|
+
function wfShowExecStatus(text, state) {
|
|
10542
|
+
const bar = document.getElementById('wfExecStatus');
|
|
10543
|
+
const textEl = document.getElementById('wfExecStatusText');
|
|
10544
|
+
if (!bar || !textEl) return;
|
|
10545
|
+
bar.style.display = 'flex';
|
|
10546
|
+
bar.className = 'wf-exec-status' + (state ? ' ' + state : '');
|
|
10547
|
+
textEl.textContent = text;
|
|
10548
|
+
}
|
|
10549
|
+
|
|
10550
|
+
function wfHideExecStatus() {
|
|
10551
|
+
const bar = document.getElementById('wfExecStatus');
|
|
10552
|
+
if (bar) bar.style.display = 'none';
|
|
10553
|
+
}
|
|
10554
|
+
|
|
10555
|
+
function wfUpdateExecTimer() {
|
|
10556
|
+
const timeEl = document.getElementById('wfExecStatusTime');
|
|
10557
|
+
if (!timeEl || !wfState.executing) return;
|
|
10558
|
+
const elapsed = Date.now() - wfExecStartTime;
|
|
10559
|
+
const secs = (elapsed / 1000).toFixed(1);
|
|
10560
|
+
timeEl.textContent = secs + 's';
|
|
10561
|
+
|
|
10562
|
+
// Check timeout
|
|
10563
|
+
if (elapsed > WF_EXEC_TIMEOUT_MS) {
|
|
10564
|
+
wfStopExecution('Execution timed out after ' + (WF_EXEC_TIMEOUT_MS / 1000) + 's');
|
|
10565
|
+
return;
|
|
10566
|
+
}
|
|
10567
|
+
wfExecTimer = requestAnimationFrame(wfUpdateExecTimer);
|
|
10568
|
+
}
|
|
10569
|
+
|
|
10570
|
+
function wfStopExecution(reason) {
|
|
10571
|
+
if (!wfState.executing) return;
|
|
10572
|
+
if (wfAbortController) {
|
|
10573
|
+
wfAbortController.abort();
|
|
10574
|
+
wfAbortController = null;
|
|
10575
|
+
}
|
|
10576
|
+
if (wfExecTimer) {
|
|
10577
|
+
cancelAnimationFrame(wfExecTimer);
|
|
10578
|
+
wfExecTimer = null;
|
|
10579
|
+
}
|
|
10580
|
+
|
|
10581
|
+
// Mark any still-running nodes as error
|
|
10582
|
+
const def = wfState.activeWorkflow;
|
|
10583
|
+
if (def) {
|
|
10584
|
+
def.steps.forEach(s => {
|
|
10585
|
+
if (wfState.executionState[s.id] === 'running') {
|
|
10586
|
+
wfState.executionState[s.id] = 'error';
|
|
10587
|
+
wfState.executionResults[s.id] = { error: reason || 'Stopped by user' };
|
|
10588
|
+
} else if (wfState.executionState[s.id] === 'pending') {
|
|
10589
|
+
wfState.executionState[s.id] = 'skipped';
|
|
10590
|
+
wfState.executionResults[s.id] = { reason: reason || 'Stopped by user' };
|
|
10591
|
+
}
|
|
10592
|
+
});
|
|
10593
|
+
wfRefreshNodes();
|
|
10594
|
+
}
|
|
10595
|
+
|
|
10596
|
+
const stopReason = reason || 'Stopped by user';
|
|
10597
|
+
wfShowExecStatus(stopReason, reason && reason.includes('timed out') ? 'error' : 'stopped');
|
|
10598
|
+
wfState.executing = false;
|
|
10599
|
+
wfSetToolbarEnabled(true);
|
|
10600
|
+
document.getElementById('wfStopBtn').style.display = 'none';
|
|
10601
|
+
wfUpdateInspector();
|
|
10602
|
+
}
|
|
10603
|
+
|
|
10604
|
+
async function wfExecute() {
|
|
10605
|
+
const def = wfState.activeWorkflow;
|
|
10606
|
+
if (!def || wfState.executing) return;
|
|
10607
|
+
|
|
10608
|
+
// Collect inputs
|
|
10609
|
+
const inputs = {};
|
|
10610
|
+
if (def.inputs) {
|
|
10611
|
+
for (const [key, spec] of Object.entries(def.inputs)) {
|
|
10612
|
+
const el = document.getElementById('wf-input-' + key);
|
|
10613
|
+
let val = el ? el.value : (spec.default !== undefined ? spec.default : undefined);
|
|
10614
|
+
if (val === '' && spec.default !== undefined) val = spec.default;
|
|
10615
|
+
if (val === '' && spec.required) {
|
|
10616
|
+
alert('Input "' + key + '" is required');
|
|
10617
|
+
return;
|
|
10618
|
+
}
|
|
10619
|
+
// Type coerce
|
|
10620
|
+
if (spec.type === 'number' && val !== undefined && val !== '') val = Number(val);
|
|
10621
|
+
if (val !== undefined && val !== '') inputs[key] = val;
|
|
10622
|
+
}
|
|
10623
|
+
}
|
|
10624
|
+
|
|
10625
|
+
wfState.executing = true;
|
|
10626
|
+
wfState.executionResults = {};
|
|
10627
|
+
wfAbortController = new AbortController();
|
|
10628
|
+
wfExecStartTime = Date.now();
|
|
10629
|
+
wfSetToolbarEnabled(false);
|
|
10630
|
+
document.getElementById('wfStopBtn').style.display = '';
|
|
10631
|
+
wfShowExecStatus('Running...', '');
|
|
10632
|
+
wfExecTimer = requestAnimationFrame(wfUpdateExecTimer);
|
|
10633
|
+
|
|
10634
|
+
// Reset all nodes to pending
|
|
10635
|
+
def.steps.forEach(s => { wfState.executionState[s.id] = 'pending'; });
|
|
10636
|
+
wfRefreshNodes();
|
|
10637
|
+
|
|
10638
|
+
let hasError = false;
|
|
10639
|
+
let errorMessage = '';
|
|
10640
|
+
|
|
10641
|
+
try {
|
|
10642
|
+
const res = await fetch('/api/workflows/execute', {
|
|
10643
|
+
method: 'POST',
|
|
10644
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10645
|
+
body: JSON.stringify({ definition: def, inputs }),
|
|
10646
|
+
signal: wfAbortController.signal,
|
|
10647
|
+
});
|
|
10648
|
+
|
|
10649
|
+
if (!res.ok) {
|
|
10650
|
+
throw new Error('Server returned ' + res.status + ': ' + (await res.text()));
|
|
10651
|
+
}
|
|
10652
|
+
|
|
10653
|
+
const reader = res.body.getReader();
|
|
10654
|
+
const decoder = new TextDecoder();
|
|
10655
|
+
let buffer = '';
|
|
10656
|
+
let currentEvent = '';
|
|
10657
|
+
|
|
10658
|
+
while (true) {
|
|
10659
|
+
const { done, value } = await reader.read();
|
|
10660
|
+
if (done) break;
|
|
10661
|
+
buffer += decoder.decode(value, { stream: true });
|
|
10662
|
+
|
|
10663
|
+
const lines = buffer.split('\n');
|
|
10664
|
+
buffer = lines.pop() || '';
|
|
10665
|
+
|
|
10666
|
+
for (const line of lines) {
|
|
10667
|
+
if (line.startsWith('event: ')) {
|
|
10668
|
+
currentEvent = line.slice(7).trim();
|
|
10669
|
+
} else if (line.startsWith('data: ') && currentEvent) {
|
|
10670
|
+
let data;
|
|
10671
|
+
try { data = JSON.parse(line.slice(6)); } catch { continue; }
|
|
10672
|
+
|
|
10673
|
+
if (currentEvent === 'step_start') {
|
|
10674
|
+
wfState.executionState[data.stepId] = 'running';
|
|
10675
|
+
const stepName = (def.steps.find(s => s.id === data.stepId) || {}).name || data.stepId;
|
|
10676
|
+
wfShowExecStatus('Running: ' + stepName, '');
|
|
10677
|
+
wfRefreshNodes();
|
|
10678
|
+
} else if (currentEvent === 'step_complete') {
|
|
10679
|
+
wfState.executionState[data.stepId] = 'completed';
|
|
10680
|
+
wfState.executionResults[data.stepId] = {
|
|
10681
|
+
output: data.output,
|
|
10682
|
+
timeMs: data.timeMs,
|
|
10683
|
+
summary: data.summary || '',
|
|
10684
|
+
};
|
|
10685
|
+
wfRefreshNodes();
|
|
10686
|
+
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
10687
|
+
} else if (currentEvent === 'step_skip') {
|
|
10688
|
+
wfState.executionState[data.stepId] = 'skipped';
|
|
10689
|
+
wfState.executionResults[data.stepId] = { reason: data.reason };
|
|
10690
|
+
wfRefreshNodes();
|
|
10691
|
+
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
10692
|
+
} else if (currentEvent === 'step_error') {
|
|
10693
|
+
wfState.executionState[data.stepId] = 'error';
|
|
10694
|
+
wfState.executionResults[data.stepId] = { error: data.error };
|
|
10695
|
+
hasError = true;
|
|
10696
|
+
errorMessage = data.error || 'Step failed';
|
|
10697
|
+
wfRefreshNodes();
|
|
10698
|
+
if (wfState.selectedNodeId === data.stepId) wfUpdateInspector();
|
|
10699
|
+
} else if (currentEvent === 'done') {
|
|
10700
|
+
wfState.executionResults._done = data;
|
|
10701
|
+
if (!wfState.selectedNodeId) wfUpdateInspector();
|
|
10702
|
+
} else if (currentEvent === 'error') {
|
|
10703
|
+
hasError = true;
|
|
10704
|
+
errorMessage = data.error || 'Workflow error';
|
|
10705
|
+
console.error('Workflow execution error:', data.error);
|
|
10706
|
+
}
|
|
10707
|
+
currentEvent = '';
|
|
10708
|
+
}
|
|
10709
|
+
}
|
|
10710
|
+
}
|
|
10711
|
+
} catch (err) {
|
|
10712
|
+
if (err.name === 'AbortError') {
|
|
10713
|
+
// Handled by wfStopExecution
|
|
10714
|
+
return;
|
|
10715
|
+
}
|
|
10716
|
+
hasError = true;
|
|
10717
|
+
errorMessage = err.message || 'Execution failed';
|
|
10718
|
+
console.error('Workflow execution failed:', err);
|
|
10719
|
+
// Mark running nodes as error
|
|
10720
|
+
def.steps.forEach(s => {
|
|
10721
|
+
if (wfState.executionState[s.id] === 'running') {
|
|
10722
|
+
wfState.executionState[s.id] = 'error';
|
|
10723
|
+
wfState.executionResults[s.id] = { error: errorMessage };
|
|
10724
|
+
}
|
|
10725
|
+
});
|
|
10726
|
+
wfRefreshNodes();
|
|
10727
|
+
}
|
|
10728
|
+
|
|
10729
|
+
if (wfExecTimer) { cancelAnimationFrame(wfExecTimer); wfExecTimer = null; }
|
|
10730
|
+
wfAbortController = null;
|
|
10731
|
+
wfState.executing = false;
|
|
10732
|
+
wfSetToolbarEnabled(true);
|
|
10733
|
+
document.getElementById('wfStopBtn').style.display = 'none';
|
|
10734
|
+
|
|
10735
|
+
const elapsed = ((Date.now() - wfExecStartTime) / 1000).toFixed(1);
|
|
10736
|
+
if (hasError) {
|
|
10737
|
+
wfShowExecStatus('Failed: ' + errorMessage, 'error');
|
|
10738
|
+
} else {
|
|
10739
|
+
wfShowExecStatus('Completed in ' + elapsed + 's', 'done');
|
|
10740
|
+
}
|
|
10741
|
+
wfUpdateInspector();
|
|
10742
|
+
}
|
|
10743
|
+
|
|
10744
|
+
function wfRefreshNodes() {
|
|
10745
|
+
// Re-render the full SVG (simple approach: redraw)
|
|
10746
|
+
if (wfState.activeWorkflow) {
|
|
10747
|
+
const svg = document.getElementById('wf-canvas');
|
|
10748
|
+
const def = wfState.activeWorkflow;
|
|
10749
|
+
const positions = wfState.nodePositions;
|
|
10750
|
+
|
|
10751
|
+
// Remove existing nodes and edges
|
|
10752
|
+
svg.querySelectorAll('.wf-node, .wf-edge-group').forEach(el => el.remove());
|
|
10753
|
+
|
|
10754
|
+
// Redraw edges
|
|
10755
|
+
const edgeGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
10756
|
+
edgeGroup.classList.add('wf-edge-group');
|
|
10757
|
+
for (const [stepId, deps] of Object.entries(wfState.graph)) {
|
|
10758
|
+
if (!deps || !Array.isArray(deps)) continue;
|
|
10759
|
+
deps.forEach(rawDepId => {
|
|
10760
|
+
const depId = rawDepId.replace(/^!/, '');
|
|
10761
|
+
if (positions[depId] && positions[stepId]) {
|
|
10762
|
+
edgeGroup.appendChild(wfDrawEdge(depId, stepId, positions));
|
|
10763
|
+
}
|
|
10764
|
+
});
|
|
10765
|
+
}
|
|
10766
|
+
svg.appendChild(edgeGroup);
|
|
10767
|
+
|
|
10768
|
+
// Redraw nodes
|
|
10769
|
+
for (const step of def.steps) {
|
|
10770
|
+
const pos = positions[step.id];
|
|
10771
|
+
if (!pos) continue;
|
|
10772
|
+
const state = wfState.executionState[step.id] || 'idle';
|
|
10773
|
+
svg.appendChild(wfDrawNode(step, pos.x, pos.y, state));
|
|
10774
|
+
}
|
|
10775
|
+
}
|
|
10776
|
+
}
|
|
10777
|
+
|
|
10778
|
+
function wfResetExecution() {
|
|
10779
|
+
if (wfState.executing) return;
|
|
10780
|
+
wfState.executionState = {};
|
|
10781
|
+
wfState.executionResults = {};
|
|
10782
|
+
wfHideExecStatus();
|
|
10783
|
+
wfRefreshNodes();
|
|
10784
|
+
wfUpdateInspector();
|
|
10785
|
+
}
|
|
10786
|
+
|
|
10787
|
+
// ── Output Modal ──
|
|
10788
|
+
let wfOutputModalData = '';
|
|
10789
|
+
|
|
10790
|
+
function wfOpenOutputModal(title, content) {
|
|
10791
|
+
wfOutputModalData = content;
|
|
10792
|
+
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
10793
|
+
const titleEl = document.getElementById('wfOutputModalTitle');
|
|
10794
|
+
const bodyEl = document.getElementById('wfOutputModalBody');
|
|
10795
|
+
const copyLabel = document.getElementById('wfOutputCopyLabel');
|
|
10796
|
+
if (!backdrop || !bodyEl) return;
|
|
10797
|
+
titleEl.textContent = title || 'Output';
|
|
10798
|
+
bodyEl.textContent = content;
|
|
10799
|
+
copyLabel.textContent = 'Copy';
|
|
10800
|
+
backdrop.style.display = 'flex';
|
|
10801
|
+
}
|
|
10802
|
+
|
|
10803
|
+
function wfCloseOutputModal() {
|
|
10804
|
+
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
10805
|
+
if (backdrop) backdrop.style.display = 'none';
|
|
10806
|
+
}
|
|
10807
|
+
|
|
10808
|
+
function wfCopyOutput() {
|
|
10809
|
+
const label = document.getElementById('wfOutputCopyLabel');
|
|
10810
|
+
navigator.clipboard.writeText(wfOutputModalData).then(() => {
|
|
10811
|
+
if (label) { label.textContent = 'Copied!'; setTimeout(() => { label.textContent = 'Copy'; }, 2000); }
|
|
10812
|
+
}).catch(() => {
|
|
10813
|
+
if (label) label.textContent = 'Failed';
|
|
10814
|
+
});
|
|
10815
|
+
}
|
|
10816
|
+
|
|
10817
|
+
// Close modal on Escape
|
|
10818
|
+
document.addEventListener('keydown', (e) => {
|
|
10819
|
+
if (e.key === 'Escape') {
|
|
10820
|
+
const backdrop = document.getElementById('wfOutputModalBackdrop');
|
|
10821
|
+
if (backdrop && backdrop.style.display !== 'none') {
|
|
10822
|
+
wfCloseOutputModal();
|
|
10823
|
+
e.preventDefault();
|
|
10824
|
+
}
|
|
10825
|
+
}
|
|
10826
|
+
});
|
|
10827
|
+
|
|
10828
|
+
// ── Zoom & Pan ──
|
|
10829
|
+
function wfZoom(direction) {
|
|
10830
|
+
// Multiplicative zoom: feels consistent at any zoom level
|
|
10831
|
+
const factor = direction > 0 ? 1.1 : 1 / 1.1;
|
|
10832
|
+
wfState.zoom = Math.max(0.2, Math.min(5, wfState.zoom * factor));
|
|
10833
|
+
wfApplyViewBox();
|
|
10834
|
+
}
|
|
10835
|
+
|
|
10836
|
+
function wfFitToView() {
|
|
10837
|
+
const svg = document.getElementById('wf-canvas');
|
|
10838
|
+
if (!svg) return;
|
|
10839
|
+
const positions = wfState.nodePositions;
|
|
10840
|
+
const ids = Object.keys(positions);
|
|
10841
|
+
if (ids.length === 0) return;
|
|
10842
|
+
|
|
10843
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
10844
|
+
ids.forEach(id => {
|
|
10845
|
+
const p = positions[id];
|
|
10846
|
+
minX = Math.min(minX, p.x);
|
|
10847
|
+
minY = Math.min(minY, p.y);
|
|
10848
|
+
maxX = Math.max(maxX, p.x + WF_NODE_W);
|
|
10849
|
+
maxY = Math.max(maxY, p.y + WF_NODE_H + 50); // extra for time label + backward edge loops
|
|
10850
|
+
});
|
|
10851
|
+
|
|
10852
|
+
const pad = 40;
|
|
10853
|
+
const vbW = maxX - minX + pad * 2;
|
|
10854
|
+
const vbH = maxY - minY + pad * 2;
|
|
10855
|
+
wfState.panX = minX - pad;
|
|
10856
|
+
wfState.panY = minY - pad;
|
|
10857
|
+
// Calculate zoom to fit
|
|
10858
|
+
const rect = svg.parentElement.getBoundingClientRect();
|
|
10859
|
+
const scaleX = rect.width / vbW;
|
|
10860
|
+
const scaleY = rect.height / vbH;
|
|
10861
|
+
wfState.zoom = Math.min(scaleX, scaleY, 1.5);
|
|
10862
|
+
wfApplyViewBox();
|
|
10863
|
+
}
|
|
10864
|
+
|
|
10865
|
+
function wfApplyViewBox() {
|
|
10866
|
+
const svg = document.getElementById('wf-canvas');
|
|
10867
|
+
if (!svg) return;
|
|
10868
|
+
const rect = svg.parentElement.getBoundingClientRect();
|
|
10869
|
+
const w = rect.width / wfState.zoom;
|
|
10870
|
+
const h = rect.height / wfState.zoom;
|
|
10871
|
+
svg.setAttribute('viewBox', `${wfState.panX} ${wfState.panY} ${w} ${h}`);
|
|
10872
|
+
}
|
|
10873
|
+
|
|
10874
|
+
// Pan via mouse drag
|
|
10875
|
+
function wfInitPan() {
|
|
10876
|
+
const svg = document.getElementById('wf-canvas');
|
|
10877
|
+
if (!svg) return;
|
|
10878
|
+
|
|
10879
|
+
svg.addEventListener('mousedown', (e) => {
|
|
10880
|
+
if (e.target === svg || e.target.tagName === 'svg') {
|
|
10881
|
+
wfState.isPanning = true;
|
|
10882
|
+
wfState.panStart = { x: e.clientX, y: e.clientY };
|
|
10883
|
+
svg.style.cursor = 'grabbing';
|
|
10884
|
+
}
|
|
10885
|
+
});
|
|
10886
|
+
|
|
10887
|
+
svg.addEventListener('mousemove', (e) => {
|
|
10888
|
+
if (!wfState.isPanning) return;
|
|
10889
|
+
const dx = (e.clientX - wfState.panStart.x) / wfState.zoom;
|
|
10890
|
+
const dy = (e.clientY - wfState.panStart.y) / wfState.zoom;
|
|
10891
|
+
wfState.panX -= dx;
|
|
10892
|
+
wfState.panY -= dy;
|
|
10893
|
+
wfState.panStart = { x: e.clientX, y: e.clientY };
|
|
10894
|
+
wfApplyViewBox();
|
|
10895
|
+
});
|
|
10896
|
+
|
|
10897
|
+
svg.addEventListener('mouseup', () => {
|
|
10898
|
+
wfState.isPanning = false;
|
|
10899
|
+
svg.style.cursor = '';
|
|
10900
|
+
});
|
|
10901
|
+
|
|
10902
|
+
svg.addEventListener('mouseleave', () => {
|
|
10903
|
+
wfState.isPanning = false;
|
|
10904
|
+
svg.style.cursor = '';
|
|
10905
|
+
});
|
|
10906
|
+
|
|
10907
|
+
// Scroll wheel zoom, centered on cursor position
|
|
10908
|
+
svg.addEventListener('wheel', (e) => {
|
|
10909
|
+
e.preventDefault();
|
|
10910
|
+
const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
|
|
10911
|
+
const oldZoom = wfState.zoom;
|
|
10912
|
+
const newZoom = Math.max(0.2, Math.min(5, oldZoom * factor));
|
|
10913
|
+
// Zoom toward cursor: keep the SVG point under the mouse fixed
|
|
10914
|
+
const rect = svg.getBoundingClientRect();
|
|
10915
|
+
const mx = (e.clientX - rect.left) / oldZoom + wfState.panX;
|
|
10916
|
+
const my = (e.clientY - rect.top) / oldZoom + wfState.panY;
|
|
10917
|
+
wfState.zoom = newZoom;
|
|
10918
|
+
wfState.panX = mx - (e.clientX - rect.left) / newZoom;
|
|
10919
|
+
wfState.panY = my - (e.clientY - rect.top) / newZoom;
|
|
10920
|
+
wfApplyViewBox();
|
|
10921
|
+
}, { passive: false });
|
|
10922
|
+
|
|
10923
|
+
// Click on canvas background to deselect
|
|
10924
|
+
svg.addEventListener('click', (e) => {
|
|
10925
|
+
if (e.target === svg || e.target.tagName === 'svg') {
|
|
10926
|
+
wfDeselectNode();
|
|
10927
|
+
}
|
|
10928
|
+
});
|
|
10929
|
+
}
|
|
10930
|
+
|
|
10931
|
+
// ── Docs shortcut (F1) ──
|
|
10932
|
+
const DOCS_URLS = {
|
|
10933
|
+
embed: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
|
|
10934
|
+
compare: 'https://docs.vaicli.com/docs/commands/embeddings/similarity',
|
|
10935
|
+
search: 'https://docs.vaicli.com/docs/commands/embeddings/rerank',
|
|
10936
|
+
multimodal: 'https://docs.vaicli.com/docs/commands/embeddings/embed',
|
|
10937
|
+
generate: 'https://docs.vaicli.com/docs/commands/project-setup/generate',
|
|
10938
|
+
chat: 'https://docs.vaicli.com/docs/commands/advanced/chat',
|
|
10939
|
+
workflows: 'https://docs.vaicli.com/docs/commands/advanced/workflow-run',
|
|
10940
|
+
benchmark: 'https://docs.vaicli.com/docs/commands/evaluation/benchmark',
|
|
10941
|
+
explore: 'https://docs.vaicli.com/docs/commands/tools-and-learning/explain',
|
|
10942
|
+
about: 'https://docs.vaicli.com/docs/',
|
|
10943
|
+
settings: 'https://docs.vaicli.com/docs/commands/tools-and-learning/config',
|
|
10944
|
+
};
|
|
10945
|
+
document.addEventListener('keydown', (e) => {
|
|
10946
|
+
if (e.key === 'F1') {
|
|
10947
|
+
e.preventDefault();
|
|
10948
|
+
const activeTab = document.querySelector('.tab-btn.active');
|
|
10949
|
+
const tabName = activeTab ? activeTab.dataset.tab : '';
|
|
10950
|
+
const url = DOCS_URLS[tabName] || 'https://docs.vaicli.com';
|
|
10951
|
+
window.open(url, '_blank', 'noopener');
|
|
10952
|
+
}
|
|
10953
|
+
});
|
|
10954
|
+
|
|
10955
|
+
// Keyboard shortcuts (when workflows tab is active)
|
|
10956
|
+
document.addEventListener('keydown', (e) => {
|
|
10957
|
+
const activeTab = document.querySelector('.tab-btn.active');
|
|
10958
|
+
if (!activeTab || activeTab.dataset.tab !== 'workflows') return;
|
|
10959
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
10960
|
+
|
|
10961
|
+
const PAN_STEP = 40;
|
|
10962
|
+
if (e.key === '+' || e.key === '=') { wfZoom(1); e.preventDefault(); }
|
|
10963
|
+
else if (e.key === '-') { wfZoom(-1); e.preventDefault(); }
|
|
10964
|
+
else if (e.key === '0') { wfFitToView(); e.preventDefault(); }
|
|
10965
|
+
else if (e.key === 'Escape') { wfDeselectNode(); e.preventDefault(); }
|
|
10966
|
+
else if (e.key === 'ArrowLeft') { wfState.panX -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
10967
|
+
else if (e.key === 'ArrowRight') { wfState.panX += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
10968
|
+
else if (e.key === 'ArrowUp') { wfState.panY -= PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
10969
|
+
else if (e.key === 'ArrowDown') { wfState.panY += PAN_STEP / wfState.zoom; wfApplyViewBox(); e.preventDefault(); }
|
|
10970
|
+
});
|
|
10971
|
+
|
|
10972
|
+
// ── Init ──
|
|
10973
|
+
function wfInit() {
|
|
10974
|
+
wfLoadLibrary();
|
|
10975
|
+
wfInitPan();
|
|
10976
|
+
}
|
|
10977
|
+
|
|
10978
|
+
// Auto-init: use MutationObserver on the panel to detect when it becomes visible
|
|
10979
|
+
let wfInitialized = false;
|
|
10980
|
+
(function() {
|
|
10981
|
+
const panel = document.getElementById('tab-workflows');
|
|
10982
|
+
if (!panel) return;
|
|
10983
|
+
const observer = new MutationObserver(() => {
|
|
10984
|
+
if (panel.classList.contains('active') && !wfInitialized) {
|
|
10985
|
+
wfInitialized = true;
|
|
10986
|
+
wfInit();
|
|
10987
|
+
}
|
|
10988
|
+
});
|
|
10989
|
+
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
|
|
10990
|
+
// Also init immediately if the tab is already active (shouldn't be, but safety)
|
|
10991
|
+
if (panel.classList.contains('active')) {
|
|
10992
|
+
wfInitialized = true;
|
|
10993
|
+
wfInit();
|
|
10994
|
+
}
|
|
10995
|
+
})();
|
|
10996
|
+
</script>
|
|
10997
|
+
|
|
8256
10998
|
<script>
|
|
8257
10999
|
(function() {
|
|
8258
11000
|
const BUG_API = 'https://vaicli.com/api/bugs';
|