webtools-cli 1.0.0__py3-none-any.whl

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.
webtools/web/script.js ADDED
@@ -0,0 +1,1805 @@
1
+ // Tailwind Config
2
+ tailwind.config = {
3
+ darkMode: 'class',
4
+ theme: {
5
+ extend: {
6
+ fontFamily: {
7
+ sans: ['Outfit', 'sans-serif'],
8
+ mono: ['JetBrains Mono', 'monospace'],
9
+ },
10
+ colors: {
11
+ primary: '#6366f1',
12
+ accent: '#ec4899',
13
+ },
14
+ animation: {
15
+ blob: "blob 7s infinite",
16
+ shine: "shine 1.5s infinite",
17
+ },
18
+ keyframes: {
19
+ blob: {
20
+ "0%": { transform: "translate(0px, 0px) scale(1)" },
21
+ "33%": { transform: "translate(30px, -50px) scale(1.1)" },
22
+ "66%": { transform: "translate(-20px, 20px) scale(0.9)" },
23
+ "100%": { transform: "translate(0px, 0px) scale(1)" },
24
+ },
25
+ shine: {
26
+ "0%": { transform: "translateX(-100%)" },
27
+ "100%": { transform: "translateX(100%)" },
28
+ }
29
+ },
30
+ }
31
+ }
32
+ }
33
+
34
+ // Image Modal State
35
+ let currentImages = [];
36
+ let currentImageIndex = 0;
37
+
38
+ function wsp_openModal(imgSrc) {
39
+ // Get all images from the grid
40
+ const grid = document.getElementById('images-grid');
41
+ const imgs = grid.querySelectorAll('img[data-src]');
42
+ currentImages = Array.from(imgs).map(img => img.dataset.src || img.src).filter(src => src);
43
+
44
+ // Find index of clicked image
45
+ currentImageIndex = currentImages.indexOf(imgSrc);
46
+ if (currentImageIndex === -1) currentImageIndex = 0;
47
+
48
+ showCurrentImage();
49
+
50
+ const modal = document.getElementById('image-modal');
51
+ modal.classList.remove('hidden');
52
+ modal.classList.add('flex');
53
+ document.body.style.overflow = 'hidden';
54
+ }
55
+
56
+ // Alias for compatibility with grid onclick
57
+ function openModal(src) {
58
+ wsp_openModal(src);
59
+ }
60
+
61
+ // Event delegation for image clicks
62
+ document.addEventListener('click', function(e) {
63
+ const imgCard = e.target.closest('[data-img-src]');
64
+ if (imgCard) {
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ wsp_openModal(imgCard.dataset.imgSrc);
68
+ }
69
+ });
70
+
71
+ function wsp_closeModal() {
72
+ const modal = document.getElementById('image-modal');
73
+ modal.classList.add('hidden');
74
+ modal.classList.remove('flex');
75
+ document.body.style.overflow = '';
76
+ }
77
+
78
+ function showCurrentImage() {
79
+ if (currentImages.length === 0) return;
80
+ const imgSrc = currentImages[currentImageIndex];
81
+ document.getElementById('modal-image').src = imgSrc;
82
+ document.getElementById('modal-download').href = imgSrc;
83
+ document.getElementById('modal-counter').textContent = `${currentImageIndex + 1} / ${currentImages.length}`;
84
+ }
85
+
86
+ function nextImage() {
87
+ if (currentImages.length === 0) return;
88
+ currentImageIndex = (currentImageIndex + 1) % currentImages.length;
89
+ showCurrentImage();
90
+ }
91
+
92
+ function prevImage() {
93
+ if (currentImages.length === 0) return;
94
+ currentImageIndex = (currentImageIndex - 1 + currentImages.length) % currentImages.length;
95
+ showCurrentImage();
96
+ }
97
+
98
+ // Keyboard navigation
99
+ document.addEventListener('keydown', (e) => {
100
+ const modal = document.getElementById('image-modal');
101
+ if (modal.classList.contains('hidden')) return;
102
+
103
+ if (e.key === 'Escape') wsp_closeModal();
104
+ if (e.key === 'ArrowRight') nextImage();
105
+ if (e.key === 'ArrowLeft') prevImage();
106
+ });
107
+
108
+ // --- VISUAL GRAPH (D3.js) ---
109
+ function renderCrawlGraph(structure) {
110
+ if (!structure || !structure.children) {
111
+ document.getElementById('graph-container').innerHTML = '<div class="text-slate-500 italic p-10 text-center">No graph data available.</div>';
112
+ return;
113
+ }
114
+
115
+ // Clear previous
116
+ document.getElementById('graph-container').innerHTML = '';
117
+
118
+ // Process Data: Flatten Tree to Links/Nodes
119
+ const nodes = [];
120
+ const links = [];
121
+ const seen = new Set();
122
+
123
+ const queue = [{ node: structure, parent: null }];
124
+
125
+ while(queue.length > 0) {
126
+ const item = queue.shift();
127
+ const n = item.node;
128
+
129
+ // Unique Nodes only
130
+ if (seen.has(n.url)) continue;
131
+ seen.add(n.url);
132
+
133
+ const isRoot = !item.parent;
134
+
135
+ nodes.push({
136
+ id: n.url,
137
+ title: n.title || n.url,
138
+ group: isRoot ? 1 : 2,
139
+ radius: isRoot ? 15 : 6
140
+ });
141
+
142
+ if (item.parent) {
143
+ links.push({
144
+ source: item.parent.url,
145
+ target: n.url,
146
+ value: 1
147
+ });
148
+ }
149
+
150
+ if (n.children) {
151
+ n.children.forEach(c => queue.push({ node: c, parent: n }));
152
+ }
153
+ }
154
+
155
+ if (nodes.length === 0) return;
156
+
157
+ // D3 Setup
158
+ const container = document.getElementById('graph-container');
159
+ const width = container.clientWidth || 800;
160
+ const height = 600;
161
+
162
+ const svg = d3.select("#graph-container").append("svg")
163
+ .attr("width", "100%")
164
+ .attr("height", "100%")
165
+ .attr("viewBox", [0, 0, width, height])
166
+ .style("background", "#0f172a"); // Match bg-slate-900
167
+
168
+ // Simulation
169
+ const simulation = d3.forceSimulation(nodes)
170
+ .force("link", d3.forceLink(links).id(d => d.id).distance(100))
171
+ .force("charge", d3.forceManyBody().strength(-200))
172
+ .force("center", d3.forceCenter(width / 2, height / 2))
173
+ .force("collide", d3.forceCollide().radius(d => d.radius + 10));
174
+
175
+ // Links
176
+ const link = svg.append("g")
177
+ .attr("stroke", "#475569")
178
+ .attr("stroke-opacity", 0.6)
179
+ .selectAll("line")
180
+ .data(links)
181
+ .join("line")
182
+ .attr("stroke-width", 1);
183
+
184
+ // Nodes
185
+ const node = svg.append("g")
186
+ .attr("stroke", "#fff")
187
+ .attr("stroke-width", 1.5)
188
+ .selectAll("circle")
189
+ .data(nodes)
190
+ .join("circle")
191
+ .attr("r", d => d.radius)
192
+ .attr("fill", d => d.group === 1 ? "#3b82f6" : "#475569")
193
+ .attr("class", "cursor-move hover:fill-orange-500 transition-colors")
194
+ .call(drag(simulation));
195
+
196
+ // Tooltips
197
+ node.append("title").text(d => d.title);
198
+
199
+ simulation.on("tick", () => {
200
+ link
201
+ .attr("x1", d => d.source.x)
202
+ .attr("y1", d => d.source.y)
203
+ .attr("x2", d => d.target.x)
204
+ .attr("y2", d => d.target.y);
205
+
206
+ node
207
+ .attr("cx", d => d.x)
208
+ .attr("cy", d => d.y);
209
+ });
210
+
211
+ // Drag Behavior
212
+ function drag(simulation) {
213
+ function dragstarted(event) {
214
+ if (!event.active) simulation.alphaTarget(0.3).restart();
215
+ event.subject.fx = event.subject.x;
216
+ event.subject.fy = event.subject.y;
217
+ }
218
+
219
+ function dragged(event) {
220
+ event.subject.fx = event.x;
221
+ event.subject.fy = event.y;
222
+ }
223
+
224
+ function dragended(event) {
225
+ if (!event.active) simulation.alphaTarget(0);
226
+ event.subject.fx = null;
227
+ event.subject.fy = null;
228
+ }
229
+
230
+ return d3.drag()
231
+ .on("start", dragstarted)
232
+ .on("drag", dragged)
233
+ .on("end", dragended);
234
+ }
235
+ }
236
+
237
+ // Layout / History Toggle
238
+ function toggleHistory() {
239
+ const mainGrid = document.getElementById('main-grid');
240
+ const inputSection = document.getElementById('input-section');
241
+ const historySection = document.getElementById('history-section');
242
+
243
+ const isHistoryVisible = !historySection.classList.contains('hidden');
244
+
245
+ if (isHistoryVisible) {
246
+ // HIDE HISTORY
247
+ historySection.classList.add('opacity-0', 'translate-y-4');
248
+
249
+ setTimeout(() => {
250
+ historySection.classList.add('hidden');
251
+ // Reset to centered input
252
+ mainGrid.classList.remove('lg:grid-cols-2');
253
+ inputSection.classList.add('max-w-3xl', 'mx-auto');
254
+ }, 300);
255
+
256
+ } else {
257
+ // SHOW HISTORY (side-by-side with input)
258
+ historySection.classList.remove('hidden');
259
+
260
+ // Split layout
261
+ inputSection.classList.remove('max-w-3xl', 'mx-auto');
262
+ mainGrid.classList.add('lg:grid-cols-2');
263
+
264
+ // Animate in
265
+ setTimeout(() => {
266
+ historySection.classList.remove('opacity-0', 'translate-y-4');
267
+ }, 50);
268
+ }
269
+ }
270
+
271
+ // Tab Switching
272
+ function switchTab(tab) {
273
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
274
+ document.getElementById(`content-${tab}`).classList.remove('hidden');
275
+ document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('tab-active'));
276
+ document.getElementById(`tab-${tab}`).classList.add('tab-active');
277
+ }
278
+
279
+ // Preview Size
280
+ function setPreviewSize(size) {
281
+ const frame = document.getElementById('preview-frame');
282
+ if (size === 'mobile') frame.style.width = '375px';
283
+ else if (size === 'tablet') frame.style.width = '768px';
284
+ else frame.style.width = '100%';
285
+ }
286
+
287
+ // Toggle Bulk Mode
288
+ function toggleBulkMode() {
289
+ const isBulk = document.getElementById('bulkMode').checked;
290
+ const singleInput = document.getElementById('urlInput').parentElement;
291
+ const bulkInput = document.getElementById('bulk-input-container');
292
+
293
+
294
+ if(isBulk) {
295
+ singleInput.classList.add('hidden');
296
+
297
+ bulkInput.classList.remove('hidden');
298
+ } else {
299
+ singleInput.classList.remove('hidden');
300
+
301
+ bulkInput.classList.add('hidden');
302
+ }
303
+ }
304
+
305
+ // Scrape Website
306
+ async function scrapeWebsite() {
307
+ const isBulk = document.getElementById('bulkMode').checked;
308
+ const inputSection = document.getElementById('input-inner-container');
309
+ const results = document.getElementById('results');
310
+ const error = document.getElementById('error');
311
+ const fetchImages = document.getElementById('fetchImages').checked;
312
+
313
+ // Button Elements
314
+ const scrapeBtn = document.getElementById('scrapeBtn');
315
+ const scrapeIcon = document.getElementById('scrape-icon');
316
+ const scrapeText = document.getElementById('scrape-text');
317
+
318
+ // Timer Variable
319
+ let timerInterval;
320
+ // GLOBAL DATA STORE
321
+ window.currentScrapeData = null;
322
+
323
+ // Helper: Start Loading
324
+ const startLoading = () => {
325
+ // Reset UI
326
+ inputSection.classList.add('loading-beam-border');
327
+ results.classList.add('hidden');
328
+ error.classList.add('hidden');
329
+
330
+ // Button Loading State
331
+ scrapeBtn.disabled = true;
332
+ scrapeBtn.classList.add('opacity-75', 'cursor-wait');
333
+ scrapeIcon.classList.add('hidden');
334
+
335
+ // Timer Logic
336
+ let seconds = 0;
337
+ scrapeText.textContent = `0`;
338
+ scrapeText.classList.remove('hidden', 'md:inline');
339
+
340
+ timerInterval = setInterval(() => {
341
+ seconds++;
342
+ scrapeText.textContent = `${seconds}`;
343
+ }, 1000);
344
+ };
345
+
346
+ try {
347
+ if(isBulk) {
348
+ // --- BULK MODE ---
349
+ const rawUrls = document.getElementById('bulkUrls').value;
350
+ const urls = rawUrls.split(String.fromCharCode(10)).map(u => u.trim()).filter(u => u.length > 0);
351
+
352
+ if(urls.length === 0) throw new Error("Please enter at least one URL");
353
+
354
+ startLoading();
355
+
356
+ const response = await fetch('/api/bulk', {
357
+ method: 'POST',
358
+ headers: { 'Content-Type': 'application/json' },
359
+ body: JSON.stringify({ urls: urls, fetch_images: fetchImages })
360
+ });
361
+
362
+ const data = await response.json();
363
+
364
+ clearInterval(timerInterval); // Stop timer
365
+
366
+ if(data.success) {
367
+ inputSection.classList.remove('loading-beam-border');
368
+
369
+ // Reset Button
370
+ scrapeBtn.disabled = false;
371
+ scrapeBtn.classList.remove('opacity-75', 'cursor-wait');
372
+ scrapeIcon.classList.remove('hidden');
373
+ scrapeText.textContent = 'Scrape';
374
+ scrapeText.classList.add('hidden', 'md:inline'); // Restore classes
375
+ scrapeText.classList.remove('md:inline'); // Wait, original was 'hidden md:inline'.
376
+ // Let's reset purely to original:
377
+ scrapeText.className = 'relative z-10 text-sm tracking-wide hidden md:inline';
378
+
379
+ window.location.href = data.download_url;
380
+ } else {
381
+ // Reset Button (on error)
382
+ scrapeBtn.disabled = false;
383
+ scrapeBtn.classList.remove('opacity-75', 'cursor-wait');
384
+ scrapeIcon.classList.remove('hidden');
385
+ scrapeText.textContent = 'Scrape';
386
+ scrapeText.className = 'relative z-10 text-sm tracking-wide hidden md:inline';
387
+ inputSection.classList.remove('loading-beam-border');
388
+
389
+ throw new Error(data.error || 'Bulk scrape failed');
390
+ }
391
+
392
+ } else {
393
+ // --- SINGLE MODE ---
394
+ let url = document.getElementById('urlInput').value.trim();
395
+ const fetchVideos = document.getElementById('fetchVideos').checked;
396
+
397
+ const fetchFonts = document.getElementById('fetchFonts').checked;
398
+ const useProxy = document.getElementById('useProxy').checked;
399
+
400
+ if (!url) {
401
+ showError('Please enter a URL');
402
+ return;
403
+ }
404
+
405
+ // Simple validation
406
+ if (url.includes(' ') || (!url.includes('.') && !url.includes('localhost'))) {
407
+ showError('Please enter a valid website URL (e.g. example.com)');
408
+ inputSection.classList.remove('loading-beam-border');
409
+
410
+ // Reset Button
411
+ scrapeBtn.disabled = false;
412
+ scrapeBtn.classList.remove('opacity-75', 'cursor-wait');
413
+ scrapeIcon.classList.remove('animate-spin');
414
+ scrapeText.classList.remove('hidden');
415
+ return;
416
+ }
417
+
418
+ if (!/^https?:\/\//i.test(url)) {
419
+ url = 'https://' + url;
420
+ document.getElementById('urlInput').value = url;
421
+ }
422
+
423
+ startLoading();
424
+
425
+ const response = await fetch('/api/scrape', {
426
+ method: 'POST',
427
+ headers: { 'Content-Type': 'application/json' },
428
+ body: JSON.stringify({
429
+ url: url,
430
+ fetch_images: fetchImages,
431
+ fetch_videos: fetchVideos,
432
+ fetch_fonts: fetchFonts,
433
+ crawl_depth: parseInt(document.getElementById('crawlDepth').value),
434
+ use_proxy: useProxy,
435
+ })
436
+ });
437
+
438
+ const data = await response.json();
439
+
440
+ clearInterval(timerInterval); // Stop timer
441
+
442
+ if (data.success) {
443
+ window.currentScrapeData = data;
444
+ inputSection.classList.remove('loading-beam-border');
445
+
446
+ // Reset Button
447
+ scrapeBtn.disabled = false;
448
+ scrapeBtn.classList.remove('opacity-75', 'cursor-wait');
449
+ scrapeIcon.classList.remove('hidden');
450
+ scrapeText.textContent = 'Scrape';
451
+ scrapeText.className = 'relative z-10 text-sm tracking-wide hidden md:inline';
452
+
453
+ // Show Results (centered below input)
454
+ const results = document.getElementById('results');
455
+ results.classList.remove('hidden');
456
+ results.classList.add('active-results');
457
+
458
+ // Conditionally show tabs based on checkboxes
459
+ document.getElementById('tab-images').classList.toggle('hidden', !fetchImages);
460
+ document.getElementById('tab-videos').classList.toggle('hidden', !fetchVideos);
461
+
462
+ // Setup Preview
463
+ const previewFrame = document.getElementById('preview-frame');
464
+ previewFrame.removeAttribute('srcdoc');
465
+ previewFrame.src = '/download/index.html?' + Date.now();
466
+
467
+ // Update stats
468
+ document.getElementById('stat-html').textContent = data.stats.html;
469
+ document.getElementById('stat-css').textContent = data.stats.css;
470
+ document.getElementById('stat-js').textContent = data.stats.js;
471
+ document.getElementById('stat-img').textContent = data.stats.image_count;
472
+ document.getElementById('stat-vid').textContent = data.stats.video_count;
473
+ document.getElementById('size-html').textContent = data.stats.html;
474
+ document.getElementById('size-css').textContent = data.stats.css;
475
+ document.getElementById('size-js').textContent = data.stats.js;
476
+
477
+ // Security Badge (Honeypot Detector)
478
+ if (data.security) {
479
+ const sec = data.security;
480
+ const levelColors = {
481
+ LOW: 'text-green-400 border-green-500/30 bg-green-500/10',
482
+ MEDIUM: 'text-yellow-400 border-yellow-500/30 bg-yellow-500/10',
483
+ HIGH: 'text-red-400 border-red-500/30 bg-red-500/10'
484
+ };
485
+ const badgeClass = levelColors[sec.level] || levelColors.LOW;
486
+
487
+ // Create or update security stats
488
+ let secContainer = document.getElementById('security-stat');
489
+ if (!secContainer) {
490
+ // Inject into stats grid if missing (using JS to append)
491
+ const statsGrid = document.querySelector('#content-seo .grid');
492
+ if (statsGrid) {
493
+ const div = document.createElement('div');
494
+ div.id = 'security-stat';
495
+ div.className = `p-4 rounded-2xl flex flex-col items-center justify-center text-center bg-transparent border shadow-[0_0_15px_rgba(0,0,0,0.2)] ${badgeClass}`;
496
+ div.innerHTML = `
497
+ <div class="mb-2"><svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg></div>
498
+ <h3 class="text-base font-bold mb-0.5">Security Level</h3>
499
+ <p class="text-xs font-mono" id="sec-level-text">${sec.level}</p>
500
+ `;
501
+ statsGrid.insertBefore(div, statsGrid.firstChild);
502
+ }
503
+ } else {
504
+ secContainer.className = `p-4 rounded-2xl flex flex-col items-center justify-center text-center bg-transparent border shadow-[0_0_15px_rgba(0,0,0,0.2)] ${badgeClass}`;
505
+ document.getElementById('sec-level-text').textContent = sec.level;
506
+ }
507
+ }
508
+
509
+ // Images
510
+ const grid = document.getElementById('images-grid');
511
+ grid.innerHTML = (data.images || []).map((img, idx) => `
512
+ <div class="relative group aspect-square bg-slate-800 rounded-xl overflow-hidden border border-slate-700 animate-blur-fade" style="animation-delay: ${idx * 0.05}s">
513
+ <img src="${img}" onclick="openModal('${img}')" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110 cursor-pointer" loading="lazy" onerror="this.style.display='none'">
514
+ <div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3 pointer-events-none">
515
+ <button onclick="openModal('${img}')" class="p-2.5 bg-slate-700 hover:bg-slate-600 text-white rounded-lg transition-all pointer-events-auto" title="View Full Size">
516
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
517
+ </button>
518
+ <a href="${img}" download target="_blank" class="p-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg transition-all pointer-events-auto" title="Download Image">
519
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
520
+ </a>
521
+ </div>
522
+ </div>
523
+ `).join('');
524
+
525
+ // Render Structure
526
+ if (data.site_structure) {
527
+ // Helper to recursively build tree HTML
528
+ const renderTree = (node) => {
529
+ if (!node) return '';
530
+ let html = `<div class="tree-item">`;
531
+ html += `<div class="flex items-center gap-2 group">
532
+ <span class="w-2 h-2 rounded-full bg-cyan-500 hover:bg-cyan-400 transition-colors shadow-[0_0_5px_rgba(6,182,212,0.5)]"></span>
533
+ <a href="${node.url}" target="_blank" class="text-cyan-400 hover:text-cyan-300 hover:underline truncate max-w-[400px] block transition-colors" title="${node.title}\n${node.url}">${node.title}</a>
534
+ </div>`;
535
+ if (node.children && node.children.length > 0) {
536
+ node.children.forEach(child => {
537
+ html += renderTree(child);
538
+ });
539
+ }
540
+ html += `</div>`;
541
+ return html;
542
+ };
543
+
544
+ const treeContainer = document.getElementById('structure-tree');
545
+ if (treeContainer) {
546
+ treeContainer.innerHTML = (data.site_structure.children && data.site_structure.children.length > 0)
547
+ ? renderTree(data.site_structure)
548
+ : '<p class="text-slate-500 italic p-2">No deep structure found. Try increasing Crawl Depth.</p>';
549
+
550
+ // Auto-switch if meaningful data
551
+ if (data.site_structure.children.length > 0) {
552
+ try {
553
+ // Simple tab switch to the combined Intelligence tab (formerly SEO)
554
+ if(typeof switchTab === 'function') switchTab('seo');
555
+ else {
556
+ // Fallback
557
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
558
+ document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('tab-active', 'text-white', 'border-b-2', 'border-indigo-500'));
559
+ document.getElementById('content-seo').classList.remove('hidden');
560
+ document.getElementById('tab-seo').classList.add('tab-active', 'text-white');
561
+ }
562
+ } catch(e) {}
563
+ }
564
+ }
565
+ }
566
+
567
+ // Render Data Table
568
+ if (data.intel) {
569
+ renderDataTable(data);
570
+ }
571
+
572
+ // Videos
573
+ const vGrid = document.getElementById('videos-grid');
574
+ if (data.videos && data.videos.length > 0) {
575
+ vGrid.innerHTML = data.videos.map((vid, idx) => {
576
+ // Clean up filename for display title
577
+ // Format: Title_Hash.ext OR Video_Hash.ext
578
+ let displayTitle = vid.filename;
579
+ try {
580
+ const lastDot = displayTitle.lastIndexOf('.');
581
+ if(lastDot > -1) displayTitle = displayTitle.substring(0, lastDot); // Remove ext
582
+
583
+ // Remove hash (last 9 chars: _12345678) if it looks like one
584
+ if (displayTitle.length > 9 && displayTitle.match(/_[a-f0-9]{8}$/)) {
585
+ displayTitle = displayTitle.substring(0, displayTitle.length - 9);
586
+ }
587
+
588
+ // Replace remaining underscores with spaces
589
+ displayTitle = displayTitle.replace(/_/g, ' ');
590
+
591
+ // If it became empty or just numbers, fall back
592
+ if(!displayTitle.trim()) displayTitle = `Video ${idx + 1}`;
593
+
594
+ } catch(e) {
595
+ displayTitle = `Video ${idx + 1}`;
596
+ }
597
+
598
+ return `
599
+ <div class="video-card rounded-xl p-3 transition-all duration-300">
600
+ <div class="flex items-start gap-3">
601
+ <div class="w-16 h-12 bg-slate-800 rounded-lg flex items-center justify-center flex-shrink-0">
602
+ <svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
603
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
604
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
605
+ </svg>
606
+ </div>
607
+ <div class="flex-1 min-w-0">
608
+ <h4 class="text-white font-semibold text-sm mb-1 truncate" title="${displayTitle}">
609
+ ${displayTitle}
610
+ ${vid.quality && vid.quality !== 'unknown' ?
611
+ `<span class="ml-2 px-1.5 py-0.5 bg-indigo-600 text-[10px] rounded text-white uppercase tracking-wider">${vid.quality}</span>`
612
+ : ''}
613
+ </h4>
614
+ <p class="text-slate-400 text-xs mb-2 truncate" title="${vid.filename}">${vid.filename}</p>
615
+ <div class="flex gap-2">
616
+ <a href="${vid.url}" download="${vid.filename}" class="px-3 py-1.5 bg-red-600 hover:bg-red-500 text-white rounded-lg text-xs font-semibold transition-all flex items-center gap-1">
617
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
618
+ Download
619
+ </a>
620
+ <button onclick="copyVideoLink('${vid.original_url}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-xs font-semibold transition-all">
621
+ Copy Link
622
+ </button>
623
+ </div>
624
+ </div>
625
+ </div>
626
+ ${vid.original_url.includes('youtube') || vid.original_url.includes('vimeo') ?
627
+ `<div class="mt-3 pt-3 border-t border-slate-700">
628
+
629
+ <span class="text-xs text-slate-500">External: ${new URL(vid.original_url).hostname}</span>
630
+ </div>` : ''
631
+ }
632
+ </div>
633
+ `}).join('');
634
+ } else {
635
+ vGrid.innerHTML = '<div class="col-span-full text-center text-slate-500 py-12">No videos found.</div>';
636
+ }
637
+
638
+ // Update SEO
639
+ if(data.seo) {
640
+ const seo = data.seo;
641
+ const circle = document.getElementById('seo-score-circle');
642
+ const circumference = 351;
643
+ const offset = circumference - (seo.score / 100) * circumference;
644
+ circle.style.strokeDashoffset = offset;
645
+ circle.classList.toggle('text-green-500', seo.score >= 80);
646
+ circle.classList.toggle('text-yellow-500', seo.score >= 50 && seo.score < 80);
647
+ circle.classList.toggle('text-red-500', seo.score < 50);
648
+ document.getElementById('seo-score-text').textContent = seo.score;
649
+
650
+ document.getElementById('seo-title').textContent = seo.title || 'Missing';
651
+ document.getElementById('seo-title').className = seo.title ? 'text-white font-medium truncate block' : 'text-red-400 font-medium truncate block';
652
+ document.getElementById('seo-desc').textContent = seo.description || 'Missing';
653
+ document.getElementById('seo-desc').className = seo.description ? 'text-white font-medium truncate block' : 'text-red-400 font-medium truncate block';
654
+
655
+ document.getElementById('seo-h1').textContent = seo.headings.h1 > 0 ? `Found (${seo.headings.h1})` : 'Missing';
656
+ document.getElementById('seo-h1').className = seo.headings.h1 > 0 ? 'text-white font-medium' : 'text-red-400 font-medium';
657
+
658
+ document.getElementById('seo-links').textContent = `${seo.links_internal} Internal / ${seo.links_external} External`;
659
+
660
+ const report = document.getElementById('seo-report-list');
661
+ let issues = [];
662
+ if(!seo.title) issues.push({type:'error', msg:'Missing Meta Title tag'});
663
+ if(!seo.description) issues.push({type:'warning', msg:'Missing Meta Description'});
664
+ if(seo.headings.h1 === 0) issues.push({type:'error', msg:'Missing H1 heading (critical for SEO)'});
665
+ if(seo.images_analysis.missing_alt > 0) issues.push({type:'warning', msg:`${seo.images_analysis.missing_alt} images missing ALT text`});
666
+ if(seo.keywords) issues.push({type:'info', msg: `Keywords found: ${seo.keywords.length} chars`});
667
+
668
+ if(seo.broken_links && seo.broken_links.length > 0) {
669
+ issues.push({type:'error', msg: `Found ${seo.broken_links.length} Broken Links!`});
670
+ seo.broken_links.forEach(link => {
671
+ issues.push({type:'error', msg: `Broken Link (${link.status}): ${link.url}`});
672
+ });
673
+ }
674
+
675
+ if(issues.length === 0) issues.push({type:'success', msg:'No critical issues found!'});
676
+
677
+ report.innerHTML = issues.map(i => `
678
+ <div class="flex items-start gap-3 p-3 rounded-lg ${i.type === 'error' ? 'bg-red-500/10 text-red-400' : i.type === 'warning' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-green-500/10 text-green-400'}">
679
+ <span class="mt-0.5 flex-shrink-0">•</span>
680
+ <span class="text-sm break-all">${i.msg}</span>
681
+ </div>
682
+ `).join('');
683
+ }
684
+
685
+ // Update Health
686
+ if (data.broken_links || (data.seo && data.seo.broken_links)) {
687
+ const broken = data.broken_links || data.seo.broken_links || [];
688
+ const totalLinks = (data.seo ? (data.seo.links_internal + data.seo.links_external) : 0);
689
+ const brokenCount = broken.length;
690
+ const okCount = Math.max(0, totalLinks - brokenCount);
691
+
692
+ const okEl = document.getElementById('health-total-ok');
693
+ const brokenEl = document.getElementById('health-total-broken');
694
+ if(okEl) okEl.textContent = okCount;
695
+ if(brokenEl) brokenEl.textContent = brokenCount;
696
+
697
+ const brokenList = document.getElementById('health-broken-list');
698
+ if (brokenList) {
699
+ if (broken.length > 0) {
700
+ brokenList.innerHTML = broken.map(link => `
701
+ <div class="p-3 bg-red-500/10 rounded-lg border border-red-500/20 flex flex-col gap-1">
702
+ <div class="flex justify-between items-start">
703
+ <span class="text-xs font-bold text-red-400 bg-red-500/20 px-2 py-0.5 rounded">${link.status}</span>
704
+ <span class="text-[10px] text-slate-500 uppercase tracking-wider">${link.is_internal ? 'Internal' : 'External'}</span>
705
+ </div>
706
+ <a href="${link.url}" target="_blank" class="text-sm text-slate-300 hover:text-white hover:underline truncate" title="${link.url}">${link.url}</a>
707
+ <p class="text-xs text-slate-500 truncate">Text: "${link.text || 'N/A'}"</p>
708
+ </div>
709
+ `).join('');
710
+ } else {
711
+ brokenList.innerHTML = '<p class="text-slate-500 italic text-sm">No broken links found.</p>';
712
+ }
713
+ }
714
+ }
715
+
716
+ // Update Intel
717
+ if (data.intel) {
718
+ const intel = data.intel;
719
+
720
+ // Contacts
721
+ const contactsContainer = document.getElementById('intel-contacts');
722
+ let contactHtml = '';
723
+ if (intel.emails.length > 0) {
724
+ contactHtml += `<div class="mb-3"><div class="text-xs text-slate-500 mb-1">EMAILS</div>`;
725
+ contactHtml += intel.emails.map(e => `
726
+ <div class="flex items-center gap-2 group">
727
+ <span class="w-8 h-8 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-400">@</span>
728
+ <span class="text-white text-sm select-all">${e}</span>
729
+ </div>`).join('');
730
+ contactHtml += `</div>`;
731
+ }
732
+ if (intel.phones.length > 0) {
733
+ contactHtml += `<div><div class="text-xs text-slate-500 mb-1">PHONES</div>`;
734
+ contactHtml += intel.phones.map(p => `
735
+ <div class="flex items-center gap-2 group">
736
+ <span class="w-8 h-8 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-400">📞</span>
737
+ <span class="text-white text-sm select-all">${p}</span>
738
+ </div>`).join('');
739
+ contactHtml += `</div>`;
740
+ }
741
+ if (!contactHtml) contactHtml = '<p class="text-slate-500 italic text-sm">No contact info found.</p>';
742
+ contactsContainer.innerHTML = contactHtml;
743
+
744
+ // Locations
745
+ const locationContainer = document.getElementById('intel-locations');
746
+ if (intel.locations && intel.locations.length > 0) {
747
+ locationContainer.innerHTML = intel.locations.map(acc => `
748
+ <div class="flex items-start gap-2 group">
749
+ <svg class="w-5 h-5 text-orange-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
750
+ <span class="text-white text-sm select-all">${acc}</span>
751
+ </div>
752
+ `).join('');
753
+ } else {
754
+ locationContainer.innerHTML = '<p class="text-slate-500 italic text-sm">No locations found.</p>';
755
+ }
756
+
757
+ // Socials
758
+ const socialContainer = document.getElementById('intel-socials');
759
+ if (intel.socials.length > 0) {
760
+ socialContainer.innerHTML = intel.socials.map(s => `
761
+ <a href="${s.url}" target="_blank" class="p-3 rounded-xl bg-slate-800/50 hover:bg-slate-700/50 border border-slate-700 hover:border-orange-500/50 transition-all flex items-center gap-3">
762
+ <div class="w-10 h-10 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-400 font-bold text-lg">${s.platform[0]}</div>
763
+ <div class="overflow-hidden">
764
+ <h4 class="text-white text-sm font-medium truncate">${s.platform}</h4>
765
+ <p class="text-slate-500 text-xs truncate">View Profile</p>
766
+ </div>
767
+ </a>
768
+ `).join('');
769
+ } else {
770
+ socialContainer.innerHTML = '<p class="text-slate-500 italic text-sm col-span-full">No social profiles found.</p>';
771
+ }
772
+
773
+ // AI Analysis
774
+ if (intel.ai_analysis) {
775
+ const ai = intel.ai_analysis;
776
+ const scoreEl = document.getElementById('ai-sentiment-score');
777
+ const labelEl = document.getElementById('ai-sentiment-label');
778
+ const barEl = document.getElementById('ai-sentiment-bar');
779
+ const keywordsContainer = document.getElementById('ai-keywords');
780
+ const summaryEl = document.getElementById('ai-summary');
781
+ const readScoreEl = document.getElementById('ai-readability-score');
782
+ const readLabelEl = document.getElementById('ai-readability-label');
783
+ const readBarEl = document.getElementById('ai-readability-bar');
784
+
785
+ // Sentiment
786
+ const polarity = ai.sentiment.polarity; // -1 to 1
787
+ const score = Math.round(((polarity + 1) / 2) * 100); // 0 to 100
788
+
789
+ if(scoreEl) scoreEl.textContent = score;
790
+ if(labelEl) labelEl.textContent = `${ai.sentiment.label} (${ai.sentiment.subjectivity_label})`;
791
+
792
+ // Color & Label Style
793
+ let colorClass = 'text-yellow-400';
794
+ if(score >= 60) colorClass = 'text-green-400';
795
+ else if(score <= 40) colorClass = 'text-red-400';
796
+
797
+ labelEl.className = `mb-1.5 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide bg-slate-800 ${colorClass}`;
798
+
799
+ // Bar Animation (Center Out)
800
+ const percentage = Math.abs(polarity) * 50; // 0 to 50%
801
+ if(barEl) {
802
+ barEl.style.width = `${percentage}%`;
803
+
804
+ if (polarity >= 0) {
805
+ barEl.style.left = '50%';
806
+ barEl.className = 'h-full absolute top-0 transition-all duration-1000 ease-out bg-gradient-to-r from-green-500 to-emerald-400';
807
+ } else {
808
+ barEl.style.left = `${50 - percentage}%`;
809
+ barEl.className = 'h-full absolute top-0 transition-all duration-1000 ease-out bg-gradient-to-r from-red-500 to-rose-400';
810
+ }
811
+ }
812
+
813
+ // Summary
814
+ if(summaryEl) summaryEl.textContent = ai.summary || 'No summary available.';
815
+
816
+ // Readability
817
+ if(ai.readability) {
818
+ const rScore = ai.readability.score;
819
+ if(readScoreEl) readScoreEl.textContent = rScore;
820
+ if(readLabelEl) readLabelEl.textContent = ai.readability.level;
821
+
822
+ // 0-100 scale (approx)
823
+ let rPercent = Math.max(0, Math.min(100, rScore));
824
+ if(readBarEl) {
825
+ readBarEl.style.width = `${rPercent}%`;
826
+ // Green = Easy (High score), Red = Hard (Low score)
827
+ if(rScore > 60) readBarEl.className = 'h-full w-0 transition-all duration-1000 bg-gradient-to-r from-green-500 to-emerald-400';
828
+ else if(rScore > 40) readBarEl.className = 'h-full w-0 transition-all duration-1000 bg-gradient-to-r from-yellow-500 to-orange-400';
829
+ else readBarEl.className = 'h-full w-0 transition-all duration-1000 bg-gradient-to-r from-red-500 to-rose-400';
830
+ }
831
+ }
832
+
833
+ // Keywords
834
+ if (keywordsContainer && ai.keywords && ai.keywords.length > 0) {
835
+ keywordsContainer.innerHTML = ai.keywords.map(k => `
836
+ <span class="px-3 py-1.5 rounded-lg bg-slate-800 border border-slate-700 text-slate-300 text-xs hover:border-purple-500/50 hover:text-purple-300 transition-colors cursor-default">${k}</span>
837
+ `).join('');
838
+ } else if (keywordsContainer) {
839
+ keywordsContainer.innerHTML = '<span class="text-slate-500 italic text-xs">No key topics detected.</span>';
840
+ }
841
+ }
842
+
843
+ // Tech Stack
844
+ const stackContainer = document.getElementById('intel-stack');
845
+ if (intel.tech_stack.length > 0) {
846
+ stackContainer.innerHTML = intel.tech_stack.map(t => `
847
+ <span class="px-3 py-1 bg-slate-800 border border-slate-700 text-slate-300 rounded-full text-xs hover:border-orange-500/50 hover:text-orange-300 transition-colors cursor-default">
848
+ ${t}
849
+ </span>
850
+ `).join('');
851
+ } else {
852
+ stackContainer.innerHTML = '<p class="text-slate-500 italic text-sm">No technologies detected.</p>';
853
+ }
854
+ }
855
+
856
+ // Update Design
857
+ if(data.design) {
858
+ const design = data.design;
859
+ document.getElementById('design-colors').innerHTML = design.colors.map(c => `
860
+ <div onclick="navigator.clipboard.writeText('${c}')" class="cursor-pointer group relative h-20 rounded-xl transition-transform hover:scale-105 shadow-lg flex items-end p-2" style="background-color: ${c}">
861
+ <div class="bg-black/40 backdrop-blur-sm px-2 py-1 rounded text-xs text-white font-mono opacity-0 group-hover:opacity-100 transition-opacity w-full text-center">
862
+ ${c}
863
+ </div>
864
+ </div>
865
+ `).join('');
866
+
867
+ document.getElementById('design-fonts').innerHTML = design.fonts.map(f => `
868
+ <div class="glass-dark p-4 rounded-xl border border-white/5 flex items-center justify-between">
869
+ <span class="text-white font-medium truncate">${f}</span>
870
+ <span class="text-xs text-slate-500 font-mono">Font Family</span>
871
+ </div>
872
+ `).join('');
873
+ }
874
+
875
+ // Update preview
876
+ document.getElementById('preview-frame').src = '/download/index.html?' + Date.now();
877
+
878
+ // Update images
879
+ if (data.images && data.images.length > 0) {
880
+ const grid = document.getElementById('images-grid');
881
+ grid.innerHTML = data.images.map(img => `
882
+ <div class="relative group aspect-square bg-slate-800 rounded-xl overflow-hidden border border-slate-700 cursor-pointer" data-img-src="${img}">
883
+ <img src="${img}" data-src="${img}" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" loading="lazy" onerror="this.style.display='none'">
884
+ <div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
885
+ <button data-img-src="${img}" class="p-2.5 bg-slate-700 hover:bg-slate-600 text-white rounded-lg transition-all" title="View Full Size">
886
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
887
+ </button>
888
+ <a href="${img}" download class="p-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg transition-all" title="Download Image" onclick="event.stopPropagation()">
889
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
890
+ </a>
891
+ </div>
892
+ </div>
893
+ `).join('');
894
+ }
895
+
896
+ // Update videos
897
+ if (data.videos && data.videos.length > 0) {
898
+ const vGrid = document.getElementById('videos-grid');
899
+ vGrid.innerHTML = data.videos.map((vid, idx) => `
900
+ <div class="video-card rounded-xl overflow-hidden bg-slate-800/50 border border-slate-700 transition-all duration-300 hover:border-indigo-500/50">
901
+ <div class="relative cursor-pointer group" onclick="openVideoModal('${vid.url}')">
902
+ <div class="w-full aspect-video bg-gradient-to-br from-slate-700 to-slate-800 flex items-center justify-center">
903
+ <svg class="w-16 h-16 text-white/80 group-hover:text-indigo-400 group-hover:scale-110 transition-all duration-300" fill="currentColor" viewBox="0 0 24 24">
904
+ <path d="M8 5v14l11-7z"/>
905
+ </svg>
906
+ </div>
907
+ <div class="absolute inset-0 bg-black/30 group-hover:bg-black/10 transition-colors"></div>
908
+ </div>
909
+ <div class="p-4">
910
+ <h4 class="text-white font-semibold text-sm mb-1 truncate">Video ${idx + 1}</h4>
911
+ <p class="text-slate-400 text-xs mb-3 truncate">${vid.filename}</p>
912
+ <div class="flex gap-2">
913
+ <button onclick="openVideoModal('${vid.url}')" class="flex-1 px-3 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold transition-all flex items-center justify-center gap-1">
914
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
915
+ <span class="hidden sm:inline">Play</span>
916
+ </button>
917
+ <a href="${vid.url}" download class="px-3 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-xs font-semibold transition-all flex items-center gap-1">
918
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
919
+ <span class="hidden sm:inline">Download</span>
920
+ </a>
921
+ </div>
922
+ </div>
923
+ </div>
924
+ `).join('');
925
+
926
+ // Initialize Players
927
+ setTimeout(() => {
928
+ data.videos.forEach((vid, idx) => {
929
+ const video = document.getElementById(`vid-${idx}`);
930
+ if (vid.is_m3u8) {
931
+ if (Hls.isSupported()) {
932
+ const hls = new Hls();
933
+ hls.loadSource(vid.original_url);
934
+ hls.attachMedia(video);
935
+ } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
936
+ video.src = vid.original_url;
937
+ }
938
+ }
939
+ });
940
+ }, 100);
941
+ }
942
+
943
+ // Add to history
944
+ addToHistory(url);
945
+ }
946
+ }
947
+ } catch (err) {
948
+ // loading.classList.add('hidden'); // 'loading' var not defined in scope, safer to remove or ignore
949
+ showError(err.message || 'Failed to scrape website');
950
+ }
951
+ }
952
+
953
+ function copyVideoLink(url) {
954
+ navigator.clipboard.writeText(url).then(() => {
955
+ alert('Video link copied to clipboard!');
956
+ }).catch(() => {
957
+ showError('Failed to copy link');
958
+ });
959
+ }
960
+
961
+ // History Management
962
+ function addToHistory(url) {
963
+ const historyList = document.getElementById('history-list');
964
+ const btn = document.createElement('button');
965
+ btn.className = 'px-3 py-1.5 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-lg text-xs text-slate-300 transition-all truncate max-w-[200px]';
966
+ try {
967
+ const domain = new URL(url).hostname;
968
+ btn.textContent = domain;
969
+ } catch {
970
+ btn.textContent = url;
971
+ }
972
+ btn.onclick = () => {
973
+ document.getElementById('urlInput').value = url;
974
+ scrapeWebsite();
975
+ };
976
+ historyList.prepend(btn);
977
+
978
+ // Only remove placeholder text, don't auto-show history
979
+ // User must click the History button to see recent scrapes
980
+
981
+ // Remove placeholder
982
+ const placeholder = historyList.querySelector('span.italic');
983
+ if (placeholder) placeholder.remove();
984
+
985
+ // Animate button
986
+ if (window.animate) {
987
+ animate(btn, { opacity: [0, 1], y: [-10, 0] }, { duration: 0.3 });
988
+ }
989
+ }
990
+
991
+
992
+
993
+ // Code Editor Functions
994
+ async function loadEditorContent() {
995
+ const filename = document.getElementById('editor-filename').value;
996
+ try {
997
+ const response = await fetch(`/download/${filename}`);
998
+ const text = await response.text();
999
+ document.getElementById('code-editor').value = text;
1000
+ } catch (e) {
1001
+ document.getElementById('code-editor').value = '// Failed to load file';
1002
+ }
1003
+ }
1004
+
1005
+ function editFile(filename) {
1006
+ document.getElementById('editor-filename').value = filename;
1007
+ switchTab('code');
1008
+ loadEditorContent();
1009
+ }
1010
+
1011
+ function handleTab(e) {
1012
+ if (e.key === 'Tab') {
1013
+ e.preventDefault();
1014
+ const start = e.target.selectionStart;
1015
+ const end = e.target.selectionEnd;
1016
+ e.target.value = e.target.value.substring(0, start) + ' ' + e.target.value.substring(end);
1017
+ e.target.selectionStart = e.target.selectionEnd = start + 4;
1018
+ }
1019
+ }
1020
+
1021
+ async function saveCode() {
1022
+ const filename = document.getElementById('editor-filename').value;
1023
+ const content = document.getElementById('code-editor').value;
1024
+
1025
+ try {
1026
+ await fetch('/api/save', {
1027
+ method: 'POST',
1028
+ headers: { 'Content-Type': 'application/json' },
1029
+ body: JSON.stringify({ filename, content })
1030
+ });
1031
+ document.getElementById('preview-frame').src = '/download/index.html?' + Date.now();
1032
+ alert('Saved successfully!');
1033
+ } catch (e) {
1034
+ showError('Failed to save file');
1035
+ }
1036
+ }
1037
+
1038
+ function formatCode() {
1039
+ const editor = document.getElementById('code-editor');
1040
+ let code = editor.value;
1041
+ code = code.replace(/>\\s*</g, '>\\n<').replace(/\\n\\s*\\n/g, '\\n');
1042
+ editor.value = code;
1043
+ }
1044
+
1045
+ // Download ZIP
1046
+ function downloadZip() {
1047
+ window.open('/api/download-zip', '_blank');
1048
+ }
1049
+
1050
+ // Clear Files
1051
+ async function clearFiles() {
1052
+ try {
1053
+ await fetch('/api/clear', { method: 'POST' });
1054
+ document.getElementById('results').classList.add('hidden');
1055
+ document.getElementById('preview-frame').src = '';
1056
+ } catch (err) {
1057
+ showError('Failed to clear files');
1058
+ }
1059
+ }
1060
+
1061
+ function showError(msg) {
1062
+ const errorEl = document.getElementById('error');
1063
+ document.getElementById('errorMsg').textContent = msg;
1064
+ errorEl.classList.remove('hidden');
1065
+ setTimeout(() => errorEl.classList.add('hidden'), 5000);
1066
+ }
1067
+
1068
+ // Entry animation & Auto-Clear
1069
+ document.addEventListener('DOMContentLoaded', () => {
1070
+ // Auto-clear scraped files on reload/load
1071
+ fetch('/api/clear', { method: 'POST' }).catch(err => console.error('Auto-clear failed:', err));
1072
+
1073
+ setTimeout(() => {
1074
+ document.getElementById('main-header').classList.remove('opacity-0', 'translate-y-[-20px]');
1075
+ document.getElementById('input-section').classList.remove('opacity-0', 'scale-95');
1076
+ }, 100);
1077
+
1078
+ // --- Image Forensics Drag \u0026 Drop + URL Monitoring ---
1079
+ const dropZone = document.getElementById('drop-zone');
1080
+ const imageUrlInput = document.getElementById('imageUrlInput');
1081
+ const analyzeBtn = document.getElementById('analyzeImageBtn');
1082
+
1083
+ if (dropZone) {
1084
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
1085
+ dropZone.addEventListener(eventName, e => {
1086
+ e.preventDefault();
1087
+ e.stopPropagation();
1088
+ }, false);
1089
+ });
1090
+
1091
+ ['dragenter', 'dragover'].forEach(eventName => {
1092
+ dropZone.addEventListener(eventName, () => dropZone.classList.add('border-indigo-500', 'bg-slate-800/50'), false);
1093
+ });
1094
+
1095
+ ['dragleave', 'drop'].forEach(eventName => {
1096
+ dropZone.addEventListener(eventName, () => dropZone.classList.remove('border-indigo-500', 'bg-slate-800/50'), false);
1097
+ });
1098
+
1099
+ dropZone.addEventListener('drop', e => {
1100
+ const dt = e.dataTransfer;
1101
+ const files = dt.files;
1102
+ const input = document.getElementById('imageInput');
1103
+ input.files = files;
1104
+ handleImageUpload(input);
1105
+ }, false);
1106
+ }
1107
+
1108
+ if (imageUrlInput) {
1109
+ imageUrlInput.addEventListener('input', () => {
1110
+ if (imageUrlInput.value.trim().length > 0) {
1111
+ analyzeBtn.classList.remove('hidden');
1112
+ } else {
1113
+ analyzeBtn.classList.add('hidden');
1114
+ }
1115
+ });
1116
+
1117
+ imageUrlInput.addEventListener('keydown', e => {
1118
+ if (e.key === 'Enter') {
1119
+ analyzeImage();
1120
+ }
1121
+ });
1122
+ }
1123
+ });
1124
+
1125
+ // Video Modal Functions
1126
+ function openVideoModal(src) {
1127
+ const dialog = document.getElementById('video-modal');
1128
+ const video = document.getElementById('modal-video');
1129
+ video.src = src;
1130
+ dialog.showModal();
1131
+ requestAnimationFrame(() => {
1132
+ video.classList.remove('scale-95', 'opacity-0');
1133
+ video.classList.add('scale-100', 'opacity-100');
1134
+ });
1135
+ }
1136
+ function closeVideoModal() {
1137
+ const dialog = document.getElementById('video-modal');
1138
+ const video = document.getElementById('modal-video');
1139
+ video.classList.remove('scale-100', 'opacity-100');
1140
+ video.classList.add('scale-95', 'opacity-0');
1141
+ setTimeout(() => {
1142
+ dialog.close();
1143
+ video.pause();
1144
+ video.src = '';
1145
+ }, 300);
1146
+ }
1147
+
1148
+ // --- FLICKERING GRID BACKGROUND ---
1149
+ class FlickeringGrid {
1150
+ constructor(canvasId) {
1151
+ this.canvas = document.getElementById(canvasId);
1152
+ this.ctx = this.canvas.getContext('2d');
1153
+ this.squareSize = 4;
1154
+ this.gridGap = 6;
1155
+ this.flickerChance = 0.05;
1156
+ this.color = '0, 255, 65'; // Neon Green
1157
+ this.maxOpacity = 0.4;
1158
+
1159
+ this.squares = [];
1160
+ this.resize();
1161
+
1162
+ window.addEventListener('resize', () => this.resize());
1163
+ this.animate();
1164
+ }
1165
+
1166
+ resize() {
1167
+ this.width = this.canvas.width = window.innerWidth;
1168
+ this.height = this.canvas.height = window.innerHeight;
1169
+
1170
+ const cols = Math.ceil(this.width / (this.squareSize + this.gridGap));
1171
+ const rows = Math.ceil(this.height / (this.squareSize + this.gridGap));
1172
+
1173
+ this.squares = new Float32Array(cols * rows);
1174
+ this.cols = cols;
1175
+ this.rows = rows;
1176
+
1177
+ // Init opacities
1178
+ for (let i = 0; i < this.squares.length; i++) {
1179
+ this.squares[i] = Math.random() * this.maxOpacity;
1180
+ }
1181
+ }
1182
+
1183
+ animate() {
1184
+ this.ctx.clearRect(0, 0, this.width, this.height);
1185
+ this.ctx.fillStyle = `rgb(${this.color})`;
1186
+
1187
+ for (let i = 0; i < this.squares.length; i++) {
1188
+ // Random flicker
1189
+ if (Math.random() < this.flickerChance) {
1190
+ this.squares[i] = Math.random() * this.maxOpacity;
1191
+ }
1192
+
1193
+ // Draw
1194
+ const col = i % this.cols;
1195
+ const row = Math.floor(i / this.cols);
1196
+ const x = col * (this.squareSize + this.gridGap);
1197
+ const y = row * (this.squareSize + this.gridGap);
1198
+
1199
+ this.ctx.globalAlpha = this.squares[i];
1200
+ this.ctx.fillRect(x, y, this.squareSize, this.squareSize);
1201
+ }
1202
+
1203
+ requestAnimationFrame(() => this.animate());
1204
+ }
1205
+ }
1206
+
1207
+ // Init on load
1208
+ window.addEventListener('DOMContentLoaded', () => {
1209
+ // Check if element exists before init to prevent errors
1210
+ if(document.getElementById('flickering-grid')) {
1211
+ new FlickeringGrid('flickering-grid');
1212
+ }
1213
+ });
1214
+
1215
+ // --- IMAGE ANALYSIS FEATURE ---
1216
+ let currentMode = 'scrape';
1217
+
1218
+ function toggleImageMode() {
1219
+ const btn = document.getElementById('btn-image-mode');
1220
+ const scraperMode = document.getElementById('mode-scraper');
1221
+ const imageMode = document.getElementById('mode-image');
1222
+
1223
+ // Check if we are currently in image mode
1224
+ const isImageMode = currentMode === 'image';
1225
+
1226
+ if (isImageMode) {
1227
+ // Switch to Scrape Mode
1228
+ currentMode = 'scrape';
1229
+ scraperMode.classList.remove('hidden');
1230
+ imageMode.classList.add('hidden');
1231
+
1232
+ // Update Button Style (Inactive)
1233
+ btn.classList.remove('text-indigo-400', 'bg-slate-800/50');
1234
+ btn.classList.add('text-slate-400');
1235
+
1236
+ // Hide image results, show scrape results if they exist
1237
+ document.getElementById('image-results').classList.add('hidden');
1238
+ if(document.querySelector('.active-results')) document.getElementById('results').classList.remove('hidden');
1239
+
1240
+ } else {
1241
+ // Switch to Image Mode
1242
+ currentMode = 'image';
1243
+ scraperMode.classList.add('hidden');
1244
+ imageMode.classList.remove('hidden');
1245
+
1246
+ // Update Button Style (Active)
1247
+ btn.classList.add('text-indigo-400', 'bg-slate-800/50');
1248
+ btn.classList.remove('text-slate-400');
1249
+
1250
+ // Hide scrape results
1251
+ document.getElementById('results').classList.add('hidden');
1252
+ }
1253
+ }
1254
+
1255
+ function handleImageUpload(input) {
1256
+ if (input.files && input.files[0]) {
1257
+ const reader = new FileReader();
1258
+ reader.onload = function(e) {
1259
+ document.getElementById('image-preview').src = e.target.result;
1260
+ document.getElementById('image-preview-container').classList.remove('hidden');
1261
+ document.getElementById('drop-content').classList.add('hidden');
1262
+
1263
+ // Auto Analyze on upload
1264
+ analyzeImage();
1265
+
1266
+ // Hide URL input if image is uploaded
1267
+ const sep = document.getElementById('url-separator');
1268
+ const cont = document.getElementById('url-input-container');
1269
+ if(sep) sep.classList.add('hidden');
1270
+ if(cont) cont.classList.add('hidden');
1271
+ }
1272
+ reader.readAsDataURL(input.files[0]);
1273
+ }
1274
+ }
1275
+
1276
+ function clearImage() {
1277
+ document.getElementById('imageInput').value = '';
1278
+ document.getElementById('image-preview').src = '';
1279
+ document.getElementById('image-preview-container').classList.add('hidden');
1280
+ document.getElementById('drop-content').classList.remove('hidden');
1281
+
1282
+ // Show URL input again
1283
+ const sep = document.getElementById('url-separator');
1284
+ const cont = document.getElementById('url-input-container');
1285
+ if(sep) sep.classList.remove('hidden');
1286
+ if(cont) cont.classList.remove('hidden');
1287
+ }
1288
+
1289
+ function copyToClipboard(text) {
1290
+ navigator.clipboard.writeText(text);
1291
+ // Optional: Toast notification
1292
+ }
1293
+
1294
+ function showError(msg) {
1295
+ const errorEl = document.getElementById('error');
1296
+ if(errorEl) {
1297
+ errorEl.classList.remove('hidden');
1298
+ document.getElementById('errorMsg').textContent = msg;
1299
+ } else {
1300
+ alert(msg);
1301
+ }
1302
+ }
1303
+
1304
+ async function analyzeImage() {
1305
+ const inputSection = document.getElementById('input-inner-container');
1306
+ const results = document.getElementById('image-results');
1307
+ const error = document.getElementById('error');
1308
+
1309
+ // Hide previous results
1310
+ document.getElementById('results').classList.add('hidden');
1311
+ error.classList.add('hidden');
1312
+ results.classList.add('hidden');
1313
+
1314
+ inputSection.classList.add('loading-beam-border');
1315
+
1316
+ try {
1317
+ const formData = new FormData();
1318
+ const fileInput = document.getElementById('imageInput');
1319
+ const urlInput = document.getElementById('imageUrlInput');
1320
+
1321
+ let hasData = false;
1322
+
1323
+ if (fileInput.files.length > 0) {
1324
+ formData.append('file', fileInput.files[0]);
1325
+ hasData = true;
1326
+ } else if (urlInput.value.trim()) {
1327
+ formData.append('url', urlInput.value.trim());
1328
+ hasData = true;
1329
+ }
1330
+
1331
+ if (!hasData) {
1332
+ showError('Please upload an image or provide a URL');
1333
+ inputSection.classList.remove('loading-beam-border');
1334
+ return;
1335
+ }
1336
+
1337
+ const response = await fetch('/api/analyze-image', {
1338
+ method: 'POST',
1339
+ body: fileInput.files.length > 0 ? formData : JSON.stringify({url: urlInput.value.trim()}),
1340
+ headers: fileInput.files.length > 0 ? {} : {'Content-Type': 'application/json'}
1341
+ });
1342
+
1343
+ const data = await response.json();
1344
+
1345
+ if (data.success) {
1346
+ inputSection.classList.remove('loading-beam-border');
1347
+
1348
+ // Show Image Results
1349
+ results.classList.remove('hidden');
1350
+
1351
+ // Render Data
1352
+ const meta = data.data;
1353
+ let imgSrc = '';
1354
+ if (data.source === 'upload') {
1355
+ imgSrc = document.getElementById('image-preview').src;
1356
+ } else {
1357
+ imgSrc = urlInput.value.trim();
1358
+ }
1359
+ document.getElementById('result-image-preview').src = imgSrc;
1360
+
1361
+ // Location
1362
+ const locDiv = document.getElementById('result-location');
1363
+ if (meta.location) {
1364
+ locDiv.innerHTML = `
1365
+ <div class="flex flex-col gap-2">
1366
+ <div class="flex items-center gap-2 text-green-400 font-mono">
1367
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
1368
+ <span class="font-bold">${meta.location.lat.toFixed(6)}, ${meta.location.lon.toFixed(6)}</span>
1369
+ </div>
1370
+ <a href="${meta.location.map_url}" target="_blank" class="px-4 py-2 bg-indigo-600 rounded-lg text-white text-xs font-semibold hover:bg-indigo-500 transition-colors inline-block text-center">View on Google Maps</a>
1371
+ </div>
1372
+ `;
1373
+ } else {
1374
+ locDiv.innerHTML = '<span class="italic text-slate-500">No GPS coordinates embedded in this image.</span>';
1375
+ }
1376
+
1377
+ // AI Detection Results (Quick Flagging)
1378
+ if (data.ai_detection) {
1379
+ const ai = data.ai_detection;
1380
+ const scoreText = document.getElementById('result-ai-score-text');
1381
+ const scoreCircle = document.getElementById('result-ai-score-circle');
1382
+ const label = document.getElementById('result-ai-label');
1383
+
1384
+ // Likelihood of REAL = 100 - manipulation likelihood
1385
+ const realLikelihood = Math.max(0, 100 - ai.score);
1386
+ scoreText.innerText = `${Math.round(realLikelihood)}%`;
1387
+
1388
+ // SVG Circle Progress (circumference = 113)
1389
+ const offset = 113 - (113 * realLikelihood) / 100;
1390
+ scoreCircle.style.strokeDashoffset = offset;
1391
+
1392
+ // Colors and Labels
1393
+ label.innerText = ai.label;
1394
+ if (ai.score > 80) {
1395
+ label.className = 'text-red-500 font-bold text-sm';
1396
+ scoreCircle.className = 'text-red-500';
1397
+ } else if (ai.score > 50) {
1398
+ label.className = 'text-yellow-500 font-bold text-sm';
1399
+ scoreCircle.className = 'text-yellow-500';
1400
+ } else {
1401
+ label.className = 'text-green-500 font-bold text-sm';
1402
+ scoreCircle.className = 'text-green-500';
1403
+ }
1404
+
1405
+ // Also update the detailed forensics block if it's visible or for future sync
1406
+ const detailedScoreVal = document.getElementById('ai-score-val');
1407
+ if (detailedScoreVal) {
1408
+ detailedScoreVal.innerText = `${ai.score}%`;
1409
+ const detailedScoreBar = document.getElementById('ai-score-bar');
1410
+ detailedScoreBar.style.width = `${ai.score}%`;
1411
+ document.getElementById('ai-label').innerText = ai.label;
1412
+ document.getElementById('ai-details').innerText = `Frequency Domain Analysis: ${ai.details}`;
1413
+ document.getElementById('ai-result-container').classList.remove('hidden');
1414
+ }
1415
+ }
1416
+
1417
+ // Camera/Basic Info
1418
+ const format = meta.basic.Format || 'Unknown';
1419
+ const size = meta.basic.Size || 'Unknown';
1420
+ const model = (meta.exif.Make ? meta.exif.Make + ' ' : '') + (meta.exif.Model || 'Unknown Camera');
1421
+ document.getElementById('result-camera').innerHTML = `
1422
+ <div class="space-y-2">
1423
+ <div class="flex justify-between border-b border-slate-700/50 pb-1"><span class="text-slate-500">Device</span> <span class="text-white font-medium text-right">${model}</span></div>
1424
+ <div class="flex justify-between border-b border-slate-700/50 pb-1"><span class="text-slate-500">Format</span> <span class="text-white font-medium text-right">${format}</span></div>
1425
+ <div class="flex justify-between border-b border-slate-700/50 pb-1"><span class="text-slate-500">Dimensions</span> <span class="text-white font-medium text-right">${size}</span></div>
1426
+ <div class="flex justify-between pb-1"><span class="text-slate-500">Color Mode</span> <span class="text-white font-medium text-right">${meta.basic.Mode}</span></div>
1427
+ </div>
1428
+ `;
1429
+
1430
+ // Table
1431
+ const tbody = document.getElementById('result-metadata-body');
1432
+ // Merge Basic + EXIF
1433
+ const allMeta = {...meta.basic, ...meta.exif};
1434
+ // Filter internal
1435
+ const rows = Object.entries(allMeta)
1436
+ .filter(([k, v]) => k !== 'MakerNote' && k !== 'UserComment' && k !== 'components_configuration' && k.length < 50 && typeof v === 'string' && v.length < 100)
1437
+ .map(([k, v]) => `
1438
+ <tr class="hover:bg-white/5 transition-colors">
1439
+ <td class="px-4 py-2 font-medium text-indigo-300 border-b border-slate-700/30 w-1/3">${k}</td>
1440
+ <td class="px-4 py-2 text-slate-300 border-b border-slate-700/30 break-all font-mono text-xs">${v}</td>
1441
+ </tr>
1442
+ `).join('');
1443
+ tbody.innerHTML = rows || '<tr><td colspan="2" class="px-4 py-2 text-center italic text-slate-500">No public metadata tags found</td></tr>';
1444
+
1445
+ } else {
1446
+ throw new Error(data.error);
1447
+ }
1448
+
1449
+ } catch (e) {
1450
+ showError(e.message);
1451
+ inputSection.classList.remove('loading-beam-border');
1452
+ }
1453
+ }
1454
+
1455
+ // --- FORENSICS ---
1456
+ async function runELA() {
1457
+ const fileInput = document.getElementById('imageInput');
1458
+ const container = document.getElementById('ela-result-container');
1459
+ const originalPreview = document.getElementById('ela-original-preview');
1460
+ const elaPreview = document.getElementById('ela-result-img');
1461
+ const loading = document.getElementById('ela-loading');
1462
+
1463
+ if (!fileInput.files || !fileInput.files[0]) {
1464
+ showError("Please upload an image first.");
1465
+ return;
1466
+ }
1467
+
1468
+ // Show UI
1469
+ container.classList.remove('hidden');
1470
+ loading.classList.remove('hidden');
1471
+ // Clear previous results
1472
+ elaPreview.src = '';
1473
+
1474
+ // Set original preview if not already set (re-use main preview logic if needed, but safe to set here)
1475
+ const reader = new FileReader();
1476
+ reader.onload = function(e) {
1477
+ originalPreview.src = e.target.result;
1478
+ };
1479
+ reader.readAsDataURL(fileInput.files[0]);
1480
+
1481
+ const formData = new FormData();
1482
+ formData.append('image', fileInput.files[0]);
1483
+
1484
+ try {
1485
+ const response = await fetch('/api/analyze/ela', {
1486
+ method: 'POST',
1487
+ body: formData
1488
+ });
1489
+
1490
+ const data = await response.json();
1491
+
1492
+ if (data.success) {
1493
+ elaPreview.src = data.ela_image;
1494
+ } else {
1495
+ throw new Error(data.error || "ELA Analysis failed");
1496
+ }
1497
+ } catch (e) {
1498
+ showError(e.message);
1499
+ container.classList.add('hidden'); // Hide on error
1500
+ } finally {
1501
+ loading.classList.add('hidden');
1502
+ }
1503
+ }
1504
+
1505
+ async function runAIDetection() {
1506
+ const fileInput = document.getElementById('imageInput');
1507
+ const container = document.getElementById('ai-result-container');
1508
+ const loading = document.getElementById('ai-loading');
1509
+ const scoreVal = document.getElementById('ai-score-val');
1510
+ const scoreBar = document.getElementById('ai-score-bar');
1511
+ const label = document.getElementById('ai-label');
1512
+ const details = document.getElementById('ai-details');
1513
+
1514
+ if (!fileInput.files || !fileInput.files[0]) {
1515
+ showError("Please upload an image first.");
1516
+ return;
1517
+ }
1518
+
1519
+ // Show UI
1520
+ container.classList.remove('hidden');
1521
+ loading.classList.remove('hidden');
1522
+
1523
+ // Reset previous results
1524
+ scoreVal.innerText = '0%';
1525
+ scoreBar.style.width = '0%';
1526
+ label.innerText = 'Analyzing...';
1527
+ label.className = 'text-xl font-bold text-white';
1528
+
1529
+ const formData = new FormData();
1530
+ formData.append('image', fileInput.files[0]);
1531
+
1532
+ try {
1533
+ const response = await fetch('/api/analyze/ai', {
1534
+ method: 'POST',
1535
+ body: formData
1536
+ });
1537
+
1538
+ const data = await response.json();
1539
+
1540
+ if (data.success) {
1541
+ const result = data.data;
1542
+
1543
+ // Update score
1544
+ scoreVal.innerText = `${result.score}%`;
1545
+ scoreBar.style.width = `${result.score}%`;
1546
+
1547
+ // Update label and color
1548
+ label.innerText = result.label;
1549
+ if (result.score > 80) {
1550
+ label.className = 'text-xl font-bold text-red-500';
1551
+ scoreBar.className = 'bg-red-500 h-4 rounded-full transition-all duration-1000';
1552
+ } else if (result.score > 60) {
1553
+ label.className = 'text-xl font-bold text-yellow-500';
1554
+ scoreBar.className = 'bg-yellow-500 h-4 rounded-full transition-all duration-1000';
1555
+ } else {
1556
+ label.className = 'text-xl font-bold text-green-500';
1557
+ scoreBar.className = 'bg-green-500 h-4 rounded-full transition-all duration-1000';
1558
+ }
1559
+
1560
+ details.innerText = `Frequency Domain Analysis: ${result.details}`;
1561
+ } else {
1562
+ throw new Error(data.error || "AI Detection failed");
1563
+ }
1564
+ } catch (e) {
1565
+ showError(e.message);
1566
+ container.classList.add('hidden');
1567
+ } finally {
1568
+ loading.classList.add('hidden');
1569
+ }
1570
+ }
1571
+
1572
+
1573
+ // Export Data Function
1574
+ async function exportData(type, format) {
1575
+ if (!window.currentScrapeData || !window.currentScrapeData.intel) {
1576
+ showError("No data available to export. Please scrape a site first.");
1577
+ return;
1578
+ }
1579
+
1580
+ let dataToExport = [];
1581
+ let filename = `export_${type}`;
1582
+
1583
+ const intel = window.currentScrapeData.intel;
1584
+
1585
+ if (type === 'emails') {
1586
+ dataToExport = intel.emails;
1587
+ } else if (type === 'phones') {
1588
+ dataToExport = intel.phones;
1589
+ } else if (type === 'socials') {
1590
+ dataToExport = intel.socials;
1591
+ } else if (type === 'locations') {
1592
+ dataToExport = intel.locations;
1593
+ }
1594
+
1595
+ if (!dataToExport || dataToExport.length === 0) {
1596
+ showError(`No ${type} found to export.`);
1597
+ return;
1598
+ }
1599
+
1600
+ try {
1601
+ const response = await fetch('/api/export', {
1602
+ method: 'POST',
1603
+ headers: { 'Content-Type': 'application/json' },
1604
+ body: JSON.stringify({
1605
+ data: dataToExport,
1606
+ format: format,
1607
+ filename: filename
1608
+ })
1609
+ });
1610
+
1611
+ if (response.ok) {
1612
+ const blob = await response.blob();
1613
+ const url = window.URL.createObjectURL(blob);
1614
+ const a = document.createElement('a');
1615
+ a.href = url;
1616
+ a.download = `${filename}.${format}`;
1617
+ document.body.appendChild(a);
1618
+ a.click();
1619
+ window.URL.revokeObjectURL(url);
1620
+ a.remove();
1621
+ } else {
1622
+ const err = await response.json();
1623
+ showError(`Export failed: ${err.error}`);
1624
+ }
1625
+ } catch (e) {
1626
+ showError(`Export error: ${e.message}`);
1627
+ }
1628
+ }
1629
+
1630
+ // --- TRANSLATION FEATURE ---
1631
+ async function translateContent(elementId) {
1632
+ const el = document.getElementById(elementId);
1633
+ const langSelect = document.getElementById('trans-lang');
1634
+ const lang = langSelect ? langSelect.value : 'hi';
1635
+ const text = el.innerText;
1636
+
1637
+ if(!text || text === '--' || text === '-') return;
1638
+
1639
+ const btn = window.event ? window.event.target : null;
1640
+ const originalBtnText = btn ? btn.innerText : 'Translate';
1641
+
1642
+ if(btn) {
1643
+ btn.innerText = 'Translating...';
1644
+ btn.disabled = true;
1645
+ }
1646
+
1647
+ try {
1648
+ const response = await fetch('/api/translate', {
1649
+ method: 'POST',
1650
+ headers: { 'Content-Type': 'application/json' },
1651
+ body: JSON.stringify({ text, target: lang })
1652
+ });
1653
+ const data = await response.json();
1654
+ if(data.success) {
1655
+ el.innerText = data.translated;
1656
+ el.classList.add('animate-pulse');
1657
+ setTimeout(() => el.classList.remove('animate-pulse'), 1000);
1658
+ } else {
1659
+ showError(data.error);
1660
+ }
1661
+ } catch (e) {
1662
+ showError("Translation failed: " + e.message);
1663
+ } finally {
1664
+ if(btn) {
1665
+ btn.innerText = originalBtnText;
1666
+ btn.disabled = false;
1667
+ }
1668
+ }
1669
+ }
1670
+
1671
+ async function translatePrompt(elementId) {
1672
+ const lang = prompt("Enter target language code (e.g., hi, es, fr, ja, de):", "hi");
1673
+ if(!lang) return;
1674
+
1675
+ const el = document.getElementById(elementId);
1676
+ const text = el.innerText;
1677
+ if(!text || text === '-' || text === '--') return;
1678
+
1679
+ try {
1680
+ const response = await fetch('/api/translate', {
1681
+ method: 'POST',
1682
+ headers: { 'Content-Type': 'application/json' },
1683
+ body: JSON.stringify({ text, target: lang })
1684
+ });
1685
+ const data = await response.json();
1686
+ if(data.success) {
1687
+ el.innerText = data.translated;
1688
+ el.title = "Original: " + text;
1689
+ el.classList.add('animate-pulse');
1690
+ setTimeout(() => el.classList.remove('animate-pulse'), 1000);
1691
+ } else {
1692
+ showError(data.error);
1693
+ }
1694
+ } catch (e) {
1695
+ showError("Translation failed: " + e.message);
1696
+ }
1697
+ }
1698
+
1699
+ function renderDataTable(data) {
1700
+ const tbody = document.getElementById('intel-table-body');
1701
+ if (!tbody || !data.intel) return;
1702
+
1703
+ let rows = [];
1704
+
1705
+ // Emails
1706
+ if (data.intel.emails && data.intel.emails.length > 0) {
1707
+ data.intel.emails.forEach(email => {
1708
+ rows.push(`
1709
+ <tr class="hover:bg-white/5 transition-colors">
1710
+ <td class="px-4 py-3 font-medium text-indigo-400">Email</td>
1711
+ <td class="px-4 py-3 select-all font-mono">${email}</td>
1712
+ <td class="px-4 py-3 text-right">
1713
+ <button onclick="copyToClipboard('${email}')" class="text-[10px] bg-slate-800 hover:bg-slate-700 px-2 py-1 rounded border border-white/5 uppercase tracking-tighter">Copy</button>
1714
+ </td>
1715
+ </tr>
1716
+ `);
1717
+ });
1718
+ }
1719
+
1720
+ // Phones
1721
+ if (data.intel.phones && data.intel.phones.length > 0) {
1722
+ data.intel.phones.forEach(phone => {
1723
+ rows.push(`
1724
+ <tr class="hover:bg-white/5 transition-colors">
1725
+ <td class="px-4 py-3 font-medium text-orange-400">Phone</td>
1726
+ <td class="px-4 py-3 select-all font-mono">${phone}</td>
1727
+ <td class="px-4 py-3 text-right">
1728
+ <button onclick="copyToClipboard('${phone}')" class="text-[10px] bg-slate-800 hover:bg-slate-700 px-2 py-1 rounded border border-white/5 uppercase tracking-tighter">Copy</button>
1729
+ </td>
1730
+ </tr>
1731
+ `);
1732
+ });
1733
+ }
1734
+
1735
+ // Socials
1736
+ if (data.intel.socials && data.intel.socials.length > 0) {
1737
+ data.intel.socials.forEach(social => {
1738
+ rows.push(`
1739
+ <tr class="hover:bg-white/5 transition-colors">
1740
+ <td class="px-4 py-3 font-medium text-pink-400">Social</td>
1741
+ <td class="px-4 py-3 truncate max-w-[200px]" title="${social.url}">${social.platform}: ${social.url}</td>
1742
+ <td class="px-4 py-3 text-right">
1743
+ <a href="${social.url}" target="_blank" class="text-[10px] bg-indigo-500/20 hover:bg-indigo-500/30 text-indigo-300 px-2 py-1 rounded border border-indigo-500/30 inline-block uppercase tracking-tighter">Visit</a>
1744
+ </td>
1745
+ </tr>
1746
+ `);
1747
+ });
1748
+ }
1749
+
1750
+ // Locations
1751
+ if (data.intel.locations && data.intel.locations.length > 0) {
1752
+ data.intel.locations.forEach(loc => {
1753
+ rows.push(`
1754
+ <tr class="hover:bg-white/5 transition-colors">
1755
+ <td class="px-4 py-3 font-medium text-green-400">Location</td>
1756
+ <td class="px-4 py-3 select-all">${loc}</td>
1757
+ <td class="px-4 py-3 text-right">
1758
+ <button onclick="copyToClipboard('${loc}')" class="text-[10px] bg-slate-800 hover:bg-slate-700 px-2 py-1 rounded border border-white/5 uppercase tracking-tighter">Copy</button>
1759
+ </td>
1760
+ </tr>
1761
+ `);
1762
+ });
1763
+ }
1764
+
1765
+ // Design Colors
1766
+ if (data.design && data.design.colors && data.design.colors.length > 0) {
1767
+ data.design.colors.forEach(color => {
1768
+ rows.push(`
1769
+ <tr class="hover:bg-white/5 transition-colors">
1770
+ <td class="px-4 py-3 font-medium text-pink-400">Color</td>
1771
+ <td class="px-4 py-3">
1772
+ <div class="flex items-center gap-2">
1773
+ <div class="w-4 h-4 rounded shadow-sm border border-white/10" style="background-color: ${color}"></div>
1774
+ <span class="font-mono text-xs">${color}</span>
1775
+ </div>
1776
+ </td>
1777
+ <td class="px-4 py-3 text-right">
1778
+ <button onclick="copyToClipboard('${color}')" class="text-[10px] bg-slate-800 hover:bg-slate-700 px-2 py-1 rounded border border-white/5 uppercase tracking-tighter">Copy</button>
1779
+ </td>
1780
+ </tr>
1781
+ `);
1782
+ });
1783
+ }
1784
+
1785
+ // Design Fonts
1786
+ if (data.design && data.design.fonts && data.design.fonts.length > 0) {
1787
+ data.design.fonts.forEach(font => {
1788
+ rows.push(`
1789
+ <tr class="hover:bg-white/5 transition-colors">
1790
+ <td class="px-4 py-3 font-medium text-cyan-400">Font</td>
1791
+ <td class="px-4 py-3 truncate max-w-[200px] font-mono text-xs">${font}</td>
1792
+ <td class="px-4 py-3 text-right">
1793
+ <button onclick="copyToClipboard('${font}')" class="text-[10px] bg-slate-800 hover:bg-slate-700 px-2 py-1 rounded border border-white/5 uppercase tracking-tighter">Copy</button>
1794
+ </td>
1795
+ </tr>
1796
+ `);
1797
+ });
1798
+ }
1799
+
1800
+ if (rows.length === 0) {
1801
+ tbody.innerHTML = '<tr><td colspan="3" class="px-4 py-8 text-center text-slate-500 italic">No intelligence data found on this page.</td></tr>';
1802
+ } else {
1803
+ tbody.innerHTML = rows.join('');
1804
+ }
1805
+ }