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/__init__.py +4 -0
- webtools/__main__.py +5 -0
- webtools/cli.py +15 -0
- webtools/core.py +2596 -0
- webtools/web/Web_Tools.png +0 -0
- webtools/web/index.html +1102 -0
- webtools/web/script.js +1805 -0
- webtools/web/style.css +71 -0
- webtools_cli-1.0.0.dist-info/METADATA +110 -0
- webtools_cli-1.0.0.dist-info/RECORD +14 -0
- webtools_cli-1.0.0.dist-info/WHEEL +5 -0
- webtools_cli-1.0.0.dist-info/entry_points.txt +2 -0
- webtools_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- webtools_cli-1.0.0.dist-info/top_level.txt +1 -0
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
|
+
}
|