voyageai-cli 1.20.6 → 1.22.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +142 -26
  2. package/README.md +130 -2
  3. package/package.json +3 -2
  4. package/src/cli.js +10 -0
  5. package/src/commands/bug.js +249 -0
  6. package/src/commands/eval.js +420 -10
  7. package/src/commands/generate.js +220 -0
  8. package/src/commands/playground.js +93 -0
  9. package/src/commands/purge.js +271 -0
  10. package/src/commands/refresh.js +322 -0
  11. package/src/commands/scaffold.js +217 -0
  12. package/src/lib/codegen.js +339 -0
  13. package/src/lib/explanations.js +155 -0
  14. package/src/lib/scaffold-structure.js +114 -0
  15. package/src/lib/templates/nextjs/README.md.tpl +106 -0
  16. package/src/lib/templates/nextjs/env.example.tpl +8 -0
  17. package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
  18. package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
  19. package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
  20. package/src/lib/templates/nextjs/package.json.tpl +33 -0
  21. package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
  22. package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
  23. package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
  24. package/src/lib/templates/nextjs/theme.js.tpl +84 -0
  25. package/src/lib/templates/python/README.md.tpl +145 -0
  26. package/src/lib/templates/python/app.py.tpl +221 -0
  27. package/src/lib/templates/python/chunker.py.tpl +127 -0
  28. package/src/lib/templates/python/env.example.tpl +12 -0
  29. package/src/lib/templates/python/mongo_client.py.tpl +125 -0
  30. package/src/lib/templates/python/requirements.txt.tpl +10 -0
  31. package/src/lib/templates/python/voyage_client.py.tpl +124 -0
  32. package/src/lib/templates/vanilla/README.md.tpl +156 -0
  33. package/src/lib/templates/vanilla/client.js.tpl +103 -0
  34. package/src/lib/templates/vanilla/connection.js.tpl +126 -0
  35. package/src/lib/templates/vanilla/env.example.tpl +11 -0
  36. package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
  37. package/src/lib/templates/vanilla/package.json.tpl +31 -0
  38. package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
  39. package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
  40. package/src/lib/templates/vanilla/server.js.tpl +81 -0
  41. package/src/lib/zip.js +130 -0
  42. package/src/playground/index.html +708 -3
@@ -660,6 +660,48 @@ select:focus { outline: none; border-color: var(--accent); }
660
660
  font-size: 12px;
661
661
  }
662
662
 
