File size: 24,565 Bytes
c5c9261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>VoiceGuard β€” Base64 Testing Console</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link
        href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap"
        rel="stylesheet">
    <style>
        * {
            font-family: 'Outfit', sans-serif;
        }

        code,
        pre,
        textarea.mono {
            font-family: 'JetBrains Mono', monospace;
        }

        .glass {
            background: rgba(30, 41, 59, 0.7);
            backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.08);
        }

        .gradient-text {
            background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 50%, #f472b6 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }

        .result-human {
            background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(16, 185, 129, 0.08));
            border-left: 4px solid #22c55e;
        }

        .result-ai {
            background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(249, 115, 22, 0.08));
            border-left: 4px solid #ef4444;
        }

        .analyzer-card {
            background: rgba(15, 23, 42, 0.6);
            border: 1px solid rgba(255, 255, 255, 0.06);
        }

        .score-ring {
            transition: stroke-dashoffset 1s ease-out;
        }

        textarea::-webkit-scrollbar {
            width: 6px;
        }

        textarea::-webkit-scrollbar-track {
            background: rgba(0, 0, 0, 0.2);
            border-radius: 3px;
        }

        textarea::-webkit-scrollbar-thumb {
            background: rgba(96, 165, 250, 0.3);
            border-radius: 3px;
        }

        .tag {
            display: inline-block;
            padding: 2px 8px;
            border-radius: 6px;
            font-size: 11px;
        }

        .tag-artifact {
            background: rgba(239, 68, 68, 0.15);
            color: #fca5a5;
            border: 1px solid rgba(239, 68, 68, 0.2);
        }

        .tag-clean {
            background: rgba(34, 197, 94, 0.15);
            color: #86efac;
            border: 1px solid rgba(34, 197, 94, 0.2);
        }

        @keyframes fadeIn {
            from {
                opacity: 0;
                transform: translateY(10px);
            }

            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .fade-in {
            animation: fadeIn 0.4s ease-out forwards;
        }

        .fade-in-delay-1 {
            animation-delay: 0.1s;
            opacity: 0;
        }

        .fade-in-delay-2 {
            animation-delay: 0.2s;
            opacity: 0;
        }

        .fade-in-delay-3 {
            animation-delay: 0.3s;
            opacity: 0;
        }

        .fade-in-delay-4 {
            animation-delay: 0.4s;
            opacity: 0;
        }

        @keyframes spin {
            to {
                transform: rotate(360deg);
            }
        }

        .spinner {
            animation: spin 1s linear infinite;
        }
    </style>
</head>

<body class="bg-slate-950 text-white min-h-screen">
    <!-- Subtle grid background -->
    <div class="fixed inset-0 opacity-[0.03]"
        style="background-image: url('data:image/svg+xml,%3Csvg width=40 height=40 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath d=%22M0 0h40v40H0z%22 fill=%22none%22 stroke=%22white%22 stroke-width=%220.5%22/%3E%3C/svg%3E');">
    </div>

    <div class="relative z-10 max-w-5xl mx-auto px-4 py-8">

        <!-- Header -->
        <div class="text-center mb-8">
            <div
                class="inline-flex items-center gap-2 px-4 py-1 rounded-full bg-purple-500/10 border border-purple-500/20 text-purple-300 text-xs font-medium mb-3">
                <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
                    <path
                        d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" />
                </svg>
                Developer Console
            </div>
            <h1 class="text-3xl font-bold gradient-text mb-1">VoiceGuard Base64 Testing</h1>
            <p class="text-slate-500 text-sm">Paste base64-encoded audio β†’ Get full forensic analysis</p>
        </div>

        <!-- Input Panel -->
        <div class="glass rounded-2xl p-6 mb-6">
            <!-- API Key -->
            <div class="mb-4">
                <label class="block text-xs text-slate-400 mb-1.5 font-medium uppercase tracking-wider">πŸ”‘ API
                    Key</label>
                <input id="apiKey" type="password" placeholder="Enter your x-api-key here"
                    class="w-full bg-slate-800/80 border border-slate-700 rounded-lg px-3 py-2.5 text-sm mono focus:outline-none focus:border-blue-500 transition-colors" />
                <p class="text-[10px] text-slate-600 mt-1">Required if API authentication is enabled</p>
            </div>

            <div class="flex flex-col md:flex-row gap-4 mb-4">
                <!-- Language -->
                <div class="flex-1">
                    <label
                        class="block text-xs text-slate-400 mb-1.5 font-medium uppercase tracking-wider">Language</label>
                    <select id="language"
                        class="w-full bg-slate-800/80 border border-slate-700 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-blue-500 transition-colors">
                        <option value="English">English</option>
                        <option value="Tamil">Tamil</option>
                        <option value="Hindi">Hindi</option>
                        <option value="Malayalam">Malayalam</option>
                        <option value="Telugu">Telugu</option>
                    </select>
                </div>
                <!-- Format -->
                <div class="flex-1">
                    <label class="block text-xs text-slate-400 mb-1.5 font-medium uppercase tracking-wider">Audio
                        Format</label>
                    <select id="audioFormat"
                        class="w-full bg-slate-800/80 border border-slate-700 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-blue-500 transition-colors">
                        <option value="mp3">MP3</option>
                        <option value="wav">WAV</option>
                    </select>
                </div>
                <!-- API Endpoint -->
                <div class="flex-1">
                    <label class="block text-xs text-slate-400 mb-1.5 font-medium uppercase tracking-wider">API
                        Endpoint</label>
                    <input id="apiUrl" type="text" value="/detect"
                        class="w-full bg-slate-800/80 border border-slate-700 rounded-lg px-3 py-2.5 text-sm mono focus:outline-none focus:border-blue-500 transition-colors" />
                </div>
                <!-- Detailed -->
                <div class="flex-shrink-0 flex items-end pb-1">
                    <label class="flex items-center gap-2 cursor-pointer">
                        <input id="detailedMode" type="checkbox" checked
                            class="w-4 h-4 rounded bg-slate-800 border-slate-600 text-blue-500 focus:ring-blue-500/30" />
                        <span class="text-sm text-slate-300">Detailed</span>
                    </label>
                </div>
            </div>

            <!-- Base64 Input -->
            <div class="mb-4">
                <div class="flex justify-between items-center mb-1.5">
                    <label class="text-xs text-slate-400 font-medium uppercase tracking-wider">Base64 Audio Data</label>
                    <div class="flex gap-2">
                        <button id="loadFileBtn" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">πŸ“‚
                            Load from file</button>
                        <button id="pasteBtn" class="text-xs text-blue-400 hover:text-blue-300 transition-colors">πŸ“‹
                            Paste</button>
                        <button id="clearBtn" class="text-xs text-slate-500 hover:text-slate-300 transition-colors">βœ•
                            Clear</button>
                    </div>
                </div>
                <textarea id="base64Input"
                    class="mono w-full h-40 bg-slate-900/80 border border-slate-700 rounded-xl px-4 py-3 text-xs text-slate-300 resize-y focus:outline-none focus:border-blue-500 transition-colors placeholder-slate-600"
                    placeholder="Paste your base64-encoded audio string here...&#10;&#10;Tip: You can also click 'Load from file' to convert an audio file to base64 automatically."
                    spellcheck="false"></textarea>
                <input type="file" id="hiddenFileInput" accept="audio/*" class="hidden" />
                <div class="flex justify-between mt-1">
                    <span id="charCount" class="text-xs text-slate-600">0 characters</span>
                    <span id="sizeEstimate" class="text-xs text-slate-600"></span>
                </div>
            </div>

            <!-- Submit -->
            <button id="analyzeBtn"
                class="w-full py-3.5 rounded-xl bg-gradient-to-r from-blue-600 via-indigo-600 to-purple-600 font-semibold text-sm shadow-lg shadow-blue-500/20 disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none transition-all transform hover:scale-[1.01] active:scale-[0.99]"
                disabled>
                <span id="btnText">πŸ” Analyze Audio</span>
            </button>
        </div>

        <!-- Results -->
        <div id="resultsContainer" class="hidden space-y-4">

            <!-- Verdict Card -->
            <div id="verdictCard" class="glass rounded-2xl p-6 fade-in">
                <div class="flex items-center justify-between">
                    <div>
                        <p class="text-xs text-slate-500 uppercase tracking-wider mb-1">Verdict</p>
                        <h2 id="verdictLabel" class="text-3xl font-bold"></h2>
                        <p id="verdictExplanation" class="text-sm text-slate-400 mt-2 max-w-lg leading-relaxed"></p>
                    </div>
                    <div class="flex flex-col items-center">
                        <div class="relative w-24 h-24">
                            <svg class="w-24 h-24 -rotate-90" viewBox="0 0 100 100">
                                <circle cx="50" cy="50" r="42" fill="none" stroke="rgba(255,255,255,0.05)"
                                    stroke-width="8" />
                                <circle id="confidenceRing" cx="50" cy="50" r="42" fill="none" stroke-width="8"
                                    stroke-linecap="round" class="score-ring" stroke-dasharray="264"
                                    stroke-dashoffset="264" />
                            </svg>
                            <div class="absolute inset-0 flex flex-col items-center justify-center">
                                <span id="confidenceValue" class="text-xl font-bold"></span>
                                <span class="text-[10px] text-slate-500 uppercase">Confidence</span>
                            </div>
                        </div>
                    </div>
                </div>
                <!-- Meta bar -->
                <div class="flex flex-wrap gap-3 mt-4 pt-4 border-t border-slate-700/50 text-xs text-slate-500">
                    <span id="metaTime">⏱️ β€”</span>
                    <span id="metaAgree">🀝 β€”</span>
                    <span id="metaDuration">🎡 β€”</span>
                    <span id="metaSNR">πŸ“Ά β€”</span>
                </div>
            </div>

            <!-- Analyzer Breakdown -->
            <div id="analyzersSection" class="hidden">
                <h3 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">Forensic Analyzers</h3>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-3" id="analyzerGrid"></div>
            </div>

            <!-- Artifacts -->
            <div id="artifactsSection" class="hidden glass rounded-2xl p-5 fade-in fade-in-delay-3">
                <h3 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">Detected Artifacts</h3>
                <div id="artifactsList" class="flex flex-wrap gap-2"></div>
            </div>

            <!-- Raw JSON -->
            <details class="glass rounded-2xl overflow-hidden fade-in fade-in-delay-4">
                <summary
                    class="px-5 py-3 text-sm font-semibold text-slate-400 cursor-pointer hover:text-slate-300 transition-colors">
                    πŸ“¦ Raw JSON Response
                </summary>
                <pre id="rawJson" class="px-5 pb-4 text-xs text-slate-400 overflow-x-auto mono leading-relaxed"></pre>
            </details>

        </div>

        <!-- Footer -->
        <div class="text-center mt-8">
            <a href="/" class="text-sm text-slate-500 hover:text-blue-400 transition-colors">← Back to Main UI</a>
        </div>
    </div>

    <script>
        // Elements
        const base64Input = document.getElementById('base64Input');
        const analyzeBtn = document.getElementById('analyzeBtn');
        const btnText = document.getElementById('btnText');
        const charCount = document.getElementById('charCount');
        const sizeEstimate = document.getElementById('sizeEstimate');
        const resultsContainer = document.getElementById('resultsContainer');

        // Character count & enable button
        base64Input.addEventListener('input', () => {
            const len = base64Input.value.trim().length;
            charCount.textContent = `${len.toLocaleString()} characters`;
            if (len > 0) {
                const sizeKB = Math.round(len * 0.75 / 1024);
                sizeEstimate.textContent = `β‰ˆ ${sizeKB > 1024 ? (sizeKB / 1024).toFixed(1) + ' MB' : sizeKB + ' KB'} audio`;
            } else {
                sizeEstimate.textContent = '';
            }
            analyzeBtn.disabled = len === 0;
        });

        // Paste button
        document.getElementById('pasteBtn').onclick = async () => {
            try {
                const text = await navigator.clipboard.readText();
                base64Input.value = text;
                base64Input.dispatchEvent(new Event('input'));
            } catch { alert('Clipboard access denied.'); }
        };

        // Clear button
        document.getElementById('clearBtn').onclick = () => {
            base64Input.value = '';
            base64Input.dispatchEvent(new Event('input'));
            resultsContainer.classList.add('hidden');
        };

        // Load from file β†’ auto-convert to base64
        const hiddenFileInput = document.getElementById('hiddenFileInput');
        document.getElementById('loadFileBtn').onclick = () => hiddenFileInput.click();
        hiddenFileInput.onchange = (e) => {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onloadend = () => {
                let b64 = reader.result;
                if (b64.includes('base64,')) b64 = b64.split('base64,')[1];
                base64Input.value = b64;
                base64Input.dispatchEvent(new Event('input'));

                // Auto-detect format
                const ext = file.name.split('.').pop().toLowerCase();
                if (ext === 'wav') document.getElementById('audioFormat').value = 'wav';
                else document.getElementById('audioFormat').value = 'mp3';
            };
            reader.readAsDataURL(file);
        };

        // Analyze
        analyzeBtn.onclick = async () => {
            const b64 = base64Input.value.trim();
            if (!b64) return;

            analyzeBtn.disabled = true;
            btnText.innerHTML = '<svg class="spinner inline-block w-4 h-4 mr-2" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>Running 5-stage analysis...';
            resultsContainer.classList.add('hidden');

            const detailed = document.getElementById('detailedMode').checked;
            const apiUrl = document.getElementById('apiUrl').value + (detailed ? '?detailed=true' : '');

            try {
                const headers = { 'Content-Type': 'application/json' };
                const apiKeyVal = document.getElementById('apiKey').value.trim();
                if (apiKeyVal) headers['x-api-key'] = apiKeyVal;

                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: headers,
                    body: JSON.stringify({
                        language: document.getElementById('language').value,
                        audioFormat: document.getElementById('audioFormat').value,
                        audioBase64: b64
                    })
                });

                const data = await response.json();
                renderResults(data);
            } catch (err) {
                alert('Request failed: ' + err.message);
            }

            analyzeBtn.disabled = false;
            btnText.textContent = 'πŸ” Analyze Again';
        };

        function renderResults(data) {
            resultsContainer.classList.remove('hidden');

            // Verdict
            const isAI = data.classification === 'AI_GENERATED';
            const verdictCard = document.getElementById('verdictCard');
            verdictCard.querySelector('#verdictLabel').textContent = isAI ? 'πŸ€– AI Generated' : 'πŸ‘€ Human Voice';
            verdictCard.querySelector('#verdictLabel').className = `text-3xl font-bold ${isAI ? 'text-red-400' : 'text-green-400'}`;
            verdictCard.querySelector('#verdictExplanation').textContent = data.explanation || '';

            // Confidence ring
            const pct = Math.round((data.confidenceScore || 0) * 100);
            const ring = document.getElementById('confidenceRing');
            const offset = 264 - (264 * pct / 100);
            ring.style.stroke = isAI ? '#ef4444' : '#22c55e';
            setTimeout(() => { ring.style.strokeDashoffset = offset; }, 50);
            document.getElementById('confidenceValue').textContent = pct + '%';
            document.getElementById('confidenceValue').className = `text-xl font-bold ${isAI ? 'text-red-400' : 'text-green-400'}`;

            // Meta bar
            document.getElementById('metaTime').textContent = `⏱️ ${data.inferenceTimeMs ? data.inferenceTimeMs + 'ms' : 'β€”'}`;
            document.getElementById('metaAgree').textContent = data.analyzersAgree ? '🀝 Analyzers agree' : '⚑ Analyzers disagree';

            if (data.audioProfile) {
                document.getElementById('metaDuration').textContent = `🎡 ${data.audioProfile.duration_sec}s`;
                document.getElementById('metaSNR').textContent = `πŸ“Ά SNR ${data.audioProfile.snr_db}dB`;
            }

            // Forensic analyzers
            const analyzersSection = document.getElementById('analyzersSection');
            const analyzerGrid = document.getElementById('analyzerGrid');
            analyzerGrid.innerHTML = '';

            if (data.forensics) {
                analyzersSection.classList.remove('hidden');
                const analyzerNames = {
                    'neural_model': { icon: '🧠', label: 'Neural Model' },
                    'spectral_analysis': { icon: '🌈', label: 'Spectral Analysis' },
                    'temporal_analysis': { icon: '⏱️', label: 'Temporal Analysis' },
                    'formant_analysis': { icon: 'πŸ”Š', label: 'Formant Analysis' },
                    'artifact_detection': { icon: 'πŸ”', label: 'Artifact Detection' },
                };

                for (const [key, val] of Object.entries(data.forensics)) {
                    const info = analyzerNames[key] || { icon: 'πŸ“Š', label: key };
                    const score = val.score || 0;
                    const scorePct = Math.round(score * 100);
                    const isItemAI = (val.verdict === 'AI_GENERATED');
                    const artifacts = val.artifacts_found || [];

                    const card = document.createElement('div');
                    card.className = 'analyzer-card rounded-xl p-4 fade-in';

                    let artifactHtml = '';
                    if (artifacts.length > 0) {
                        artifactHtml = '<div class="flex flex-wrap gap-1 mt-2">' +
                            artifacts.map(a => `<span class="tag tag-artifact">${a.replace(/_/g, ' ')}</span>`).join('') +
                            '</div>';
                    } else {
                        artifactHtml = '<div class="mt-2"><span class="tag tag-clean">βœ“ No artifacts</span></div>';
                    }

                    // Details
                    let detailsHtml = '';
                    if (val.details && Object.keys(val.details).length > 0) {
                        detailsHtml = '<details class="mt-2"><summary class="text-[10px] text-slate-600 cursor-pointer hover:text-slate-400">Details</summary>' +
                            '<div class="mt-1 text-[10px] text-slate-500 mono space-y-0.5">' +
                            Object.entries(val.details).map(([k, v]) => `<div>${k}: <span class="text-slate-400">${typeof v === 'number' ? v.toFixed(4) : v}</span></div>`).join('') +
                            '</div></details>';
                    }

                    card.innerHTML = `
                        <div class="flex items-center justify-between mb-2">
                            <div class="flex items-center gap-2">
                                <span class="text-lg">${info.icon}</span>
                                <span class="text-sm font-semibold text-slate-200">${info.label}</span>
                            </div>
                            <div class="flex items-center gap-2">
                                <span class="text-xs font-mono ${isItemAI ? 'text-red-400' : 'text-green-400'}">${scorePct}%</span>
                                <span class="text-[10px] px-2 py-0.5 rounded-full ${isItemAI ? 'bg-red-500/15 text-red-400' : 'bg-green-500/15 text-green-400'}">${val.verdict || 'β€”'}</span>
                            </div>
                        </div>
                        <div class="w-full bg-slate-800 rounded-full h-1.5">
                            <div class="h-1.5 rounded-full transition-all duration-700 ${isItemAI ? 'bg-red-500' : 'bg-green-500'}" style="width: ${scorePct}%"></div>
                        </div>
                        ${artifactHtml}
                        ${detailsHtml}
                    `;
                    analyzerGrid.appendChild(card);
                }
            } else {
                analyzersSection.classList.add('hidden');
            }

            // Artifacts summary
            const artifactsSection = document.getElementById('artifactsSection');
            const artifactsList = document.getElementById('artifactsList');
            artifactsList.innerHTML = '';

            if (data.artifactsSummary && data.artifactsSummary.length > 0) {
                artifactsSection.classList.remove('hidden');
                data.artifactsSummary.forEach(a => {
                    const span = document.createElement('span');
                    span.className = 'tag tag-artifact';
                    span.textContent = a.replace(/_/g, ' ');
                    artifactsList.appendChild(span);
                });
            } else {
                artifactsSection.classList.add('hidden');
            }

            // Raw JSON
            document.getElementById('rawJson').textContent = JSON.stringify(data, null, 2);
        }
    </script>
</body>

</html>