663
+ /* Subtabs - segmented control style */
664
+ .subtabs {
665
+ display: flex;
666
+ background: var(--card);
667
+ border-radius: var(--radius);
668
+ padding: 4px;
669
+ gap: 4px;
670
+ margin-bottom: 20px;
671
+ border: 1px solid var(--accent-dim);
672
+ }
673
+ .subtab {
674
+ flex: 1;
675
+ background: transparent;
676
+ color: var(--text-secondary);
677
+ border: none;
678
+ padding: 10px 20px;
679
+ border-radius: calc(var(--radius) - 2px);
680
+ font-size: 14px;
681
+ font-weight: 500;
682
+ font-family: var(--font);
683
+ cursor: pointer;
684
+ transition: all 0.2s;
685
+ display: flex;
686
+ align-items: center;
687
+ justify-content: center;
688
+ gap: 8px;
689
+ }
690
+ .subtab:hover {
691
+ color: var(--text);
692
+ background: rgba(0, 237, 100, 0.05);
693
+ }
694
+ .subtab.active {
695
+ background: var(--accent);
696
+ color: var(--green-dark);
697
+ font-weight: 600;
698
+ }
699
+ .subtab svg {
700
+ width: 16px;
701
+ height: 16px;
702
+ }
703
+ [data-theme="light"] .subtab.active { color: #FFFFFF; }
704
+
663
705
  .options-row {
664
706
  display: flex;
665
707
  gap: 12px;
@@ -2297,6 +2339,9 @@ select:focus { outline: none; border-color: var(--accent); }
2297
2339
  <symbol id="lg-config" viewBox="0 0 16 16">
2298
2340
  <path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 1.5A.5.5 0 0 1 7 1h2a.5.5 0 0 1 .5.5v1.05a5 5 0 0 1 1.37.57l.74-.74a.5.5 0 0 1 .7 0l1.42 1.42a.5.5 0 0 1 0 .7l-.74.74c.25.43.44.89.57 1.37H14.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-1.05a5 5 0 0 1-.57 1.37l.74.74a.5.5 0 0 1 0 .7l-1.42 1.42a.5.5 0 0 1-.7 0l-.74-.74a5 5 0 0 1-1.37.57V14.5a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5v-1.05a5 5 0 0 1-1.37-.57l-.74.74a.5.5 0 0 1-.7 0L2.27 12.2a.5.5 0 0 1 0-.7l.74-.74a5 5 0 0 1-.57-1.37H1.5A.5.5 0 0 1 1 9V7a.5.5 0 0 1 .5-.5h1.05c.13-.48.32-.94.57-1.37l-.74-.74a.5.5 0 0 1 0-.7L3.8 2.27a.5.5 0 0 1 .7 0l.74.74A5 5 0 0 1 6.6 2.44V1.5zM8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z" fill="currentColor"/>
2299
2341
  </symbol>
2342
+ <symbol id="lg-code" viewBox="0 0 16 16">
2343
+ <path fill="currentColor" d="M5.854 4.146a.5.5 0 0 1 0 .708L2.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0zm4.292 0a.5.5 0 0 0 0 .708L13.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0z"/>
2344
+ </symbol>
2300
2345
  </svg>
2301
2346
 
2302
2347
  <div class="app-shell">
@@ -2315,10 +2360,11 @@ select:focus { outline: none; border-color: var(--accent); }
2315
2360
  <button class="tab-btn" data-tab="compare" role="tab" aria-selected="false" aria-controls="tab-compare" id="tab-btn-compare"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-arrows"/></svg></span><span>Compare</span></button>
2316
2361
  <button class="tab-btn" data-tab="search" role="tab" aria-selected="false" aria-controls="tab-search" id="tab-btn-search"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-search"/></svg></span><span>Search</span></button>
2317
2362
  <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>
2363
+ <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>
2318
2364
  </div>
2319
2365
  <div class="sidebar-nav-divider"></div>
2320
- <div class="sidebar-nav-group" role="tablist" aria-label="Resources">
2321
- <div class="sidebar-nav-label" id="nav-resources-label">Resources</div>
2366
+ <div class="sidebar-nav-group" role="tablist" aria-label="Learn">
2367
+ <div class="sidebar-nav-label" id="nav-learn-label">Learn</div>
2322
2368
  <button class="tab-btn" data-tab="benchmark" role="tab" aria-selected="false" aria-controls="tab-benchmark" id="tab-btn-benchmark"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-gauge"/></svg></span><span>Benchmark</span></button>
2323
2369
  <button class="tab-btn" data-tab="explore" role="tab" aria-selected="false" aria-controls="tab-explore" id="tab-btn-explore"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-bulb"/></svg></span><span>Explore</span></button>
2324
2370
  <button class="tab-btn" data-tab="about" role="tab" aria-selected="false" aria-controls="tab-about" id="tab-btn-about"><span class="tab-btn-icon" aria-hidden="true"><svg><use href="#lg-info"/></svg></span><span>About</span></button>
@@ -3214,6 +3260,179 @@ Reranking models rescore initial search results to improve relevance ordering.</
3214
3260
  </div>
3215
3261
  </div>
3216
3262
 
3263
+ <!-- ========== GENERATE TAB ========== -->
3264
+ <div class="tab-panel" id="tab-generate" role="tabpanel" aria-labelledby="tab-btn-generate" tabindex="0">
3265
+ <div class="page-header">
3266
+ <h2 class="page-header-title">Generate</h2>
3267
+ <p class="page-header-subtitle">Generate code and scaffold projects</p>
3268
+ </div>
3269
+
3270
+ <!-- Mode Toggle -->
3271
+ <div class="subtabs" role="tablist">
3272
+ <button class="subtab active" id="genModeCode" onclick="setGenerateMode('code')" role="tab" aria-selected="true">
3273
+ <svg viewBox="0 0 16 16"><path fill="currentColor" d="M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8l-3.147-3.146z"/></svg>
3274
+ Generate Code
3275
+ </button>
3276
+ <button class="subtab" id="genModeScaffold" onclick="setGenerateMode('scaffold')" role="tab" aria-selected="false">
3277
+ <svg viewBox="0 0 16 16"><path fill="currentColor" d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/><path fill="currentColor" d="M5 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 5 8zm0-2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0 5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-1-5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zM4 8a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm0 2.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/></svg>
3278
+ Scaffold Project
3279
+ </button>
3280
+ </div>
3281
+
3282
+ <!-- Generate Code Panel -->
3283
+ <div id="generateCodePanel" style="display:block;">
3284
+ <div class="card">
3285
+ <div class="card-header"><h3>Generate Code Snippet</h3></div>
3286
+ <div class="card-body">
3287
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
3288
+ <label>
3289
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Target Framework</span>
3290
+ <select id="genTarget" onchange="updateGenComponents()" style="width:100%;">
3291
+ <option value="vanilla">Node.js + Express</option>
3292
+ <option value="nextjs">Next.js + MUI</option>
3293
+ <option value="python">Python + Flask</option>
3294
+ </select>
3295
+ </label>
3296
+ <label>
3297
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Component</span>
3298
+ <select id="genComponent" style="width:100%;">
3299
+ <option value="client">API Client</option>
3300
+ <option value="connection">MongoDB Connection</option>
3301
+ <option value="retrieval">RAG Retrieval</option>
3302
+ <option value="ingest">Document Ingest</option>
3303
+ <option value="search-api">Search API</option>
3304
+ </select>
3305
+ </label>
3306
+ </div>
3307
+
3308
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:16px;">
3309
+ <label>
3310
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Model</span>
3311
+ <select id="genModel" style="width:100%;">
3312
+ <option value="voyage-4-large">voyage-4-large</option>
3313
+ <option value="voyage-4">voyage-4</option>
3314
+ <option value="voyage-4-lite">voyage-4-lite</option>
3315
+ <option value="voyage-3-large">voyage-3-large</option>
3316
+ </select>
3317
+ </label>
3318
+ <label>
3319
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Database</span>
3320
+ <input type="text" id="genDb" value="myapp" style="width:100%;">
3321
+ </label>
3322
+ <label>
3323
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Collection</span>
3324
+ <input type="text" id="genCollection" value="documents" style="width:100%;">
3325
+ </label>
3326
+ </div>
3327
+
3328
+ <label style="display:flex;align-items:center;gap:8px;margin-bottom:16px;">
3329
+ <input type="checkbox" id="genRerank" checked>
3330
+ <span>Include reranking</span>
3331
+ </label>
3332
+
3333
+ <button class="btn btn-primary" onclick="generateCode()" id="genCodeBtn">
3334
+ Generate Code
3335
+ </button>
3336
+ </div>
3337
+ </div>
3338
+
3339
+ <!-- Generated Code Output -->
3340
+ <div class="card" id="genOutputCard" style="margin-top:16px;display:none;">
3341
+ <div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
3342
+ <h3 id="genOutputFilename">Generated Code</h3>
3343
+ <button class="btn btn-outline" onclick="copyGeneratedCode()" style="padding:4px 12px;">Copy</button>
3344
+ </div>
3345
+ <div class="card-body" style="padding:0;">
3346
+ <pre id="genOutputCode" style="margin:0;padding:16px;overflow:auto;max-height:500px;background:var(--surface-darker);border-radius:0 0 8px 8px;font-size:13px;"></pre>
3347
+ </div>
3348
+ </div>
3349
+ </div>
3350
+
3351
+ <!-- Scaffold Project Panel -->
3352
+ <div id="scaffoldPanel" style="display:none;">
3353
+ <div class="card">
3354
+ <div class="card-header"><h3>Scaffold New Project</h3></div>
3355
+ <div class="card-body">
3356
+ <p style="margin-bottom:16px;color:var(--text-dim);">Create a complete, ready-to-run project with all files pre-configured.</p>
3357
+
3358
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;">
3359
+ <label>
3360
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Project Name</span>
3361
+ <input type="text" id="scaffoldName" value="my-rag-app" placeholder="my-rag-app" style="width:100%;">
3362
+ </label>
3363
+ <label>
3364
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Target Framework</span>
3365
+ <select id="scaffoldTarget" onchange="updateScaffoldPreview()" style="width:100%;">
3366
+ <option value="vanilla">Node.js + Express (9 files)</option>
3367
+ <option value="nextjs">Next.js + MUI (13 files)</option>
3368
+ <option value="python">Python + Flask (8 files)</option>
3369
+ </select>
3370
+ </label>
3371
+ </div>
3372
+
3373
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:16px;">
3374
+ <label>
3375
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Model</span>
3376
+ <select id="scaffoldModel" style="width:100%;">
3377
+ <option value="voyage-4-large">voyage-4-large</option>
3378
+ <option value="voyage-4">voyage-4</option>
3379
+ <option value="voyage-4-lite">voyage-4-lite</option>
3380
+ </select>
3381
+ </label>
3382
+ <label>
3383
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Database</span>
3384
+ <input type="text" id="scaffoldDb" value="myapp" style="width:100%;">
3385
+ </label>
3386
+ <label>
3387
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Collection</span>
3388
+ <input type="text" id="scaffoldCollection" value="documents" style="width:100%;">
3389
+ </label>
3390
+ </div>
3391
+
3392
+ <!-- Location Picker (Electron only) -->
3393
+ <div id="scaffoldLocationSection" style="margin-bottom:16px;display:none;">
3394
+ <span style="display:block;margin-bottom:4px;font-weight:500;">Location</span>
3395
+ <div style="display:flex;gap:8px;">
3396
+ <input type="text" id="scaffoldPath" readonly placeholder="Choose a folder..." style="flex:1;background:var(--surface-darker);">
3397
+ <button class="btn btn-outline" onclick="pickScaffoldDirectory()">Browse...</button>
3398
+ </div>
3399
+ </div>
3400
+
3401
+ <!-- Web Mode: Download ZIP -->
3402
+ <div id="scaffoldWebSection" style="margin-bottom:16px;display:none;">
3403
+ <div style="padding:12px;background:var(--surface-darker);border-radius:8px;margin-bottom:12px;">
3404
+ <p style="margin:0;font-size:14px;">📦 <strong>Web Mode</strong> — Download as ZIP or use CLI</p>
3405
+ <p style="margin:8px 0 0;font-size:13px;color:var(--text-dim);">
3406
+ CLI: <code style="padding:4px 8px;background:var(--surface);border-radius:4px;">vai scaffold <span id="scaffoldCliName">my-rag-app</span> --target <span id="scaffoldCliTarget">vanilla</span></code>
3407
+ </p>
3408
+ </div>
3409
+ </div>
3410
+
3411
+ <!-- Buttons: different for Electron vs Web -->
3412
+ <div id="scaffoldElectronButtons" style="display:none;">
3413
+ <button class="btn btn-primary" onclick="createScaffold()" id="scaffoldBtn" disabled>
3414
+ Create Project
3415
+ </button>
3416
+ </div>
3417
+ <div id="scaffoldWebButtons" style="display:none;">
3418
+ <button class="btn btn-primary" onclick="downloadScaffoldZip()" id="scaffoldDownloadBtn">
3419
+ <svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:6px;"><path fill="currentColor" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path fill="currentColor" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
3420
+ Download ZIP
3421
+ </button>
3422
+ </div>
3423
+ </div>
3424
+ </div>
3425
+
3426
+ <!-- Scaffold Progress/Result -->
3427
+ <div class="card" id="scaffoldResultCard" style="margin-top:16px;display:none;">
3428
+ <div class="card-header"><h3>Project Created</h3></div>
3429
+ <div class="card-body">
3430
+ <div id="scaffoldResult"></div>
3431
+ </div>
3432
+ </div>
3433
+ </div>
3434
+ </div>
3435
+
3217
3436
  <!-- ========== EXPLORE TAB ========== -->
3218
3437
  <div class="tab-panel" id="tab-explore" role="tabpanel" aria-labelledby="tab-btn-explore" tabindex="0">
3219
3438
  <div class="page-header">
@@ -4114,6 +4333,261 @@ function createResultItem(rank, result, maxScore, movement) {
4114
4333
  }
4115
4334
 
4116
4335
  // ── Explore ──
4336
+ // ── Generate & Scaffold ──
4337
+
4338
+ let generateMode = 'code';
4339
+
4340
+ function setGenerateMode(mode) {
4341
+ generateMode = mode;
4342
+ const codeBtn = document.getElementById('genModeCode');
4343
+ const scaffoldBtn = document.getElementById('genModeScaffold');
4344
+ codeBtn.classList.toggle('active', mode === 'code');
4345
+ codeBtn.setAttribute('aria-selected', mode === 'code');
4346
+ scaffoldBtn.classList.toggle('active', mode === 'scaffold');
4347
+ scaffoldBtn.setAttribute('aria-selected', mode === 'scaffold');
4348
+ document.getElementById('generateCodePanel').style.display = mode === 'code' ? 'block' : 'none';
4349
+ document.getElementById('scaffoldPanel').style.display = mode === 'scaffold' ? 'block' : 'none';
4350
+ }
4351
+
4352
+ function updateGenComponents() {
4353
+ const target = document.getElementById('genTarget').value;
4354
+ const select = document.getElementById('genComponent');
4355
+ const components = {
4356
+ vanilla: ['client', 'connection', 'retrieval', 'ingest', 'search-api'],
4357
+ nextjs: ['client', 'connection', 'retrieval', 'ingest', 'search-page'],
4358
+ python: ['client', 'connection', 'retrieval', 'ingest'],
4359
+ };
4360
+ const labels = {
4361
+ client: 'API Client',
4362
+ connection: 'MongoDB Connection',
4363
+ retrieval: 'RAG Retrieval',
4364
+ ingest: 'Document Ingest',
4365
+ 'search-api': 'Search API',
4366
+ 'search-page': 'Search Page (MUI)',
4367
+ };
4368
+ select.innerHTML = (components[target] || []).map(c =>
4369
+ `<option value="${c}">${labels[c] || c}</option>`
4370
+ ).join('');
4371
+ }
4372
+
4373
+ async function generateCode() {
4374
+ const btn = document.getElementById('genCodeBtn');
4375
+ const outputCard = document.getElementById('genOutputCard');
4376
+
4377
+ btn.disabled = true;
4378
+ btn.textContent = 'Generating...';
4379
+
4380
+ try {
4381
+ const config = {
4382
+ model: document.getElementById('genModel').value,
4383
+ db: document.getElementById('genDb').value,
4384
+ collection: document.getElementById('genCollection').value,
4385
+ rerank: document.getElementById('genRerank').checked,
4386
+ };
4387
+
4388
+ const target = document.getElementById('genTarget').value;
4389
+ const component = document.getElementById('genComponent').value;
4390
+
4391
+ // Check if running in Electron with vai.generate
4392
+ if (window.vai && window.vai.generate) {
4393
+ const result = await window.vai.generate.code({ target, component, config });
4394
+ if (result.error) throw new Error(result.error);
4395
+
4396
+ document.getElementById('genOutputFilename').textContent = result.filename;
4397
+ document.getElementById('genOutputCode').textContent = result.code;
4398
+ outputCard.style.display = 'block';
4399
+ } else {
4400
+ // Web fallback - call the API
4401
+ const res = await fetch('/api/generate', {
4402
+ method: 'POST',
4403
+ headers: { 'Content-Type': 'application/json' },
4404
+ body: JSON.stringify({ target, component, config }),
4405
+ });
4406
+ const result = await res.json();
4407
+ if (result.error) throw new Error(result.error);
4408
+
4409
+ document.getElementById('genOutputFilename').textContent = result.filename || `${component}.js`;
4410
+ document.getElementById('genOutputCode').textContent = result.code;
4411
+ outputCard.style.display = 'block';
4412
+ }
4413
+ } catch (err) {
4414
+ alert('Error generating code: ' + err.message);
4415
+ } finally {
4416
+ btn.disabled = false;
4417
+ btn.textContent = 'Generate Code';
4418
+ }
4419
+ }
4420
+
4421
+ function copyGeneratedCode() {
4422
+ const code = document.getElementById('genOutputCode').textContent;
4423
+ navigator.clipboard.writeText(code);
4424
+ // Brief feedback
4425
+ const btn = event.target;
4426
+ btn.textContent = 'Copied!';
4427
+ setTimeout(() => { btn.textContent = 'Copy'; }, 1500);
4428
+ }
4429
+
4430
+ // Scaffold functions
4431
+ function initScaffold() {
4432
+ const isElectron = window.vai && window.vai.scaffold;
4433
+
4434
+ // Show/hide mode-specific sections
4435
+ document.getElementById('scaffoldLocationSection').style.display = isElectron ? 'block' : 'none';
4436
+ document.getElementById('scaffoldWebSection').style.display = isElectron ? 'none' : 'block';
4437
+ document.getElementById('scaffoldElectronButtons').style.display = isElectron ? 'block' : 'none';
4438
+ document.getElementById('scaffoldWebButtons').style.display = isElectron ? 'none' : 'block';
4439
+
4440
+ // Update CLI command preview on input
4441
+ document.getElementById('scaffoldName').addEventListener('input', updateScaffoldPreview);
4442
+ document.getElementById('scaffoldTarget').addEventListener('change', updateScaffoldPreview);
4443
+ }
4444
+
4445
+ function updateScaffoldPreview() {
4446
+ const name = document.getElementById('scaffoldName').value || 'my-rag-app';
4447
+ const target = document.getElementById('scaffoldTarget').value;
4448
+ document.getElementById('scaffoldCliName').textContent = name;
4449
+ document.getElementById('scaffoldCliTarget').textContent = target;
4450
+ }
4451
+
4452
+ async function downloadScaffoldZip() {
4453
+ const btn = document.getElementById('scaffoldDownloadBtn');
4454
+ const projectName = document.getElementById('scaffoldName').value || 'my-rag-app';
4455
+ const target = document.getElementById('scaffoldTarget').value;
4456
+ const config = {
4457
+ model: document.getElementById('scaffoldModel').value,
4458
+ db: document.getElementById('scaffoldDb').value,
4459
+ collection: document.getElementById('scaffoldCollection').value,
4460
+ };
4461
+
4462
+ btn.disabled = true;
4463
+ btn.innerHTML = '<span style="margin-right:6px;">⏳</span> Generating...';
4464
+
4465
+ try {
4466
+ const res = await fetch('/api/scaffold', {
4467
+ method: 'POST',
4468
+ headers: { 'Content-Type': 'application/json' },
4469
+ body: JSON.stringify({ projectName, target, config }),
4470
+ });
4471
+
4472
+ if (!res.ok) {
4473
+ const err = await res.json();
4474
+ throw new Error(err.error || 'Failed to generate scaffold');
4475
+ }
4476
+
4477
+ // Download the ZIP
4478
+ const blob = await res.blob();
4479
+ const url = URL.createObjectURL(blob);
4480
+ const a = document.createElement('a');
4481
+ a.href = url;
4482
+ a.download = `${projectName}.zip`;
4483
+ document.body.appendChild(a);
4484
+ a.click();
4485
+ document.body.removeChild(a);
4486
+ URL.revokeObjectURL(url);
4487
+
4488
+ // Show success
4489
+ btn.innerHTML = '<span style="margin-right:6px;">✓</span> Downloaded!';
4490
+ setTimeout(() => {
4491
+ btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:6px;"><path fill="currentColor" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path fill="currentColor" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg> Download ZIP';
4492
+ btn.disabled = false;
4493
+ }, 2000);
4494
+ } catch (err) {
4495
+ alert('Error: ' + err.message);
4496
+ btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right:6px;"><path fill="currentColor" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path fill="currentColor" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg> Download ZIP';
4497
+ btn.disabled = false;
4498
+ }
4499
+ }
4500
+
4501
+ async function pickScaffoldDirectory() {
4502
+ if (!window.vai || !window.vai.scaffold) return;
4503
+
4504
+ const result = await window.vai.scaffold.pickDirectory();
4505
+ if (!result.canceled) {
4506
+ document.getElementById('scaffoldPath').value = result.path;
4507
+ document.getElementById('scaffoldBtn').disabled = false;
4508
+ }
4509
+ }
4510
+
4511
+ async function createScaffold() {
4512
+ if (!window.vai || !window.vai.scaffold) return;
4513
+
4514
+ const btn = document.getElementById('scaffoldBtn');
4515
+ const resultCard = document.getElementById('scaffoldResultCard');
4516
+ const resultDiv = document.getElementById('scaffoldResult');
4517
+
4518
+ const dirPath = document.getElementById('scaffoldPath').value;
4519
+ const projectName = document.getElementById('scaffoldName').value || 'my-rag-app';
4520
+ const target = document.getElementById('scaffoldTarget').value;
4521
+
4522
+ if (!dirPath) {
4523
+ alert('Please choose a location first.');
4524
+ return;
4525
+ }
4526
+
4527
+ btn.disabled = true;
4528
+ btn.textContent = 'Creating...';
4529
+ resultCard.style.display = 'none';
4530
+
4531
+ try {
4532
+ // Check if directory exists
4533
+ const check = await window.vai.scaffold.checkDirectory({ dirPath, projectName });
4534
+ if (check.exists && check.fileCount > 0) {
4535
+ const confirm = await window.vai.scaffold.confirmOverwrite({ projectName });
4536
+ if (!confirm.confirmed) {
4537
+ btn.disabled = false;
4538
+ btn.textContent = 'Create Project';
4539
+ return;
4540
+ }
4541
+ }
4542
+
4543
+ // Listen for progress
4544
+ const cleanup = window.vai.scaffold.onProgress((data) => {
4545
+ resultDiv.innerHTML = `<p>Creating: ${data.file} (${data.current}/${data.total})</p>`;
4546
+ resultCard.style.display = 'block';
4547
+ });
4548
+
4549
+ // Create the project
4550
+ const config = {
4551
+ model: document.getElementById('scaffoldModel').value,
4552
+ db: document.getElementById('scaffoldDb').value,
4553
+ collection: document.getElementById('scaffoldCollection').value,
4554
+ };
4555
+
4556
+ const result = await window.vai.scaffold.create({ dirPath, projectName, target, config });
4557
+ cleanup();
4558
+
4559
+ if (result.error) throw new Error(result.error);
4560
+
4561
+ // Show success
4562
+ resultDiv.innerHTML = `
4563
+ <p style="color:var(--primary);">✓ Created ${result.files.length} files in <code>${projectName}/</code></p>
4564
+ <div style="margin:12px 0;padding:12px;background:var(--surface-darker);border-radius:8px;">
4565
+ <p style="margin:0 0 8px;font-weight:500;">Next steps:</p>
4566
+ <code style="display:block;margin-bottom:4px;">cd ${projectName}</code>
4567
+ <code style="display:block;margin-bottom:4px;">cp .env.example .env</code>
4568
+ <code style="display:block;margin-bottom:4px;">${result.postInstall}</code>
4569
+ <code style="display:block;">${result.startCommand}</code>
4570
+ </div>
4571
+ <button class="btn btn-outline" onclick="window.vai.scaffold.openDirectory({ dirPath: '${result.projectDir.replace(/'/g, "\\'")}' })">
4572
+ Open in Finder
4573
+ </button>
4574
+ `;
4575
+ resultCard.style.display = 'block';
4576
+
4577
+ } catch (err) {
4578
+ resultDiv.innerHTML = `<p style="color:var(--error);">Error: ${err.message}</p>`;
4579
+ resultCard.style.display = 'block';
4580
+ } finally {
4581
+ btn.disabled = false;
4582
+ btn.textContent = 'Create Project';
4583
+ }
4584
+ }
4585
+
4586
+ // Initialize scaffold on load
4587
+ document.addEventListener('DOMContentLoaded', () => {
4588
+ setTimeout(initScaffold, 100);
4589
+ });
4590
+
4117
4591
  // ── Explore: icons and tab mappings per concept ──
4118
4592
  const CONCEPT_META = {
4119
4593
  embeddings: { icon: '🧮', tab: 'embed' },
@@ -4139,6 +4613,10 @@ const CONCEPT_META = {
4139
4613
  'modality-gap': { icon: '🕳️', tab: 'multimodal' },
4140
4614
  'multimodal-rag': { icon: '📄', tab: 'multimodal' },
4141
4615
  'provider-comparison': { icon: '⚖️', tab: 'explore' },
4616
+ // Code generation & scaffolding
4617
+ 'code-generation': { icon: '💻', tab: 'explore' },
4618
+ scaffolding: { icon: '🏗️', tab: 'explore' },
4619
+ 'eval-comparison': { icon: '📊', tab: 'benchmark' },
4142
4620
  };
4143
4621
 
4144
4622
  let exploreConcepts = {};
@@ -5598,7 +6076,7 @@ function initOnboarding() {
5598
6076
  target: null, // welcome card, no spotlight
5599
6077
  icon: '🚀',
5600
6078
  title: 'Welcome to Vai',
5601
- body: 'Explore embeddings, compare text similarity, and search with vector models — all from one playground.',
6079
+ body: 'Explore embeddings, build RAG pipelines, generate production code — all from one playground.',
5602
6080
  },
5603
6081
  {
5604
6082
  target: '[data-tab="settings"]',
@@ -5629,6 +6107,35 @@ function initOnboarding() {
5629
6107
  body: 'Run <strong>vector search</strong> against a set of documents and optionally <strong>rerank</strong> results for higher precision. Great for building RAG pipelines.',
5630
6108
  arrow: 'left',
5631
6109
  },
6110
+ {
6111
+ target: '[data-tab="multimodal"]',
6112
+ icon: '🖼️',
6113
+ title: 'Multimodal',
6114
+ body: 'Embed <strong>images and text</strong> in the same vector space. Search images with text queries, or find similar images across your collection.',
6115
+ arrow: 'left',
6116
+ },
6117
+ {
6118
+ target: '[data-tab="generate"]',
6119
+ icon: '💻',
6120
+ title: 'Generate & Scaffold',
6121
+ body: '<strong>Generate code snippets</strong> for retrieval, ingestion, and API routes — or <strong>scaffold entire projects</strong> with Next.js, Express, or Flask.' +
6122
+ (isElectron ? ' Create projects directly on disk.' : ' Download as ZIP.'),
6123
+ arrow: 'left',
6124
+ },
6125
+ {
6126
+ target: '[data-tab="benchmark"]',
6127
+ icon: '⏱️',
6128
+ title: 'Benchmark',
6129
+ body: 'Compare model <strong>latency, throughput, and cost</strong>. Test embedding speeds, reranking quality, and quantization trade-offs.',
6130
+ arrow: 'left',
6131
+ },
6132
+ {
6133
+ target: '[data-tab="explore"]',
6134
+ icon: '💡',
6135
+ title: 'Explore & Learn',
6136
+ body: 'Dive into <strong>25 interactive explainers</strong> covering embeddings, vector search, RAG, reranking, and more. Each topic links to live demos.',
6137
+ arrow: 'left',
6138
+ },
5632
6139
  ];
5633
6140
 
5634
6141
  let currentStep = 0;
@@ -6519,6 +7026,15 @@ function startVSI(canvas, onExit) {
6519
7026
  loop();
6520
7027
  }
6521
7028
 
7029
+ // ── Expose functions to global scope for onclick handlers ──
7030
+ window.setGenerateMode = setGenerateMode;
7031
+ window.copyGeneratedCode = copyGeneratedCode;
7032
+ window.generateCode = generateCode;
7033
+ window.updateGenComponents = updateGenComponents;
7034
+ window.createScaffold = createScaffold;
7035
+ window.pickScaffoldDirectory = pickScaffoldDirectory;
7036
+ window.downloadScaffoldZip = downloadScaffoldZip;
7037
+
6522
7038
  // ── Start ──
6523
7039
  init();
6524
7040
  })();
@@ -6607,5 +7123,194 @@ init();
6607
7123
  <canvas id="vsiCanvas" width="600" height="500" style="border:1px solid #3D4F58;border-radius:8px;image-rendering:pixelated;"></canvas>
6608
7124
  <div style="color:#889397;font-size:11px;font-family:monospace;text-align:center;">← → move &nbsp;|&nbsp; SPACE shoot &nbsp;|&nbsp; ESC exit</div>
6609
7125
  </div>
7126
+
7127
+ <!-- 🐛 Bug Reporter -->
7128
+ <style>
7129
+ .bug-floating-button{position:fixed;bottom:20px;right:20px;width:48px;height:48px;border-radius:50%;background:linear-gradient(135deg,#ff6b6b,#ee5a5a);border:none;cursor:pointer;font-size:24px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(238,90,90,0.4);transition:all .2s;z-index:9998}
7130
+ .bug-floating-button:hover{transform:scale(1.1);box-shadow:0 6px 16px rgba(238,90,90,0.5)}
7131
+ .bug-reporter-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999}
7132
+ .bug-reporter-modal{background:var(--bg-surface);border-radius:12px;width:90%;max-width:500px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
7133
+ .bug-reporter-header{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border)}
7134
+ .bug-reporter-header h2{flex:1;margin:0;font-size:18px;font-weight:600;color:var(--accent-text)}
7135
+ .bug-reporter-header .close-btn{background:none;border:none;color:var(--text-muted);font-size:24px;cursor:pointer;padding:4px 8px;border-radius:4px}
7136
+ .bug-reporter-header .close-btn:hover{background:rgba(255,255,255,0.1);color:var(--text)}
7137
+ .bug-reporter-form{padding:20px}
7138
+ .bug-reporter-form .form-group{margin-bottom:16px}
7139
+ .bug-reporter-form label{display:block;font-size:13px;font-weight:500;color:var(--text-muted);margin-bottom:6px}
7140
+ .bug-reporter-form input,.bug-reporter-form textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-input);color:var(--text);font-size:14px;font-family:inherit}
7141
+ .bug-reporter-form input:focus,.bug-reporter-form textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
7142
+ .bug-reporter-form textarea{resize:vertical;min-height:80px}
7143
+ .bug-env-info{padding:12px;background:var(--accent-glow);border-radius:8px;font-size:12px;margin-bottom:16px;color:var(--accent)}
7144
+ .bug-env-info code{color:var(--text-muted);font-size:11px}
7145
+ .bug-error{padding:10px 12px;background:rgba(255,107,107,0.15);border:1px solid rgba(255,107,107,0.3);border-radius:8px;color:var(--error);font-size:13px;margin-bottom:16px}
7146
+ .bug-actions{display:flex;gap:12px}
7147
+ .bug-actions button{flex:1;padding:12px 16px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s;border:none}
7148
+ .bug-actions .primary{background:linear-gradient(135deg,var(--accent),#00c853);color:#000}
7149
+ .bug-actions .primary:hover{box-shadow:0 4px 12px rgba(0,237,100,0.4)}
7150
+ .bug-actions .primary:disabled{opacity:0.6;cursor:not-allowed}
7151
+ .bug-actions .secondary{background:rgba(255,255,255,0.1);color:var(--text);border:1px solid var(--border)}
7152
+ .bug-actions .secondary:hover{background:rgba(255,255,255,0.15)}
7153
+ .bug-success{padding:40px 20px;text-align:center}
7154
+ .bug-success .icon{width:60px;height:60px;border-radius:50%;background:linear-gradient(135deg,var(--accent),#00c853);color:#000;font-size:32px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px}
7155
+ .bug-success h3{margin:0 0 12px;color:var(--accent-text);font-size:20px}
7156
+ .bug-success p{margin:8px 0;color:var(--text-muted)}
7157
+ .bug-success code{background:rgba(255,255,255,0.1);padding:4px 8px;border-radius:4px;font-size:12px;color:var(--accent)}
7158
+ </style>
7159
+
7160
+ <button class="bug-floating-button" id="bugButton" title="Report a Bug">🐛</button>
7161
+
7162
+ <div class="bug-reporter-overlay" id="bugOverlay" style="display:none">
7163
+ <div class="bug-reporter-modal">
7164
+ <div class="bug-reporter-header">
7165
+ <span style="font-size:28px">🐛</span>
7166
+ <h2>Report a Bug</h2>
7167
+ <button class="close-btn" id="bugClose">×</button>
7168
+ </div>
7169
+ <div class="bug-reporter-form" id="bugForm">
7170
+ <div class="form-group">
7171
+ <label>Title *</label>
7172
+ <input type="text" id="bugTitle" placeholder="Brief description of the bug" maxlength="200">
7173
+ </div>
7174
+ <div class="form-group">
7175
+ <label>Description *</label>
7176
+ <textarea id="bugDescription" placeholder="What happened? What did you expect?" rows="4" maxlength="5000"></textarea>
7177
+ </div>
7178
+ <div class="form-group">
7179
+ <label>Steps to Reproduce</label>
7180
+ <textarea id="bugSteps" placeholder="1. Go to...&#10;2. Click on...&#10;3. See error" rows="3" maxlength="2000"></textarea>
7181
+ </div>
7182
+ <div class="form-group">
7183
+ <label>Email (optional, for follow-up)</label>
7184
+ <input type="email" id="bugEmail" placeholder="your@email.com">
7185
+ </div>
7186
+ <div class="bug-env-info">
7187
+ <span>📋 Environment will be included:</span><br>
7188
+ <code id="bugEnvInfo">Loading...</code>
7189
+ </div>
7190
+ <div class="bug-error" id="bugError" style="display:none"></div>
7191
+ <div class="bug-actions">
7192
+ <button class="primary" id="bugSubmit">Submit Bug Report</button>
7193
+ <button class="secondary" id="bugGithub">Open GitHub Issue</button>
7194
+ </div>
7195
+ </div>
7196
+ <div class="bug-success" id="bugSuccess" style="display:none">
7197
+ <div class="icon">✓</div>
7198
+ <h3>Bug Reported!</h3>
7199
+ <p>Bug ID: <code id="bugResultId"></code></p>
7200
+ <p>Thank you for helping improve Vai!</p>
7201
+ <div class="bug-actions" style="justify-content:center;margin-top:24px">
7202
+ <button class="primary" id="bugSuccessClose">Close</button>
7203
+ </div>
7204
+ </div>
7205
+ </div>
7206
+ </div>
7207
+
7208
+ <script>
7209
+ (function() {
7210
+ const BUG_API = 'https://vai.mlynn.org/api/bugs';
7211
+ const GITHUB_URL = 'https://github.com/mrlynn/voyageai-cli/issues/new';
7212
+
7213
+ function getEnv() {
7214
+ const env = { platform: navigator.platform, source: 'desktop-app' };
7215
+ if (window.electronAPI) {
7216
+ env.appVersion = window.electronAPI.appVersion || 'unknown';
7217
+ env.cliVersion = window.electronAPI.cliVersion || 'unknown';
7218
+ env.electronVersion = window.electronAPI.electronVersion || 'unknown';
7219
+ }
7220
+ env.currentScreen = document.querySelector('.tab-btn.active')?.textContent || 'unknown';
7221
+ return env;
7222
+ }
7223
+
7224
+ function showBugReporter() {
7225
+ const env = getEnv();
7226
+ document.getElementById('bugEnvInfo').textContent =
7227
+ `${env.platform} • App v${env.appVersion || '?'} • ${env.currentScreen}`;
7228
+ document.getElementById('bugOverlay').style.display = 'flex';
7229
+ document.getElementById('bugForm').style.display = 'block';
7230
+ document.getElementById('bugSuccess').style.display = 'none';
7231
+ document.getElementById('bugError').style.display = 'none';
7232
+ document.getElementById('bugTitle').value = '';
7233
+ document.getElementById('bugDescription').value = '';
7234
+ document.getElementById('bugSteps').value = '';
7235
+ document.getElementById('bugEmail').value = '';
7236
+ }
7237
+
7238
+ function hideBugReporter() {
7239
+ document.getElementById('bugOverlay').style.display = 'none';
7240
+ }
7241
+
7242
+ async function submitBug() {
7243
+ const title = document.getElementById('bugTitle').value.trim();
7244
+ const description = document.getElementById('bugDescription').value.trim();
7245
+ const steps = document.getElementById('bugSteps').value.trim();
7246
+ const email = document.getElementById('bugEmail').value.trim();
7247
+
7248
+ if (!title || title.length < 5) {
7249
+ document.getElementById('bugError').textContent = 'Title is required (min 5 characters)';
7250
+ document.getElementById('bugError').style.display = 'block';
7251
+ return;
7252
+ }
7253
+ if (!description || description.length < 10) {
7254
+ document.getElementById('bugError').textContent = 'Description is required (min 10 characters)';
7255
+ document.getElementById('bugError').style.display = 'block';
7256
+ return;
7257
+ }
7258
+
7259
+ document.getElementById('bugSubmit').disabled = true;
7260
+ document.getElementById('bugSubmit').textContent = 'Submitting...';
7261
+ document.getElementById('bugError').style.display = 'none';
7262
+
7263
+ try {
7264
+ const env = getEnv();
7265
+ const res = await fetch(BUG_API, {
7266
+ method: 'POST',
7267
+ headers: { 'Content-Type': 'application/json' },
7268
+ body: JSON.stringify({
7269
+ title, description, stepsToReproduce: steps || null, email: email || null,
7270
+ ...env
7271
+ })
7272
+ });
7273
+
7274
+ if (!res.ok) {
7275
+ const err = await res.json().catch(() => ({}));
7276
+ throw new Error(err.error || `HTTP ${res.status}`);
7277
+ }
7278
+
7279
+ const data = await res.json();
7280
+ document.getElementById('bugResultId').textContent = data.bugId;
7281
+ document.getElementById('bugForm').style.display = 'none';
7282
+ document.getElementById('bugSuccess').style.display = 'block';
7283
+ } catch (err) {
7284
+ document.getElementById('bugError').textContent = err.message;
7285
+ document.getElementById('bugError').style.display = 'block';
7286
+ } finally {
7287
+ document.getElementById('bugSubmit').disabled = false;
7288
+ document.getElementById('bugSubmit').textContent = 'Submit Bug Report';
7289
+ }
7290
+ }
7291
+
7292
+ function openGithub() {
7293
+ const title = document.getElementById('bugTitle').value.trim() || 'Bug Report';
7294
+ const description = document.getElementById('bugDescription').value.trim();
7295
+ const steps = document.getElementById('bugSteps').value.trim();
7296
+ const env = getEnv();
7297
+
7298
+ const body = `## Description\n${description}\n\n## Steps to Reproduce\n${steps || '1. \\n2. \\n3. '}\n\n## Environment\n- **App Version:** ${env.appVersion || 'N/A'}\n- **Platform:** ${env.platform}\n- **Screen:** ${env.currentScreen}`;
7299
+ const url = `${GITHUB_URL}?title=${encodeURIComponent('[Bug] ' + title)}&body=${encodeURIComponent(body)}&labels=bug`;
7300
+ window.open(url, '_blank');
7301
+ }
7302
+
7303
+ // Event listeners
7304
+ document.getElementById('bugButton').addEventListener('click', showBugReporter);
7305
+ document.getElementById('bugClose').addEventListener('click', hideBugReporter);
7306
+ document.getElementById('bugOverlay').addEventListener('click', (e) => {
7307
+ if (e.target.id === 'bugOverlay') hideBugReporter();
7308
+ });
7309
+ document.getElementById('bugSubmit').addEventListener('click', submitBug);
7310
+ document.getElementById('bugGithub').addEventListener('click', openGithub);
7311
+ document.getElementById('bugSuccessClose').addEventListener('click', hideBugReporter);
7312
+ })();
7313
+ </script>
7314
+
6610
7315
  </body>
6611
7316
  </html>