chrome ext, enhancements
- SHA
a311f1334058fdfb635ccc310fb7347a4c6058ec- Parents
-
1e4e26d - Tree
2311a6e
a311f13
a311f1334058fdfb635ccc310fb7347a4c6058ec1e4e26d
2311a6e| Status | File | + | - |
|---|---|---|---|
| A |
dashboard/dashboard.js
|
825 | 0 |
| A |
dashboard/index.html
|
243 | 0 |
| A |
dashboard/styles.css
|
673 | 0 |
| A |
extension-chrome/background/pattern-learning.js
|
505 | 0 |
| A |
extension-chrome/background/screenshot-manager.js
|
408 | 0 |
| A |
extension-chrome/background/service-worker.js
|
393 | 0 |
| A |
extension-chrome/background/unified-handler-v3.js
|
293 | 0 |
| A |
extension-chrome/manifest.json
|
80 | 0 |
dashboard/dashboard.jsadded@@ -0,0 +1,825 @@ | |||
| 1 | +// LooseCannon Analytics Dashboard JavaScript | ||
| 2 | +// Real-time monitoring and control interface | ||
| 3 | + | ||
| 4 | +class LooseCannonDashboard { | ||
| 5 | + constructor() { | ||
| 6 | + this.serverUrl = 'http://localhost:8765'; | ||
| 7 | + this.ws = null; | ||
| 8 | + this.charts = {}; | ||
| 9 | + this.updateInterval = null; | ||
| 10 | + this.startTime = Date.now(); | ||
| 11 | + this.data = { | ||
| 12 | + messages: [], | ||
| 13 | + scammers: [], | ||
| 14 | + conversations: new Map(), | ||
| 15 | + patterns: [], | ||
| 16 | + screenshots: [] | ||
| 17 | + }; | ||
| 18 | + this.init(); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + async init() { | ||
| 22 | + await this.checkServerConnection(); | ||
| 23 | + this.initializeCharts(); | ||
| 24 | + this.setupEventListeners(); | ||
| 25 | + this.connectWebSocket(); | ||
| 26 | + this.startRealtimeUpdates(); | ||
| 27 | + this.loadInitialData(); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + async checkServerConnection() { | ||
| 31 | + const statusElement = document.getElementById('serverStatus'); | ||
| 32 | + | ||
| 33 | + try { | ||
| 34 | + const response = await fetch(`${this.serverUrl}/status`); | ||
| 35 | + if (response.ok) { | ||
| 36 | + statusElement.classList.add('connected'); | ||
| 37 | + console.log('Server connected'); | ||
| 38 | + } else { | ||
| 39 | + throw new Error('Server not responding'); | ||
| 40 | + } | ||
| 41 | + } catch (error) { | ||
| 42 | + console.error('Server connection failed:', error); | ||
| 43 | + statusElement.classList.remove('connected'); | ||
| 44 | + this.showNotification('Server offline - some features unavailable', 'error'); | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + connectWebSocket() { | ||
| 49 | + // WebSocket for real-time updates | ||
| 50 | + this.ws = new WebSocket('ws://localhost:8766'); | ||
| 51 | + | ||
| 52 | + this.ws.onopen = () => { | ||
| 53 | + console.log('WebSocket connected'); | ||
| 54 | + this.showNotification('Connected to real-time feed', 'success'); | ||
| 55 | + }; | ||
| 56 | + | ||
| 57 | + this.ws.onmessage = (event) => { | ||
| 58 | + const data = JSON.parse(event.data); | ||
| 59 | + this.handleRealtimeUpdate(data); | ||
| 60 | + }; | ||
| 61 | + | ||
| 62 | + this.ws.onerror = (error) => { | ||
| 63 | + console.error('WebSocket error:', error); | ||
| 64 | + }; | ||
| 65 | + | ||
| 66 | + this.ws.onclose = () => { | ||
| 67 | + console.log('WebSocket disconnected, reconnecting...'); | ||
| 68 | + setTimeout(() => this.connectWebSocket(), 5000); | ||
| 69 | + }; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + handleRealtimeUpdate(data) { | ||
| 73 | + switch (data.type) { | ||
| 74 | + case 'MESSAGE': | ||
| 75 | + this.addActivityItem(data); | ||
| 76 | + this.updateMessageStats(data); | ||
| 77 | + break; | ||
| 78 | + | ||
| 79 | + case 'SCAMMER_DETECTED': | ||
| 80 | + this.handleScammerDetection(data); | ||
| 81 | + break; | ||
| 82 | + | ||
| 83 | + case 'SESSION_UPDATE': | ||
| 84 | + this.updateActiveSessions(data); | ||
| 85 | + break; | ||
| 86 | + | ||
| 87 | + case 'SCREENSHOT': | ||
| 88 | + this.addScreenshot(data); | ||
| 89 | + break; | ||
| 90 | + | ||
| 91 | + case 'PATTERN_LEARNED': | ||
| 92 | + this.addPattern(data); | ||
| 93 | + break; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + this.updateCharts(); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + initializeCharts() { | ||
| 100 | + const chartOptions = { | ||
| 101 | + responsive: true, | ||
| 102 | + maintainAspectRatio: false, | ||
| 103 | + plugins: { | ||
| 104 | + legend: { | ||
| 105 | + labels: { | ||
| 106 | + color: '#e0e0e0' | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + }, | ||
| 110 | + scales: { | ||
| 111 | + y: { | ||
| 112 | + grid: { | ||
| 113 | + color: '#333' | ||
| 114 | + }, | ||
| 115 | + ticks: { | ||
| 116 | + color: '#888' | ||
| 117 | + } | ||
| 118 | + }, | ||
| 119 | + x: { | ||
| 120 | + grid: { | ||
| 121 | + color: '#333' | ||
| 122 | + }, | ||
| 123 | + ticks: { | ||
| 124 | + color: '#888' | ||
| 125 | + } | ||
| 126 | + } | ||
| 127 | + } | ||
| 128 | + }; | ||
| 129 | + | ||
| 130 | + // Volume Chart | ||
| 131 | + this.charts.volume = new Chart(document.getElementById('volumeChart'), { | ||
| 132 | + type: 'line', | ||
| 133 | + data: { | ||
| 134 | + labels: this.generateTimeLabels(24), | ||
| 135 | + datasets: [{ | ||
| 136 | + label: 'Messages', | ||
| 137 | + data: new Array(24).fill(0), | ||
| 138 | + borderColor: '#667eea', | ||
| 139 | + backgroundColor: 'rgba(102, 126, 234, 0.1)', | ||
| 140 | + tension: 0.4 | ||
| 141 | + }] | ||
| 142 | + }, | ||
| 143 | + options: chartOptions | ||
| 144 | + }); | ||
| 145 | + | ||
| 146 | + // Detection Chart | ||
| 147 | + this.charts.detection = new Chart(document.getElementById('detectionChart'), { | ||
| 148 | + type: 'bar', | ||
| 149 | + data: { | ||
| 150 | + labels: ['Low', 'Medium', 'High', 'Critical'], | ||
| 151 | + datasets: [{ | ||
| 152 | + label: 'Detections', | ||
| 153 | + data: [0, 0, 0, 0], | ||
| 154 | + backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#7c3aed'] | ||
| 155 | + }] | ||
| 156 | + }, | ||
| 157 | + options: chartOptions | ||
| 158 | + }); | ||
| 159 | + | ||
| 160 | + // Platform Chart | ||
| 161 | + this.charts.platform = new Chart(document.getElementById('platformChart'), { | ||
| 162 | + type: 'doughnut', | ||
| 163 | + data: { | ||
| 164 | + labels: ['WhatsApp', 'Telegram', 'Messenger'], | ||
| 165 | + datasets: [{ | ||
| 166 | + data: [0, 0, 0], | ||
| 167 | + backgroundColor: ['#25d366', '#0088cc', '#0084ff'] | ||
| 168 | + }] | ||
| 169 | + }, | ||
| 170 | + options: { | ||
| 171 | + ...chartOptions, | ||
| 172 | + plugins: { | ||
| 173 | + legend: { | ||
| 174 | + position: 'bottom', | ||
| 175 | + labels: { | ||
| 176 | + color: '#e0e0e0' | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + } | ||
| 181 | + }); | ||
| 182 | + | ||
| 183 | + // Response Time Chart | ||
| 184 | + this.charts.response = new Chart(document.getElementById('responseChart'), { | ||
| 185 | + type: 'line', | ||
| 186 | + data: { | ||
| 187 | + labels: this.generateTimeLabels(60, 'minutes'), | ||
| 188 | + datasets: [{ | ||
| 189 | + label: 'Response Time (ms)', | ||
| 190 | + data: new Array(60).fill(0), | ||
| 191 | + borderColor: '#3b82f6', | ||
| 192 | + backgroundColor: 'rgba(59, 130, 246, 0.1)', | ||
| 193 | + tension: 0.4 | ||
| 194 | + }] | ||
| 195 | + }, | ||
| 196 | + options: chartOptions | ||
| 197 | + }); | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + generateTimeLabels(count, unit = 'hours') { | ||
| 201 | + const labels = []; | ||
| 202 | + for (let i = count - 1; i >= 0; i--) { | ||
| 203 | + if (unit === 'hours') { | ||
| 204 | + labels.push(`${i}h`); | ||
| 205 | + } else { | ||
| 206 | + labels.push(`${i}m`); | ||
| 207 | + } | ||
| 208 | + } | ||
| 209 | + return labels; | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + updateCharts() { | ||
| 213 | + // Update volume chart with message counts | ||
| 214 | + const volumeData = this.calculateVolumeData(); | ||
| 215 | + this.charts.volume.data.datasets[0].data = volumeData; | ||
| 216 | + this.charts.volume.update(); | ||
| 217 | + | ||
| 218 | + // Update detection chart | ||
| 219 | + const detectionData = this.calculateDetectionData(); | ||
| 220 | + this.charts.detection.data.datasets[0].data = detectionData; | ||
| 221 | + this.charts.detection.update(); | ||
| 222 | + | ||
| 223 | + // Update platform distribution | ||
| 224 | + const platformData = this.calculatePlatformData(); | ||
| 225 | + this.charts.platform.data.datasets[0].data = platformData; | ||
| 226 | + this.charts.platform.update(); | ||
| 227 | + | ||
| 228 | + // Update response times | ||
| 229 | + const responseData = this.calculateResponseData(); | ||
| 230 | + this.charts.response.data.datasets[0].data = responseData; | ||
| 231 | + this.charts.response.update(); | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + calculateVolumeData() { | ||
| 235 | + // Group messages by hour | ||
| 236 | + const hourCounts = new Array(24).fill(0); | ||
| 237 | + const now = Date.now(); | ||
| 238 | + | ||
| 239 | + this.data.messages.forEach(msg => { | ||
| 240 | + const age = (now - msg.timestamp) / (1000 * 60 * 60); | ||
| 241 | + if (age < 24) { | ||
| 242 | + const hour = Math.floor(age); | ||
| 243 | + hourCounts[23 - hour]++; | ||
| 244 | + } | ||
| 245 | + }); | ||
| 246 | + | ||
| 247 | + return hourCounts; | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + calculateDetectionData() { | ||
| 251 | + const counts = [0, 0, 0, 0]; // Low, Medium, High, Critical | ||
| 252 | + | ||
| 253 | + this.data.scammers.forEach(scammer => { | ||
| 254 | + if (scammer.score < 0.3) counts[0]++; | ||
| 255 | + else if (scammer.score < 0.6) counts[1]++; | ||
| 256 | + else if (scammer.score < 0.9) counts[2]++; | ||
| 257 | + else counts[3]++; | ||
| 258 | + }); | ||
| 259 | + | ||
| 260 | + return counts; | ||
| 261 | + } | ||
| 262 | + | ||
| 263 | + calculatePlatformData() { | ||
| 264 | + const counts = { whatsapp: 0, telegram: 0, messenger: 0 }; | ||
| 265 | + | ||
| 266 | + this.data.conversations.forEach(conv => { | ||
| 267 | + counts[conv.platform] = (counts[conv.platform] || 0) + 1; | ||
| 268 | + }); | ||
| 269 | + | ||
| 270 | + return [counts.whatsapp, counts.telegram, counts.messenger]; | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + calculateResponseData() { | ||
| 274 | + // Return last 60 response time measurements | ||
| 275 | + return this.data.messages | ||
| 276 | + .slice(-60) | ||
| 277 | + .map(msg => msg.responseTime || 0); | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + addActivityItem(data) { | ||
| 281 | + const feed = document.getElementById('activityFeed'); | ||
| 282 | + const item = document.createElement('div'); | ||
| 283 | + item.className = 'activity-item'; | ||
| 284 | + | ||
| 285 | + if (data.scammerScore > 0.7) { | ||
| 286 | + item.classList.add('scammer'); | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + const time = new Date(data.timestamp).toLocaleTimeString(); | ||
| 290 | + | ||
| 291 | + item.innerHTML = ` | ||
| 292 | + <span class="activity-time">${time}</span> | ||
| 293 | + <span class="activity-message">${data.message || 'New activity'}</span> | ||
| 294 | + `; | ||
| 295 | + | ||
| 296 | + feed.insertBefore(item, feed.firstChild); | ||
| 297 | + | ||
| 298 | + // Keep only last 50 items | ||
| 299 | + while (feed.children.length > 50) { | ||
| 300 | + feed.removeChild(feed.lastChild); | ||
| 301 | + } | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + updateMessageStats(data) { | ||
| 305 | + this.data.messages.push(data); | ||
| 306 | + | ||
| 307 | + // Update counters | ||
| 308 | + document.getElementById('totalMessages').textContent = this.data.messages.length; | ||
| 309 | + | ||
| 310 | + // Calculate and show trend | ||
| 311 | + const trend = this.calculateTrend('messages'); | ||
| 312 | + const trendElement = document.getElementById('messageTrend'); | ||
| 313 | + trendElement.textContent = `${trend > 0 ? '+' : ''}${trend}%`; | ||
| 314 | + trendElement.className = trend >= 0 ? 'stat-trend' : 'stat-trend negative'; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + handleScammerDetection(data) { | ||
| 318 | + this.data.scammers.push(data); | ||
| 319 | + | ||
| 320 | + // Update counter | ||
| 321 | + document.getElementById('scammersDetected').textContent = this.data.scammers.length; | ||
| 322 | + | ||
| 323 | + // Add special notification | ||
| 324 | + this.showNotification(`Scammer detected! Confidence: ${(data.score * 100).toFixed(0)}%`, 'warning'); | ||
| 325 | + | ||
| 326 | + // Flash the stat card | ||
| 327 | + const card = document.querySelector('.stat-card.danger'); | ||
| 328 | + card.style.animation = 'pulse 0.5s'; | ||
| 329 | + setTimeout(() => { | ||
| 330 | + card.style.animation = ''; | ||
| 331 | + }, 500); | ||
| 332 | + | ||
| 333 | + // Add to activity feed | ||
| 334 | + this.addActivityItem({ | ||
| 335 | + ...data, | ||
| 336 | + message: `Scammer detected on ${data.platform} (${(data.score * 100).toFixed(0)}% confidence)` | ||
| 337 | + }); | ||
| 338 | + } | ||
| 339 | + | ||
| 340 | + updateActiveSessions(data) { | ||
| 341 | + document.getElementById('activeSessions').textContent = data.count || 0; | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + addScreenshot(data) { | ||
| 345 | + const grid = document.getElementById('evidenceGrid'); | ||
| 346 | + | ||
| 347 | + // Remove placeholder if exists | ||
| 348 | + const placeholder = grid.querySelector('.evidence-placeholder'); | ||
| 349 | + if (placeholder) { | ||
| 350 | + placeholder.remove(); | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + const item = document.createElement('div'); | ||
| 354 | + item.className = 'evidence-item'; | ||
| 355 | + item.innerHTML = ` | ||
| 356 | + <img src="${data.thumbnail}" alt="Evidence"> | ||
| 357 | + <div class="evidence-meta"> | ||
| 358 | + ${data.platform} - ${new Date(data.timestamp).toLocaleString()} | ||
| 359 | + </div> | ||
| 360 | + `; | ||
| 361 | + | ||
| 362 | + grid.insertBefore(item, grid.firstChild); | ||
| 363 | + | ||
| 364 | + // Keep only last 12 screenshots | ||
| 365 | + while (grid.children.length > 12) { | ||
| 366 | + grid.removeChild(grid.lastChild); | ||
| 367 | + } | ||
| 368 | + | ||
| 369 | + this.data.screenshots.push(data); | ||
| 370 | + } | ||
| 371 | + | ||
| 372 | + addPattern(data) { | ||
| 373 | + this.data.patterns.push(data); | ||
| 374 | + | ||
| 375 | + // Update pattern stats | ||
| 376 | + document.getElementById('totalPatterns').textContent = this.data.patterns.length; | ||
| 377 | + document.getElementById('patternAccuracy').textContent = | ||
| 378 | + `${(this.calculatePatternAccuracy() * 100).toFixed(0)}%`; | ||
| 379 | + | ||
| 380 | + // Update patterns list | ||
| 381 | + const list = document.getElementById('patternsList'); | ||
| 382 | + const item = document.createElement('div'); | ||
| 383 | + item.className = 'pattern-item'; | ||
| 384 | + item.innerHTML = ` | ||
| 385 | + <span class="pattern-type">${data.type}</span> | ||
| 386 | + <span class="pattern-count">${data.occurrences} occurrences</span> | ||
| 387 | + `; | ||
| 388 | + | ||
| 389 | + list.insertBefore(item, list.firstChild); | ||
| 390 | + | ||
| 391 | + // Keep only last 10 patterns | ||
| 392 | + while (list.children.length > 10) { | ||
| 393 | + list.removeChild(list.lastChild); | ||
| 394 | + } | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + calculatePatternAccuracy() { | ||
| 398 | + if (this.data.patterns.length === 0) return 0; | ||
| 399 | + | ||
| 400 | + const correct = this.data.patterns.filter(p => p.verified).length; | ||
| 401 | + return correct / this.data.patterns.length; | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + calculateTrend(metric) { | ||
| 405 | + // Simple trend calculation (current hour vs previous hour) | ||
| 406 | + const now = Date.now(); | ||
| 407 | + const hourAgo = now - (60 * 60 * 1000); | ||
| 408 | + | ||
| 409 | + let current = 0; | ||
| 410 | + let previous = 0; | ||
| 411 | + | ||
| 412 | + if (metric === 'messages') { | ||
| 413 | + this.data.messages.forEach(msg => { | ||
| 414 | + if (msg.timestamp > hourAgo) current++; | ||
| 415 | + else if (msg.timestamp > (hourAgo - 60 * 60 * 1000)) previous++; | ||
| 416 | + }); | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + if (previous === 0) return current > 0 ? 100 : 0; | ||
| 420 | + return Math.round(((current - previous) / previous) * 100); | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + setupEventListeners() { | ||
| 424 | + // Export button | ||
| 425 | + document.getElementById('exportBtn').addEventListener('click', () => { | ||
| 426 | + this.exportData(); | ||
| 427 | + }); | ||
| 428 | + | ||
| 429 | + // Emergency stop | ||
| 430 | + document.getElementById('emergencyStop').addEventListener('click', () => { | ||
| 431 | + this.emergencyStop(); | ||
| 432 | + }); | ||
| 433 | + | ||
| 434 | + // Clear data | ||
| 435 | + document.getElementById('clearDataBtn').addEventListener('click', () => { | ||
| 436 | + this.clearOldData(); | ||
| 437 | + }); | ||
| 438 | + | ||
| 439 | + // Sync patterns | ||
| 440 | + document.getElementById('syncBtn').addEventListener('click', () => { | ||
| 441 | + this.syncPatterns(); | ||
| 442 | + }); | ||
| 443 | + | ||
| 444 | + // Personality builder | ||
| 445 | + document.getElementById('personalityBtn').addEventListener('click', () => { | ||
| 446 | + this.openPersonalityBuilder(); | ||
| 447 | + }); | ||
| 448 | + | ||
| 449 | + // Conversation replay | ||
| 450 | + document.getElementById('replayBtn').addEventListener('click', () => { | ||
| 451 | + this.openConversationReplay(); | ||
| 452 | + }); | ||
| 453 | + | ||
| 454 | + // Modal controls | ||
| 455 | + document.getElementById('modalClose').addEventListener('click', () => { | ||
| 456 | + document.getElementById('personalityModal').classList.remove('active'); | ||
| 457 | + }); | ||
| 458 | + | ||
| 459 | + document.getElementById('replayClose').addEventListener('click', () => { | ||
| 460 | + document.getElementById('replayModal').classList.remove('active'); | ||
| 461 | + }); | ||
| 462 | + | ||
| 463 | + // Personality form | ||
| 464 | + document.getElementById('personalityForm').addEventListener('submit', (e) => { | ||
| 465 | + e.preventDefault(); | ||
| 466 | + this.savePersonality(); | ||
| 467 | + }); | ||
| 468 | + | ||
| 469 | + // Test personality | ||
| 470 | + document.getElementById('testPersonality').addEventListener('click', () => { | ||
| 471 | + this.testPersonality(); | ||
| 472 | + }); | ||
| 473 | + | ||
| 474 | + // Temperature slider | ||
| 475 | + document.getElementById('temperature').addEventListener('input', (e) => { | ||
| 476 | + document.getElementById('tempValue').textContent = e.target.value; | ||
| 477 | + }); | ||
| 478 | + } | ||
| 479 | + | ||
| 480 | + async exportData() { | ||
| 481 | + try { | ||
| 482 | + const data = { | ||
| 483 | + timestamp: new Date().toISOString(), | ||
| 484 | + messages: this.data.messages, | ||
| 485 | + scammers: this.data.scammers, | ||
| 486 | + patterns: this.data.patterns, | ||
| 487 | + screenshots: this.data.screenshots.map(s => s.id), | ||
| 488 | + conversations: Array.from(this.data.conversations.values()) | ||
| 489 | + }; | ||
| 490 | + | ||
| 491 | + const blob = new Blob([JSON.stringify(data, null, 2)], { | ||
| 492 | + type: 'application/json' | ||
| 493 | + }); | ||
| 494 | + | ||
| 495 | + const url = URL.createObjectURL(blob); | ||
| 496 | + const a = document.createElement('a'); | ||
| 497 | + a.href = url; | ||
| 498 | + a.download = `loosecannon-export-${Date.now()}.json`; | ||
| 499 | + a.click(); | ||
| 500 | + | ||
| 501 | + this.showNotification('Data exported successfully', 'success'); | ||
| 502 | + } catch (error) { | ||
| 503 | + console.error('Export error:', error); | ||
| 504 | + this.showNotification('Export failed', 'error'); | ||
| 505 | + } | ||
| 506 | + } | ||
| 507 | + | ||
| 508 | + async emergencyStop() { | ||
| 509 | + if (!confirm('This will stop ALL active LooseCannon sessions. Continue?')) { | ||
| 510 | + return; | ||
| 511 | + } | ||
| 512 | + | ||
| 513 | + try { | ||
| 514 | + const response = await fetch(`${this.serverUrl}/emergency-stop`, { | ||
| 515 | + method: 'POST' | ||
| 516 | + }); | ||
| 517 | + | ||
| 518 | + if (response.ok) { | ||
| 519 | + this.showNotification('Emergency stop activated', 'warning'); | ||
| 520 | + | ||
| 521 | + // Clear active sessions display | ||
| 522 | + document.getElementById('activeSessions').textContent = '0'; | ||
| 523 | + | ||
| 524 | + // Send stop command via WebSocket | ||
| 525 | + if (this.ws && this.ws.readyState === WebSocket.OPEN) { | ||
| 526 | + this.ws.send(JSON.stringify({ type: 'EMERGENCY_STOP' })); | ||
| 527 | + } | ||
| 528 | + } | ||
| 529 | + } catch (error) { | ||
| 530 | + console.error('Emergency stop error:', error); | ||
| 531 | + } | ||
| 532 | + } | ||
| 533 | + | ||
| 534 | + async clearOldData() { | ||
| 535 | + if (!confirm('Clear data older than 24 hours?')) { | ||
| 536 | + return; | ||
| 537 | + } | ||
| 538 | + | ||
| 539 | + const cutoff = Date.now() - (24 * 60 * 60 * 1000); | ||
| 540 | + | ||
| 541 | + // Filter data | ||
| 542 | + this.data.messages = this.data.messages.filter(m => m.timestamp > cutoff); | ||
| 543 | + this.data.scammers = this.data.scammers.filter(s => s.timestamp > cutoff); | ||
| 544 | + this.data.screenshots = this.data.screenshots.filter(s => s.timestamp > cutoff); | ||
| 545 | + | ||
| 546 | + // Update displays | ||
| 547 | + this.updateCharts(); | ||
| 548 | + this.showNotification('Old data cleared', 'success'); | ||
| 549 | + } | ||
| 550 | + | ||
| 551 | + async syncPatterns() { | ||
| 552 | + try { | ||
| 553 | + const response = await fetch(`${this.serverUrl}/patterns/sync`, { | ||
| 554 | + method: 'GET' | ||
| 555 | + }); | ||
| 556 | + | ||
| 557 | + if (response.ok) { | ||
| 558 | + const patterns = await response.json(); | ||
| 559 | + | ||
| 560 | + // Merge with local patterns | ||
| 561 | + patterns.forEach(pattern => { | ||
| 562 | + if (!this.data.patterns.find(p => p.id === pattern.id)) { | ||
| 563 | + this.addPattern(pattern); | ||
| 564 | + } | ||
| 565 | + }); | ||
| 566 | + | ||
| 567 | + this.showNotification(`Synced ${patterns.length} patterns`, 'success'); | ||
| 568 | + } | ||
| 569 | + } catch (error) { | ||
| 570 | + console.error('Pattern sync error:', error); | ||
| 571 | + this.showNotification('Pattern sync failed', 'error'); | ||
| 572 | + } | ||
| 573 | + } | ||
| 574 | + | ||
| 575 | + openPersonalityBuilder() { | ||
| 576 | + document.getElementById('personalityModal').classList.add('active'); | ||
| 577 | + } | ||
| 578 | + | ||
| 579 | + async savePersonality() { | ||
| 580 | + const personality = { | ||
| 581 | + id: document.getElementById('personalityName').value.toLowerCase().replace(/\s+/g, '-'), | ||
| 582 | + name: document.getElementById('personalityName').value, | ||
| 583 | + systemPrompt: document.getElementById('systemPrompt').value, | ||
| 584 | + temperature: parseFloat(document.getElementById('temperature').value), | ||
| 585 | + examples: document.getElementById('examples').value.split('\n').filter(e => e.trim()) | ||
| 586 | + }; | ||
| 587 | + | ||
| 588 | + try { | ||
| 589 | + const response = await fetch(`${this.serverUrl}/personalities`, { | ||
| 590 | + method: 'POST', | ||
| 591 | + headers: { 'Content-Type': 'application/json' }, | ||
| 592 | + body: JSON.stringify(personality) | ||
| 593 | + }); | ||
| 594 | + | ||
| 595 | + if (response.ok) { | ||
| 596 | + this.showNotification('Personality saved successfully', 'success'); | ||
| 597 | + document.getElementById('personalityModal').classList.remove('active'); | ||
| 598 | + document.getElementById('personalityForm').reset(); | ||
| 599 | + } | ||
| 600 | + } catch (error) { | ||
| 601 | + console.error('Save personality error:', error); | ||
| 602 | + this.showNotification('Failed to save personality', 'error'); | ||
| 603 | + } | ||
| 604 | + } | ||
| 605 | + | ||
| 606 | + async testPersonality() { | ||
| 607 | + const testPrompt = "Hello, I have an important message about your account."; | ||
| 608 | + const systemPrompt = document.getElementById('systemPrompt').value; | ||
| 609 | + const temperature = parseFloat(document.getElementById('temperature').value); | ||
| 610 | + | ||
| 611 | + try { | ||
| 612 | + const response = await fetch(`${this.serverUrl}/test-personality`, { | ||
| 613 | + method: 'POST', | ||
| 614 | + headers: { 'Content-Type': 'application/json' }, | ||
| 615 | + body: JSON.stringify({ | ||
| 616 | + systemPrompt, | ||
| 617 | + temperature, | ||
| 618 | + message: testPrompt | ||
| 619 | + }) | ||
| 620 | + }); | ||
| 621 | + | ||
| 622 | + if (response.ok) { | ||
| 623 | + const result = await response.json(); | ||
| 624 | + document.getElementById('testOutput').innerHTML = ` | ||
| 625 | + <strong>Test Input:</strong> "${testPrompt}"<br> | ||
| 626 | + <strong>Response:</strong> "${result.response}" | ||
| 627 | + `; | ||
| 628 | + } | ||
| 629 | + } catch (error) { | ||
| 630 | + console.error('Test personality error:', error); | ||
| 631 | + document.getElementById('testOutput').innerHTML = 'Test failed'; | ||
| 632 | + } | ||
| 633 | + } | ||
| 634 | + | ||
| 635 | + openConversationReplay() { | ||
| 636 | + document.getElementById('replayModal').classList.add('active'); | ||
| 637 | + this.loadConversations(); | ||
| 638 | + } | ||
| 639 | + | ||
| 640 | + async loadConversations() { | ||
| 641 | + const select = document.getElementById('conversationSelect'); | ||
| 642 | + select.innerHTML = '<option>Select a conversation...</option>'; | ||
| 643 | + | ||
| 644 | + this.data.conversations.forEach((conv, id) => { | ||
| 645 | + const option = document.createElement('option'); | ||
| 646 | + option.value = id; | ||
| 647 | + option.textContent = `${conv.platform} - ${conv.chatId} (${conv.messages.length} messages)`; | ||
| 648 | + select.appendChild(option); | ||
| 649 | + }); | ||
| 650 | + } | ||
| 651 | + | ||
| 652 | + startRealtimeUpdates() { | ||
| 653 | + // Update uptime | ||
| 654 | + setInterval(() => { | ||
| 655 | + const uptime = Date.now() - this.startTime; | ||
| 656 | + const hours = Math.floor(uptime / (1000 * 60 * 60)); | ||
| 657 | + const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60)); | ||
| 658 | + document.getElementById('uptime').textContent = `${hours}h ${minutes}m`; | ||
| 659 | + }, 60000); | ||
| 660 | + | ||
| 661 | + // Update last updated | ||
| 662 | + setInterval(() => { | ||
| 663 | + document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString(); | ||
| 664 | + }, 1000); | ||
| 665 | + | ||
| 666 | + // Update response time | ||
| 667 | + setInterval(() => { | ||
| 668 | + const avgResponse = this.calculateAverageResponseTime(); | ||
| 669 | + document.getElementById('responseTime').textContent = `${avgResponse}ms`; | ||
| 670 | + }, 5000); | ||
| 671 | + } | ||
| 672 | + | ||
| 673 | + calculateAverageResponseTime() { | ||
| 674 | + if (this.data.messages.length === 0) return 0; | ||
| 675 | + | ||
| 676 | + const recent = this.data.messages.slice(-20); | ||
| 677 | + const sum = recent.reduce((acc, msg) => acc + (msg.responseTime || 0), 0); | ||
| 678 | + return Math.round(sum / recent.length); | ||
| 679 | + } | ||
| 680 | + | ||
| 681 | + async loadInitialData() { | ||
| 682 | + try { | ||
| 683 | + // Load analytics | ||
| 684 | + const analyticsResponse = await fetch(`${this.serverUrl}/analytics`); | ||
| 685 | + if (analyticsResponse.ok) { | ||
| 686 | + const analytics = await analyticsResponse.json(); | ||
| 687 | + document.getElementById('totalMessages').textContent = analytics.totalMessages || 0; | ||
| 688 | + document.getElementById('scammersDetected').textContent = analytics.scammersDetected || 0; | ||
| 689 | + } | ||
| 690 | + | ||
| 691 | + // Load active conversations | ||
| 692 | + const conversationsResponse = await fetch(`${this.serverUrl}/conversations/active`); | ||
| 693 | + if (conversationsResponse.ok) { | ||
| 694 | + const conversations = await conversationsResponse.json(); | ||
| 695 | + this.updateConversationCards(conversations); | ||
| 696 | + } | ||
| 697 | + | ||
| 698 | + // Load patterns | ||
| 699 | + const patternsResponse = await fetch(`${this.serverUrl}/patterns`); | ||
| 700 | + if (patternsResponse.ok) { | ||
| 701 | + const patterns = await patternsResponse.json(); | ||
| 702 | + patterns.forEach(pattern => this.addPattern(pattern)); | ||
| 703 | + } | ||
| 704 | + } catch (error) { | ||
| 705 | + console.error('Error loading initial data:', error); | ||
| 706 | + } | ||
| 707 | + } | ||
| 708 | + | ||
| 709 | + updateConversationCards(conversations) { | ||
| 710 | + const grid = document.getElementById('conversationsGrid'); | ||
| 711 | + grid.innerHTML = ''; | ||
| 712 | + | ||
| 713 | + if (conversations.length === 0) { | ||
| 714 | + grid.innerHTML = ` | ||
| 715 | + <div class="conversation-card"> | ||
| 716 | + <div class="conversation-header"> | ||
| 717 | + <span class="conversation-id">No active conversations</span> | ||
| 718 | + </div> | ||
| 719 | + </div> | ||
| 720 | + `; | ||
| 721 | + return; | ||
| 722 | + } | ||
| 723 | + | ||
| 724 | + conversations.forEach(conv => { | ||
| 725 | + this.data.conversations.set(conv.id, conv); | ||
| 726 | + | ||
| 727 | + const card = document.createElement('div'); | ||
| 728 | + card.className = 'conversation-card'; | ||
| 729 | + card.innerHTML = ` | ||
| 730 | + <div class="conversation-header"> | ||
| 731 | + <span class="platform-badge ${conv.platform}">${conv.platform}</span> | ||
| 732 | + <span class="conversation-id">${conv.chatId}</span> | ||
| 733 | + </div> | ||
| 734 | + <div class="conversation-stats"> | ||
| 735 | + <span>Messages: ${conv.messageCount}</span> | ||
| 736 | + <span>Score: ${conv.scammerScore.toFixed(2)}</span> | ||
| 737 | + <span>Duration: ${Math.round(conv.duration / 60000)}m</span> | ||
| 738 | + </div> | ||
| 739 | + `; | ||
| 740 | + | ||
| 741 | + card.addEventListener('click', () => { | ||
| 742 | + this.viewConversation(conv.id); | ||
| 743 | + }); | ||
| 744 | + | ||
| 745 | + grid.appendChild(card); | ||
| 746 | + }); | ||
| 747 | + | ||
| 748 | + document.getElementById('activeSessions').textContent = conversations.length; | ||
| 749 | + } | ||
| 750 | + | ||
| 751 | + viewConversation(conversationId) { | ||
| 752 | + const conv = this.data.conversations.get(conversationId); | ||
| 753 | + if (!conv) return; | ||
| 754 | + | ||
| 755 | + // Open replay modal with this conversation | ||
| 756 | + document.getElementById('replayModal').classList.add('active'); | ||
| 757 | + document.getElementById('conversationSelect').value = conversationId; | ||
| 758 | + this.loadConversationMessages(conversationId); | ||
| 759 | + } | ||
| 760 | + | ||
| 761 | + loadConversationMessages(conversationId) { | ||
| 762 | + const conv = this.data.conversations.get(conversationId); | ||
| 763 | + if (!conv) return; | ||
| 764 | + | ||
| 765 | + const container = document.getElementById('replayMessages'); | ||
| 766 | + container.innerHTML = ''; | ||
| 767 | + | ||
| 768 | + conv.messages.forEach(msg => { | ||
| 769 | + const msgDiv = document.createElement('div'); | ||
| 770 | + msgDiv.className = `replay-message ${msg.sender}`; | ||
| 771 | + msgDiv.innerHTML = ` | ||
| 772 | + <div class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</div> | ||
| 773 | + <div class="message-content">${msg.content}</div> | ||
| 774 | + `; | ||
| 775 | + container.appendChild(msgDiv); | ||
| 776 | + }); | ||
| 777 | + | ||
| 778 | + // Update stats | ||
| 779 | + document.getElementById('replayStats').innerHTML = ` | ||
| 780 | + <span>Messages: ${conv.messages.length}</span> | ||
| 781 | + <span>Duration: ${Math.round(conv.duration / 60000)}m</span> | ||
| 782 | + <span>Scammer Score: ${conv.scammerScore.toFixed(2)}</span> | ||
| 783 | + `; | ||
| 784 | + } | ||
| 785 | + | ||
| 786 | + showNotification(message, type = 'info') { | ||
| 787 | + // Create notification element | ||
| 788 | + const notification = document.createElement('div'); | ||
| 789 | + notification.className = `notification ${type}`; | ||
| 790 | + notification.textContent = message; | ||
| 791 | + notification.style.cssText = ` | ||
| 792 | + position: fixed; | ||
| 793 | + top: 80px; | ||
| 794 | + right: 20px; | ||
| 795 | + padding: 1rem 1.5rem; | ||
| 796 | + border-radius: 8px; | ||
| 797 | + z-index: 2000; | ||
| 798 | + animation: slideIn 0.3s ease; | ||
| 799 | + `; | ||
| 800 | + | ||
| 801 | + // Style based on type | ||
| 802 | + const colors = { | ||
| 803 | + info: '#3b82f6', | ||
| 804 | + success: '#10b981', | ||
| 805 | + warning: '#f59e0b', | ||
| 806 | + error: '#ef4444' | ||
| 807 | + }; | ||
| 808 | + | ||
| 809 | + notification.style.background = colors[type] || colors.info; | ||
| 810 | + notification.style.color = 'white'; | ||
| 811 | + | ||
| 812 | + document.body.appendChild(notification); | ||
| 813 | + | ||
| 814 | + // Auto remove after 5 seconds | ||
| 815 | + setTimeout(() => { | ||
| 816 | + notification.style.animation = 'slideOut 0.3s ease'; | ||
| 817 | + setTimeout(() => notification.remove(), 300); | ||
| 818 | + }, 5000); | ||
| 819 | + } | ||
| 820 | +} | ||
| 821 | + | ||
| 822 | +// Initialize dashboard when DOM is ready | ||
| 823 | +document.addEventListener('DOMContentLoaded', () => { | ||
| 824 | + window.dashboard = new LooseCannonDashboard(); | ||
| 825 | +}); | ||
dashboard/index.htmladded@@ -0,0 +1,243 @@ | |||
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="en"> | ||
| 3 | +<head> | ||
| 4 | + <meta charset="UTF-8"> | ||
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 6 | + <title>LooseCannon Analytics Dashboard</title> | ||
| 7 | + <link rel="stylesheet" href="styles.css"> | ||
| 8 | + <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> | ||
| 9 | +</head> | ||
| 10 | +<body> | ||
| 11 | + <div class="dashboard"> | ||
| 12 | + <!-- Header --> | ||
| 13 | + <header class="dashboard-header"> | ||
| 14 | + <div class="header-content"> | ||
| 15 | + <h1>LooseCannon Command Center</h1> | ||
| 16 | + <div class="server-status"> | ||
| 17 | + <span class="status-indicator" id="serverStatus"></span> | ||
| 18 | + <span>Server: <span id="serverUrl">localhost:8765</span></span> | ||
| 19 | + </div> | ||
| 20 | + </div> | ||
| 21 | + </header> | ||
| 22 | + | ||
| 23 | + <!-- Main Grid --> | ||
| 24 | + <main class="dashboard-main"> | ||
| 25 | + <!-- Stats Cards --> | ||
| 26 | + <section class="stats-grid"> | ||
| 27 | + <div class="stat-card"> | ||
| 28 | + <div class="stat-value" id="totalMessages">0</div> | ||
| 29 | + <div class="stat-label">Messages Processed</div> | ||
| 30 | + <div class="stat-trend" id="messageTrend">+0%</div> | ||
| 31 | + </div> | ||
| 32 | + | ||
| 33 | + <div class="stat-card danger"> | ||
| 34 | + <div class="stat-value" id="scammersDetected">0</div> | ||
| 35 | + <div class="stat-label">Scammers Detected</div> | ||
| 36 | + <div class="stat-trend" id="scammerTrend">+0%</div> | ||
| 37 | + </div> | ||
| 38 | + | ||
| 39 | + <div class="stat-card success"> | ||
| 40 | + <div class="stat-value" id="activeSessions">0</div> | ||
| 41 | + <div class="stat-label">Active Sessions</div> | ||
| 42 | + <div class="stat-indicator"> | ||
| 43 | + <span class="pulse"></span> | ||
| 44 | + Live | ||
| 45 | + </div> | ||
| 46 | + </div> | ||
| 47 | + | ||
| 48 | + <div class="stat-card info"> | ||
| 49 | + <div class="stat-value" id="responseTime">0ms</div> | ||
| 50 | + <div class="stat-label">Avg Response Time</div> | ||
| 51 | + <div class="stat-trend" id="responseTrend">↓ 0%</div> | ||
| 52 | + </div> | ||
| 53 | + </section> | ||
| 54 | + | ||
| 55 | + <!-- Real-time Activity Feed --> | ||
| 56 | + <section class="activity-section"> | ||
| 57 | + <h2>Live Activity</h2> | ||
| 58 | + <div class="activity-feed" id="activityFeed"> | ||
| 59 | + <div class="activity-item"> | ||
| 60 | + <span class="activity-time">Waiting for activity...</span> | ||
| 61 | + <span class="activity-message">System ready</span> | ||
| 62 | + </div> | ||
| 63 | + </div> | ||
| 64 | + </section> | ||
| 65 | + | ||
| 66 | + <!-- Charts Section --> | ||
| 67 | + <section class="charts-section"> | ||
| 68 | + <div class="chart-container"> | ||
| 69 | + <h3>Message Volume (24h)</h3> | ||
| 70 | + <canvas id="volumeChart"></canvas> | ||
| 71 | + </div> | ||
| 72 | + | ||
| 73 | + <div class="chart-container"> | ||
| 74 | + <h3>Scammer Detection Rate</h3> | ||
| 75 | + <canvas id="detectionChart"></canvas> | ||
| 76 | + </div> | ||
| 77 | + | ||
| 78 | + <div class="chart-container"> | ||
| 79 | + <h3>Platform Distribution</h3> | ||
| 80 | + <canvas id="platformChart"></canvas> | ||
| 81 | + </div> | ||
| 82 | + | ||
| 83 | + <div class="chart-container"> | ||
| 84 | + <h3>Response Times</h3> | ||
| 85 | + <canvas id="responseChart"></canvas> | ||
| 86 | + </div> | ||
| 87 | + </section> | ||
| 88 | + | ||
| 89 | + <!-- Active Conversations --> | ||
| 90 | + <section class="conversations-section"> | ||
| 91 | + <h2>Active Conversations</h2> | ||
| 92 | + <div class="conversations-grid" id="conversationsGrid"> | ||
| 93 | + <div class="conversation-card"> | ||
| 94 | + <div class="conversation-header"> | ||
| 95 | + <span class="platform-badge whatsapp">WhatsApp</span> | ||
| 96 | + <span class="conversation-id">No active conversations</span> | ||
| 97 | + </div> | ||
| 98 | + <div class="conversation-stats"> | ||
| 99 | + <span>Messages: 0</span> | ||
| 100 | + <span>Score: 0.0</span> | ||
| 101 | + <span>Duration: 0m</span> | ||
| 102 | + </div> | ||
| 103 | + </div> | ||
| 104 | + </div> | ||
| 105 | + </section> | ||
| 106 | + | ||
| 107 | + <!-- Screenshot Evidence --> | ||
| 108 | + <section class="evidence-section"> | ||
| 109 | + <h2>Recent Evidence</h2> | ||
| 110 | + <div class="evidence-grid" id="evidenceGrid"> | ||
| 111 | + <div class="evidence-placeholder"> | ||
| 112 | + No screenshots captured yet | ||
| 113 | + </div> | ||
| 114 | + </div> | ||
| 115 | + </section> | ||
| 116 | + | ||
| 117 | + <!-- Pattern Learning --> | ||
| 118 | + <section class="patterns-section"> | ||
| 119 | + <h2>Learned Patterns</h2> | ||
| 120 | + <div class="patterns-container"> | ||
| 121 | + <div class="pattern-stats"> | ||
| 122 | + <div class="pattern-stat"> | ||
| 123 | + <span class="pattern-value" id="totalPatterns">0</span> | ||
| 124 | + <span class="pattern-label">Patterns Identified</span> | ||
| 125 | + </div> | ||
| 126 | + <div class="pattern-stat"> | ||
| 127 | + <span class="pattern-value" id="patternAccuracy">0%</span> | ||
| 128 | + <span class="pattern-label">Detection Accuracy</span> | ||
| 129 | + </div> | ||
| 130 | + </div> | ||
| 131 | + <div class="patterns-list" id="patternsList"> | ||
| 132 | + <div class="pattern-item"> | ||
| 133 | + <span class="pattern-type">No patterns learned yet</span> | ||
| 134 | + <span class="pattern-count">0 occurrences</span> | ||
| 135 | + </div> | ||
| 136 | + </div> | ||
| 137 | + </div> | ||
| 138 | + </section> | ||
| 139 | + | ||
| 140 | + <!-- Controls --> | ||
| 141 | + <section class="controls-section"> | ||
| 142 | + <h2>System Controls</h2> | ||
| 143 | + <div class="controls-grid"> | ||
| 144 | + <button class="control-btn primary" id="exportBtn"> | ||
| 145 | + Export Data | ||
| 146 | + </button> | ||
| 147 | + <button class="control-btn danger" id="emergencyStop"> | ||
| 148 | + Emergency Stop All | ||
| 149 | + </button> | ||
| 150 | + <button class="control-btn" id="clearDataBtn"> | ||
| 151 | + Clear Old Data | ||
| 152 | + </button> | ||
| 153 | + <button class="control-btn" id="syncBtn"> | ||
| 154 | + Sync Patterns | ||
| 155 | + </button> | ||
| 156 | + <button class="control-btn success" id="personalityBtn"> | ||
| 157 | + Personality Builder | ||
| 158 | + </button> | ||
| 159 | + <button class="control-btn info" id="replayBtn"> | ||
| 160 | + Conversation Replay | ||
| 161 | + </button> | ||
| 162 | + </div> | ||
| 163 | + </section> | ||
| 164 | + </main> | ||
| 165 | + | ||
| 166 | + <!-- Footer --> | ||
| 167 | + <footer class="dashboard-footer"> | ||
| 168 | + <div class="footer-content"> | ||
| 169 | + <span>LooseCannon v0.3.0</span> | ||
| 170 | + <span>Uptime: <span id="uptime">0h 0m</span></span> | ||
| 171 | + <span>Last Updated: <span id="lastUpdated">Never</span></span> | ||
| 172 | + </div> | ||
| 173 | + </footer> | ||
| 174 | + </div> | ||
| 175 | + | ||
| 176 | + <!-- Personality Builder Modal --> | ||
| 177 | + <div class="modal" id="personalityModal"> | ||
| 178 | + <div class="modal-content"> | ||
| 179 | + <div class="modal-header"> | ||
| 180 | + <h2>Personality Builder</h2> | ||
| 181 | + <button class="modal-close" id="modalClose">×</button> | ||
| 182 | + </div> | ||
| 183 | + <div class="modal-body"> | ||
| 184 | + <form id="personalityForm"> | ||
| 185 | + <div class="form-group"> | ||
| 186 | + <label>Personality Name</label> | ||
| 187 | + <input type="text" id="personalityName" placeholder="e.g., Overly Helpful"> | ||
| 188 | + </div> | ||
| 189 | + <div class="form-group"> | ||
| 190 | + <label>System Prompt</label> | ||
| 191 | + <textarea id="systemPrompt" rows="6" placeholder="Describe the personality traits and behavior..."></textarea> | ||
| 192 | + </div> | ||
| 193 | + <div class="form-group"> | ||
| 194 | + <label>Temperature (0.1 - 1.0)</label> | ||
| 195 | + <input type="range" id="temperature" min="0.1" max="1" step="0.1" value="0.8"> | ||
| 196 | + <span id="tempValue">0.8</span> | ||
| 197 | + </div> | ||
| 198 | + <div class="form-group"> | ||
| 199 | + <label>Example Responses</label> | ||
| 200 | + <textarea id="examples" rows="4" placeholder="One example per line..."></textarea> | ||
| 201 | + </div> | ||
| 202 | + <div class="form-buttons"> | ||
| 203 | + <button type="button" class="btn secondary" id="testPersonality">Test</button> | ||
| 204 | + <button type="submit" class="btn primary">Save Personality</button> | ||
| 205 | + </div> | ||
| 206 | + </form> | ||
| 207 | + <div class="test-output" id="testOutput"></div> | ||
| 208 | + </div> | ||
| 209 | + </div> | ||
| 210 | + </div> | ||
| 211 | + | ||
| 212 | + <!-- Conversation Replay Modal --> | ||
| 213 | + <div class="modal" id="replayModal"> | ||
| 214 | + <div class="modal-content"> | ||
| 215 | + <div class="modal-header"> | ||
| 216 | + <h2>Conversation Replay</h2> | ||
| 217 | + <button class="modal-close" id="replayClose">×</button> | ||
| 218 | + </div> | ||
| 219 | + <div class="modal-body"> | ||
| 220 | + <select id="conversationSelect"> | ||
| 221 | + <option>Select a conversation...</option> | ||
| 222 | + </select> | ||
| 223 | + <div class="replay-container" id="replayContainer"> | ||
| 224 | + <div class="replay-controls"> | ||
| 225 | + <button id="playBtn">▶ Play</button> | ||
| 226 | + <button id="pauseBtn">⏸ Pause</button> | ||
| 227 | + <button id="speedBtn">Speed: 1x</button> | ||
| 228 | + <input type="range" id="replayProgress" min="0" max="100" value="0"> | ||
| 229 | + </div> | ||
| 230 | + <div class="replay-messages" id="replayMessages"></div> | ||
| 231 | + <div class="replay-stats" id="replayStats"> | ||
| 232 | + <span>Messages: 0</span> | ||
| 233 | + <span>Duration: 0m</span> | ||
| 234 | + <span>Scammer Score: 0.0</span> | ||
| 235 | + </div> | ||
| 236 | + </div> | ||
| 237 | + </div> | ||
| 238 | + </div> | ||
| 239 | + </div> | ||
| 240 | + | ||
| 241 | + <script src="dashboard.js" type="module"></script> | ||
| 242 | +</body> | ||
| 243 | +</html> | ||
dashboard/styles.cssadded@@ -0,0 +1,673 @@ | |||
| 1 | +/* LooseCannon Analytics Dashboard Styles */ | ||
| 2 | + | ||
| 3 | +:root { | ||
| 4 | + --bg-primary: #0f0f0f; | ||
| 5 | + --bg-secondary: #1a1a1a; | ||
| 6 | + --bg-card: #242424; | ||
| 7 | + --text-primary: #e0e0e0; | ||
| 8 | + --text-secondary: #888; | ||
| 9 | + --accent: #667eea; | ||
| 10 | + --success: #10b981; | ||
| 11 | + --danger: #ef4444; | ||
| 12 | + --warning: #f59e0b; | ||
| 13 | + --info: #3b82f6; | ||
| 14 | + --border: #333; | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +* { | ||
| 18 | + margin: 0; | ||
| 19 | + padding: 0; | ||
| 20 | + box-sizing: border-box; | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +body { | ||
| 24 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; | ||
| 25 | + background: var(--bg-primary); | ||
| 26 | + color: var(--text-primary); | ||
| 27 | + line-height: 1.6; | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +/* Dashboard Layout */ | ||
| 31 | +.dashboard { | ||
| 32 | + min-height: 100vh; | ||
| 33 | + display: flex; | ||
| 34 | + flex-direction: column; | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +/* Header */ | ||
| 38 | +.dashboard-header { | ||
| 39 | + background: var(--bg-secondary); | ||
| 40 | + border-bottom: 1px solid var(--border); | ||
| 41 | + padding: 1.5rem 2rem; | ||
| 42 | + position: sticky; | ||
| 43 | + top: 0; | ||
| 44 | + z-index: 100; | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +.header-content { | ||
| 48 | + display: flex; | ||
| 49 | + justify-content: space-between; | ||
| 50 | + align-items: center; | ||
| 51 | + max-width: 1400px; | ||
| 52 | + margin: 0 auto; | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +.dashboard-header h1 { | ||
| 56 | + font-size: 1.8rem; | ||
| 57 | + font-weight: 600; | ||
| 58 | + background: linear-gradient(135deg, var(--accent), #764ba2); | ||
| 59 | + -webkit-background-clip: text; | ||
| 60 | + -webkit-text-fill-color: transparent; | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +.server-status { | ||
| 64 | + display: flex; | ||
| 65 | + align-items: center; | ||
| 66 | + gap: 0.5rem; | ||
| 67 | + font-size: 0.9rem; | ||
| 68 | + color: var(--text-secondary); | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +.status-indicator { | ||
| 72 | + width: 10px; | ||
| 73 | + height: 10px; | ||
| 74 | + border-radius: 50%; | ||
| 75 | + background: var(--danger); | ||
| 76 | + animation: pulse 2s infinite; | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +.status-indicator.connected { | ||
| 80 | + background: var(--success); | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +/* Main Content */ | ||
| 84 | +.dashboard-main { | ||
| 85 | + flex: 1; | ||
| 86 | + padding: 2rem; | ||
| 87 | + max-width: 1400px; | ||
| 88 | + margin: 0 auto; | ||
| 89 | + width: 100%; | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +/* Stats Grid */ | ||
| 93 | +.stats-grid { | ||
| 94 | + display: grid; | ||
| 95 | + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||
| 96 | + gap: 1.5rem; | ||
| 97 | + margin-bottom: 2rem; | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +.stat-card { | ||
| 101 | + background: var(--bg-card); | ||
| 102 | + border: 1px solid var(--border); | ||
| 103 | + border-radius: 12px; | ||
| 104 | + padding: 1.5rem; | ||
| 105 | + transition: transform 0.2s, border-color 0.2s; | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +.stat-card:hover { | ||
| 109 | + transform: translateY(-2px); | ||
| 110 | + border-color: var(--accent); | ||
| 111 | +} | ||
| 112 | + | ||
| 113 | +.stat-card.danger { | ||
| 114 | + border-color: var(--danger); | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +.stat-card.success { | ||
| 118 | + border-color: var(--success); | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +.stat-card.info { | ||
| 122 | + border-color: var(--info); | ||
| 123 | +} | ||
| 124 | + | ||
| 125 | +.stat-value { | ||
| 126 | + font-size: 2.5rem; | ||
| 127 | + font-weight: bold; | ||
| 128 | + margin-bottom: 0.5rem; | ||
| 129 | +} | ||
| 130 | + | ||
| 131 | +.stat-label { | ||
| 132 | + color: var(--text-secondary); | ||
| 133 | + font-size: 0.9rem; | ||
| 134 | + text-transform: uppercase; | ||
| 135 | + letter-spacing: 1px; | ||
| 136 | +} | ||
| 137 | + | ||
| 138 | +.stat-trend { | ||
| 139 | + margin-top: 0.5rem; | ||
| 140 | + font-size: 0.85rem; | ||
| 141 | + color: var(--success); | ||
| 142 | +} | ||
| 143 | + | ||
| 144 | +.stat-trend.negative { | ||
| 145 | + color: var(--danger); | ||
| 146 | +} | ||
| 147 | + | ||
| 148 | +.stat-indicator { | ||
| 149 | + display: flex; | ||
| 150 | + align-items: center; | ||
| 151 | + gap: 0.5rem; | ||
| 152 | + margin-top: 0.5rem; | ||
| 153 | + font-size: 0.85rem; | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +.pulse { | ||
| 157 | + width: 8px; | ||
| 158 | + height: 8px; | ||
| 159 | + background: var(--success); | ||
| 160 | + border-radius: 50%; | ||
| 161 | + animation: pulse 1.5s infinite; | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +/* Activity Feed */ | ||
| 165 | +.activity-section { | ||
| 166 | + background: var(--bg-card); | ||
| 167 | + border: 1px solid var(--border); | ||
| 168 | + border-radius: 12px; | ||
| 169 | + padding: 1.5rem; | ||
| 170 | + margin-bottom: 2rem; | ||
| 171 | +} | ||
| 172 | + | ||
| 173 | +.activity-section h2 { | ||
| 174 | + margin-bottom: 1rem; | ||
| 175 | + font-size: 1.2rem; | ||
| 176 | + color: var(--text-primary); | ||
| 177 | +} | ||
| 178 | + | ||
| 179 | +.activity-feed { | ||
| 180 | + max-height: 300px; | ||
| 181 | + overflow-y: auto; | ||
| 182 | + space-y: 0.5rem; | ||
| 183 | +} | ||
| 184 | + | ||
| 185 | +.activity-item { | ||
| 186 | + display: flex; | ||
| 187 | + gap: 1rem; | ||
| 188 | + padding: 0.75rem; | ||
| 189 | + background: var(--bg-secondary); | ||
| 190 | + border-radius: 8px; | ||
| 191 | + margin-bottom: 0.5rem; | ||
| 192 | + font-size: 0.9rem; | ||
| 193 | +} | ||
| 194 | + | ||
| 195 | +.activity-time { | ||
| 196 | + color: var(--text-secondary); | ||
| 197 | + min-width: 80px; | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +.activity-message { | ||
| 201 | + flex: 1; | ||
| 202 | +} | ||
| 203 | + | ||
| 204 | +.activity-item.scammer { | ||
| 205 | + border-left: 3px solid var(--danger); | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +.activity-item.success { | ||
| 209 | + border-left: 3px solid var(--success); | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +/* Charts Section */ | ||
| 213 | +.charts-section { | ||
| 214 | + display: grid; | ||
| 215 | + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); | ||
| 216 | + gap: 1.5rem; | ||
| 217 | + margin-bottom: 2rem; | ||
| 218 | +} | ||
| 219 | + | ||
| 220 | +.chart-container { | ||
| 221 | + background: var(--bg-card); | ||
| 222 | + border: 1px solid var(--border); | ||
| 223 | + border-radius: 12px; | ||
| 224 | + padding: 1.5rem; | ||
| 225 | +} | ||
| 226 | + | ||
| 227 | +.chart-container h3 { | ||
| 228 | + margin-bottom: 1rem; | ||
| 229 | + font-size: 1rem; | ||
| 230 | + color: var(--text-primary); | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.chart-container canvas { | ||
| 234 | + max-height: 250px; | ||
| 235 | +} | ||
| 236 | + | ||
| 237 | +/* Conversations */ | ||
| 238 | +.conversations-section { | ||
| 239 | + background: var(--bg-card); | ||
| 240 | + border: 1px solid var(--border); | ||
| 241 | + border-radius: 12px; | ||
| 242 | + padding: 1.5rem; | ||
| 243 | + margin-bottom: 2rem; | ||
| 244 | +} | ||
| 245 | + | ||
| 246 | +.conversations-section h2 { | ||
| 247 | + margin-bottom: 1rem; | ||
| 248 | + font-size: 1.2rem; | ||
| 249 | +} | ||
| 250 | + | ||
| 251 | +.conversations-grid { | ||
| 252 | + display: grid; | ||
| 253 | + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| 254 | + gap: 1rem; | ||
| 255 | +} | ||
| 256 | + | ||
| 257 | +.conversation-card { | ||
| 258 | + background: var(--bg-secondary); | ||
| 259 | + border: 1px solid var(--border); | ||
| 260 | + border-radius: 8px; | ||
| 261 | + padding: 1rem; | ||
| 262 | + transition: border-color 0.2s; | ||
| 263 | +} | ||
| 264 | + | ||
| 265 | +.conversation-card:hover { | ||
| 266 | + border-color: var(--accent); | ||
| 267 | + cursor: pointer; | ||
| 268 | +} | ||
| 269 | + | ||
| 270 | +.conversation-header { | ||
| 271 | + display: flex; | ||
| 272 | + justify-content: space-between; | ||
| 273 | + align-items: center; | ||
| 274 | + margin-bottom: 0.75rem; | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +.platform-badge { | ||
| 278 | + padding: 0.25rem 0.5rem; | ||
| 279 | + border-radius: 4px; | ||
| 280 | + font-size: 0.75rem; | ||
| 281 | + text-transform: uppercase; | ||
| 282 | + font-weight: bold; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.platform-badge.whatsapp { | ||
| 286 | + background: #25d366; | ||
| 287 | + color: white; | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +.platform-badge.telegram { | ||
| 291 | + background: #0088cc; | ||
| 292 | + color: white; | ||
| 293 | +} | ||
| 294 | + | ||
| 295 | +.platform-badge.messenger { | ||
| 296 | + background: #0084ff; | ||
| 297 | + color: white; | ||
| 298 | +} | ||
| 299 | + | ||
| 300 | +.conversation-id { | ||
| 301 | + font-size: 0.85rem; | ||
| 302 | + color: var(--text-secondary); | ||
| 303 | +} | ||
| 304 | + | ||
| 305 | +.conversation-stats { | ||
| 306 | + display: flex; | ||
| 307 | + gap: 1rem; | ||
| 308 | + font-size: 0.85rem; | ||
| 309 | + color: var(--text-secondary); | ||
| 310 | +} | ||
| 311 | + | ||
| 312 | +/* Evidence Section */ | ||
| 313 | +.evidence-section { | ||
| 314 | + background: var(--bg-card); | ||
| 315 | + border: 1px solid var(--border); | ||
| 316 | + border-radius: 12px; | ||
| 317 | + padding: 1.5rem; | ||
| 318 | + margin-bottom: 2rem; | ||
| 319 | +} | ||
| 320 | + | ||
| 321 | +.evidence-section h2 { | ||
| 322 | + margin-bottom: 1rem; | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +.evidence-grid { | ||
| 326 | + display: grid; | ||
| 327 | + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | ||
| 328 | + gap: 1rem; | ||
| 329 | +} | ||
| 330 | + | ||
| 331 | +.evidence-item { | ||
| 332 | + position: relative; | ||
| 333 | + border: 1px solid var(--border); | ||
| 334 | + border-radius: 8px; | ||
| 335 | + overflow: hidden; | ||
| 336 | + cursor: pointer; | ||
| 337 | + transition: transform 0.2s; | ||
| 338 | +} | ||
| 339 | + | ||
| 340 | +.evidence-item:hover { | ||
| 341 | + transform: scale(1.05); | ||
| 342 | +} | ||
| 343 | + | ||
| 344 | +.evidence-item img { | ||
| 345 | + width: 100%; | ||
| 346 | + height: 150px; | ||
| 347 | + object-fit: cover; | ||
| 348 | +} | ||
| 349 | + | ||
| 350 | +.evidence-meta { | ||
| 351 | + position: absolute; | ||
| 352 | + bottom: 0; | ||
| 353 | + left: 0; | ||
| 354 | + right: 0; | ||
| 355 | + background: rgba(0, 0, 0, 0.8); | ||
| 356 | + padding: 0.5rem; | ||
| 357 | + font-size: 0.75rem; | ||
| 358 | + color: white; | ||
| 359 | +} | ||
| 360 | + | ||
| 361 | +.evidence-placeholder { | ||
| 362 | + grid-column: 1 / -1; | ||
| 363 | + text-align: center; | ||
| 364 | + color: var(--text-secondary); | ||
| 365 | + padding: 2rem; | ||
| 366 | +} | ||
| 367 | + | ||
| 368 | +/* Patterns Section */ | ||
| 369 | +.patterns-section { | ||
| 370 | + background: var(--bg-card); | ||
| 371 | + border: 1px solid var(--border); | ||
| 372 | + border-radius: 12px; | ||
| 373 | + padding: 1.5rem; | ||
| 374 | + margin-bottom: 2rem; | ||
| 375 | +} | ||
| 376 | + | ||
| 377 | +.patterns-container { | ||
| 378 | + display: grid; | ||
| 379 | + grid-template-columns: 200px 1fr; | ||
| 380 | + gap: 2rem; | ||
| 381 | +} | ||
| 382 | + | ||
| 383 | +.pattern-stats { | ||
| 384 | + display: flex; | ||
| 385 | + flex-direction: column; | ||
| 386 | + gap: 1rem; | ||
| 387 | +} | ||
| 388 | + | ||
| 389 | +.pattern-stat { | ||
| 390 | + text-align: center; | ||
| 391 | + padding: 1rem; | ||
| 392 | + background: var(--bg-secondary); | ||
| 393 | + border-radius: 8px; | ||
| 394 | +} | ||
| 395 | + | ||
| 396 | +.pattern-value { | ||
| 397 | + display: block; | ||
| 398 | + font-size: 2rem; | ||
| 399 | + font-weight: bold; | ||
| 400 | + color: var(--accent); | ||
| 401 | +} | ||
| 402 | + | ||
| 403 | +.pattern-label { | ||
| 404 | + display: block; | ||
| 405 | + font-size: 0.8rem; | ||
| 406 | + color: var(--text-secondary); | ||
| 407 | + margin-top: 0.25rem; | ||
| 408 | +} | ||
| 409 | + | ||
| 410 | +.patterns-list { | ||
| 411 | + max-height: 200px; | ||
| 412 | + overflow-y: auto; | ||
| 413 | +} | ||
| 414 | + | ||
| 415 | +.pattern-item { | ||
| 416 | + display: flex; | ||
| 417 | + justify-content: space-between; | ||
| 418 | + padding: 0.75rem; | ||
| 419 | + background: var(--bg-secondary); | ||
| 420 | + border-radius: 6px; | ||
| 421 | + margin-bottom: 0.5rem; | ||
| 422 | +} | ||
| 423 | + | ||
| 424 | +.pattern-type { | ||
| 425 | + font-weight: 500; | ||
| 426 | +} | ||
| 427 | + | ||
| 428 | +.pattern-count { | ||
| 429 | + color: var(--text-secondary); | ||
| 430 | + font-size: 0.9rem; | ||
| 431 | +} | ||
| 432 | + | ||
| 433 | +/* Controls */ | ||
| 434 | +.controls-section { | ||
| 435 | + background: var(--bg-card); | ||
| 436 | + border: 1px solid var(--border); | ||
| 437 | + border-radius: 12px; | ||
| 438 | + padding: 1.5rem; | ||
| 439 | + margin-bottom: 2rem; | ||
| 440 | +} | ||
| 441 | + | ||
| 442 | +.controls-grid { | ||
| 443 | + display: grid; | ||
| 444 | + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | ||
| 445 | + gap: 1rem; | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +.control-btn { | ||
| 449 | + padding: 0.75rem 1.5rem; | ||
| 450 | + border: 1px solid var(--border); | ||
| 451 | + border-radius: 8px; | ||
| 452 | + background: var(--bg-secondary); | ||
| 453 | + color: var(--text-primary); | ||
| 454 | + font-size: 0.9rem; | ||
| 455 | + cursor: pointer; | ||
| 456 | + transition: all 0.2s; | ||
| 457 | +} | ||
| 458 | + | ||
| 459 | +.control-btn:hover { | ||
| 460 | + transform: translateY(-2px); | ||
| 461 | + border-color: var(--accent); | ||
| 462 | +} | ||
| 463 | + | ||
| 464 | +.control-btn.primary { | ||
| 465 | + background: var(--accent); | ||
| 466 | + color: white; | ||
| 467 | + border-color: var(--accent); | ||
| 468 | +} | ||
| 469 | + | ||
| 470 | +.control-btn.danger { | ||
| 471 | + background: var(--danger); | ||
| 472 | + color: white; | ||
| 473 | + border-color: var(--danger); | ||
| 474 | +} | ||
| 475 | + | ||
| 476 | +.control-btn.success { | ||
| 477 | + background: var(--success); | ||
| 478 | + color: white; | ||
| 479 | + border-color: var(--success); | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +.control-btn.info { | ||
| 483 | + background: var(--info); | ||
| 484 | + color: white; | ||
| 485 | + border-color: var(--info); | ||
| 486 | +} | ||
| 487 | + | ||
| 488 | +/* Modal */ | ||
| 489 | +.modal { | ||
| 490 | + display: none; | ||
| 491 | + position: fixed; | ||
| 492 | + top: 0; | ||
| 493 | + left: 0; | ||
| 494 | + right: 0; | ||
| 495 | + bottom: 0; | ||
| 496 | + background: rgba(0, 0, 0, 0.8); | ||
| 497 | + z-index: 1000; | ||
| 498 | + align-items: center; | ||
| 499 | + justify-content: center; | ||
| 500 | +} | ||
| 501 | + | ||
| 502 | +.modal.active { | ||
| 503 | + display: flex; | ||
| 504 | +} | ||
| 505 | + | ||
| 506 | +.modal-content { | ||
| 507 | + background: var(--bg-card); | ||
| 508 | + border: 1px solid var(--border); | ||
| 509 | + border-radius: 12px; | ||
| 510 | + width: 90%; | ||
| 511 | + max-width: 600px; | ||
| 512 | + max-height: 80vh; | ||
| 513 | + overflow: auto; | ||
| 514 | +} | ||
| 515 | + | ||
| 516 | +.modal-header { | ||
| 517 | + display: flex; | ||
| 518 | + justify-content: space-between; | ||
| 519 | + align-items: center; | ||
| 520 | + padding: 1.5rem; | ||
| 521 | + border-bottom: 1px solid var(--border); | ||
| 522 | +} | ||
| 523 | + | ||
| 524 | +.modal-close { | ||
| 525 | + background: none; | ||
| 526 | + border: none; | ||
| 527 | + color: var(--text-secondary); | ||
| 528 | + font-size: 1.5rem; | ||
| 529 | + cursor: pointer; | ||
| 530 | +} | ||
| 531 | + | ||
| 532 | +.modal-body { | ||
| 533 | + padding: 1.5rem; | ||
| 534 | +} | ||
| 535 | + | ||
| 536 | +/* Forms */ | ||
| 537 | +.form-group { | ||
| 538 | + margin-bottom: 1.5rem; | ||
| 539 | +} | ||
| 540 | + | ||
| 541 | +.form-group label { | ||
| 542 | + display: block; | ||
| 543 | + margin-bottom: 0.5rem; | ||
| 544 | + color: var(--text-primary); | ||
| 545 | + font-size: 0.9rem; | ||
| 546 | +} | ||
| 547 | + | ||
| 548 | +.form-group input, | ||
| 549 | +.form-group textarea, | ||
| 550 | +.form-group select { | ||
| 551 | + width: 100%; | ||
| 552 | + padding: 0.75rem; | ||
| 553 | + background: var(--bg-secondary); | ||
| 554 | + border: 1px solid var(--border); | ||
| 555 | + border-radius: 6px; | ||
| 556 | + color: var(--text-primary); | ||
| 557 | + font-size: 0.9rem; | ||
| 558 | +} | ||
| 559 | + | ||
| 560 | +.form-group input:focus, | ||
| 561 | +.form-group textarea:focus { | ||
| 562 | + outline: none; | ||
| 563 | + border-color: var(--accent); | ||
| 564 | +} | ||
| 565 | + | ||
| 566 | +.form-buttons { | ||
| 567 | + display: flex; | ||
| 568 | + gap: 1rem; | ||
| 569 | + justify-content: flex-end; | ||
| 570 | +} | ||
| 571 | + | ||
| 572 | +.btn { | ||
| 573 | + padding: 0.75rem 1.5rem; | ||
| 574 | + border: 1px solid var(--border); | ||
| 575 | + border-radius: 6px; | ||
| 576 | + background: var(--bg-secondary); | ||
| 577 | + color: var(--text-primary); | ||
| 578 | + cursor: pointer; | ||
| 579 | + transition: all 0.2s; | ||
| 580 | +} | ||
| 581 | + | ||
| 582 | +.btn.primary { | ||
| 583 | + background: var(--accent); | ||
| 584 | + color: white; | ||
| 585 | + border-color: var(--accent); | ||
| 586 | +} | ||
| 587 | + | ||
| 588 | +.btn.secondary { | ||
| 589 | + background: var(--bg-secondary); | ||
| 590 | + color: var(--text-primary); | ||
| 591 | +} | ||
| 592 | + | ||
| 593 | +/* Footer */ | ||
| 594 | +.dashboard-footer { | ||
| 595 | + background: var(--bg-secondary); | ||
| 596 | + border-top: 1px solid var(--border); | ||
| 597 | + padding: 1rem 2rem; | ||
| 598 | + margin-top: auto; | ||
| 599 | +} | ||
| 600 | + | ||
| 601 | +.footer-content { | ||
| 602 | + display: flex; | ||
| 603 | + justify-content: space-between; | ||
| 604 | + align-items: center; | ||
| 605 | + max-width: 1400px; | ||
| 606 | + margin: 0 auto; | ||
| 607 | + font-size: 0.85rem; | ||
| 608 | + color: var(--text-secondary); | ||
| 609 | +} | ||
| 610 | + | ||
| 611 | +/* Animations */ | ||
| 612 | +@keyframes pulse { | ||
| 613 | + 0% { | ||
| 614 | + box-shadow: 0 0 0 0 currentColor; | ||
| 615 | + opacity: 1; | ||
| 616 | + } | ||
| 617 | + 70% { | ||
| 618 | + box-shadow: 0 0 0 10px currentColor; | ||
| 619 | + opacity: 0; | ||
| 620 | + } | ||
| 621 | + 100% { | ||
| 622 | + box-shadow: 0 0 0 0 currentColor; | ||
| 623 | + opacity: 0; | ||
| 624 | + } | ||
| 625 | +} | ||
| 626 | + | ||
| 627 | +/* Scrollbar */ | ||
| 628 | +::-webkit-scrollbar { | ||
| 629 | + width: 8px; | ||
| 630 | + height: 8px; | ||
| 631 | +} | ||
| 632 | + | ||
| 633 | +::-webkit-scrollbar-track { | ||
| 634 | + background: var(--bg-secondary); | ||
| 635 | +} | ||
| 636 | + | ||
| 637 | +::-webkit-scrollbar-thumb { | ||
| 638 | + background: var(--border); | ||
| 639 | + border-radius: 4px; | ||
| 640 | +} | ||
| 641 | + | ||
| 642 | +::-webkit-scrollbar-thumb:hover { | ||
| 643 | + background: var(--accent); | ||
| 644 | +} | ||
| 645 | + | ||
| 646 | +/* Responsive */ | ||
| 647 | +@media (max-width: 768px) { | ||
| 648 | + .dashboard-main { | ||
| 649 | + padding: 1rem; | ||
| 650 | + } | ||
| 651 | + | ||
| 652 | + .stats-grid { | ||
| 653 | + grid-template-columns: 1fr; | ||
| 654 | + } | ||
| 655 | + | ||
| 656 | + .charts-section { | ||
| 657 | + grid-template-columns: 1fr; | ||
| 658 | + } | ||
| 659 | + | ||
| 660 | + .conversations-grid { | ||
| 661 | + grid-template-columns: 1fr; | ||
| 662 | + } | ||
| 663 | + | ||
| 664 | + .patterns-container { | ||
| 665 | + grid-template-columns: 1fr; | ||
| 666 | + } | ||
| 667 | + | ||
| 668 | + .footer-content { | ||
| 669 | + flex-direction: column; | ||
| 670 | + gap: 0.5rem; | ||
| 671 | + text-align: center; | ||
| 672 | + } | ||
| 673 | +} | ||
extension-chrome/background/pattern-learning.jsadded@@ -0,0 +1,505 @@ | |||
| 1 | +// Pattern Learning System for Scammer Detection | ||
| 2 | +// Uses machine learning-inspired techniques to identify and learn scammer patterns | ||
| 3 | + | ||
| 4 | +export class PatternLearning { | ||
| 5 | + constructor() { | ||
| 6 | + this.patterns = new Map(); | ||
| 7 | + this.weights = new Map(); | ||
| 8 | + this.threshold = 0.7; | ||
| 9 | + this.learningRate = 0.1; | ||
| 10 | + this.decayRate = 0.95; | ||
| 11 | + this.minConfidence = 0.6; | ||
| 12 | + this.init(); | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + async init() { | ||
| 16 | + await this.loadPatterns(); | ||
| 17 | + await this.loadWeights(); | ||
| 18 | + this.startPeriodicTraining(); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + async loadPatterns() { | ||
| 22 | + const stored = await chrome.storage.local.get('learned_patterns'); | ||
| 23 | + if (stored.learned_patterns) { | ||
| 24 | + stored.learned_patterns.forEach(pattern => { | ||
| 25 | + this.patterns.set(pattern.id, pattern); | ||
| 26 | + }); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + // Load default patterns | ||
| 30 | + this.loadDefaultPatterns(); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + loadDefaultPatterns() { | ||
| 34 | + const defaults = [ | ||
| 35 | + { | ||
| 36 | + id: 'urgent_action', | ||
| 37 | + type: 'keyword_cluster', | ||
| 38 | + features: ['urgent', 'immediate', 'act now', 'expire', 'limited time'], | ||
| 39 | + weight: 0.8, | ||
| 40 | + confidence: 0.9 | ||
| 41 | + }, | ||
| 42 | + { | ||
| 43 | + id: 'money_request', | ||
| 44 | + type: 'keyword_cluster', | ||
| 45 | + features: ['send money', 'wire transfer', 'gift card', 'payment', 'fee'], | ||
| 46 | + weight: 0.9, | ||
| 47 | + confidence: 0.95 | ||
| 48 | + }, | ||
| 49 | + { | ||
| 50 | + id: 'personal_info', | ||
| 51 | + type: 'keyword_cluster', | ||
| 52 | + features: ['social security', 'password', 'account number', 'pin', 'verification code'], | ||
| 53 | + weight: 0.85, | ||
| 54 | + confidence: 0.9 | ||
| 55 | + }, | ||
| 56 | + { | ||
| 57 | + id: 'suspicious_link', | ||
| 58 | + type: 'url_pattern', | ||
| 59 | + features: ['bit.ly', 'tinyurl', 'short.link', 'click.here'], | ||
| 60 | + weight: 0.75, | ||
| 61 | + confidence: 0.8 | ||
| 62 | + }, | ||
| 63 | + { | ||
| 64 | + id: 'impersonation', | ||
| 65 | + type: 'keyword_cluster', | ||
| 66 | + features: ['official', 'authorized', 'representative', 'department', 'agency'], | ||
| 67 | + weight: 0.7, | ||
| 68 | + confidence: 0.75 | ||
| 69 | + }, | ||
| 70 | + { | ||
| 71 | + id: 'threat_language', | ||
| 72 | + type: 'keyword_cluster', | ||
| 73 | + features: ['suspended', 'terminated', 'legal action', 'arrest', 'prosecution'], | ||
| 74 | + weight: 0.85, | ||
| 75 | + confidence: 0.85 | ||
| 76 | + } | ||
| 77 | + ]; | ||
| 78 | + | ||
| 79 | + defaults.forEach(pattern => { | ||
| 80 | + if (!this.patterns.has(pattern.id)) { | ||
| 81 | + this.patterns.set(pattern.id, pattern); | ||
| 82 | + this.weights.set(pattern.id, pattern.weight); | ||
| 83 | + } | ||
| 84 | + }); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + async loadWeights() { | ||
| 88 | + const stored = await chrome.storage.local.get('pattern_weights'); | ||
| 89 | + if (stored.pattern_weights) { | ||
| 90 | + Object.entries(stored.pattern_weights).forEach(([id, weight]) => { | ||
| 91 | + this.weights.set(id, weight); | ||
| 92 | + }); | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + async savePatterns() { | ||
| 97 | + const patterns = Array.from(this.patterns.values()); | ||
| 98 | + await chrome.storage.local.set({ learned_patterns: patterns }); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + async saveWeights() { | ||
| 102 | + const weights = Object.fromEntries(this.weights); | ||
| 103 | + await chrome.storage.local.set({ pattern_weights: weights }); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + analyzeMessage(message, metadata = {}) { | ||
| 107 | + const analysis = { | ||
| 108 | + score: 0, | ||
| 109 | + matchedPatterns: [], | ||
| 110 | + features: [], | ||
| 111 | + confidence: 0, | ||
| 112 | + recommendation: 'monitor' | ||
| 113 | + }; | ||
| 114 | + | ||
| 115 | + // Extract features from message | ||
| 116 | + const features = this.extractFeatures(message, metadata); | ||
| 117 | + analysis.features = features; | ||
| 118 | + | ||
| 119 | + // Check against learned patterns | ||
| 120 | + this.patterns.forEach((pattern, patternId) => { | ||
| 121 | + const match = this.matchPattern(features, pattern); | ||
| 122 | + if (match.score > this.minConfidence) { | ||
| 123 | + analysis.matchedPatterns.push({ | ||
| 124 | + id: patternId, | ||
| 125 | + type: pattern.type, | ||
| 126 | + score: match.score, | ||
| 127 | + weight: this.weights.get(patternId) || 0.5 | ||
| 128 | + }); | ||
| 129 | + } | ||
| 130 | + }); | ||
| 131 | + | ||
| 132 | + // Calculate overall score | ||
| 133 | + if (analysis.matchedPatterns.length > 0) { | ||
| 134 | + const weightedSum = analysis.matchedPatterns.reduce((sum, pattern) => | ||
| 135 | + sum + (pattern.score * pattern.weight), 0 | ||
| 136 | + ); | ||
| 137 | + const totalWeight = analysis.matchedPatterns.reduce((sum, pattern) => | ||
| 138 | + sum + pattern.weight, 0 | ||
| 139 | + ); | ||
| 140 | + | ||
| 141 | + analysis.score = weightedSum / totalWeight; | ||
| 142 | + analysis.confidence = this.calculateConfidence(analysis.matchedPatterns); | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + // Determine recommendation | ||
| 146 | + if (analysis.score > 0.9) { | ||
| 147 | + analysis.recommendation = 'block'; | ||
| 148 | + } else if (analysis.score > 0.7) { | ||
| 149 | + analysis.recommendation = 'warn'; | ||
| 150 | + } else if (analysis.score > 0.5) { | ||
| 151 | + analysis.recommendation = 'monitor_closely'; | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + return analysis; | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + extractFeatures(message, metadata) { | ||
| 158 | + const features = { | ||
| 159 | + keywords: [], | ||
| 160 | + urls: [], | ||
| 161 | + patterns: [], | ||
| 162 | + metrics: {}, | ||
| 163 | + behavioral: [] | ||
| 164 | + }; | ||
| 165 | + | ||
| 166 | + const text = message.content || message.text || ''; | ||
| 167 | + const lowerText = text.toLowerCase(); | ||
| 168 | + | ||
| 169 | + // Extract keywords | ||
| 170 | + features.keywords = this.extractKeywords(lowerText); | ||
| 171 | + | ||
| 172 | + // Extract URLs | ||
| 173 | + features.urls = this.extractUrls(text); | ||
| 174 | + | ||
| 175 | + // Extract patterns | ||
| 176 | + features.patterns = this.extractPatterns(text); | ||
| 177 | + | ||
| 178 | + // Calculate metrics | ||
| 179 | + features.metrics = { | ||
| 180 | + length: text.length, | ||
| 181 | + wordCount: text.split(/\s+/).length, | ||
| 182 | + uppercaseRatio: (text.match(/[A-Z]/g) || []).length / text.length, | ||
| 183 | + punctuationCount: (text.match(/[!?]/g) || []).length, | ||
| 184 | + numberCount: (text.match(/\d+/g) || []).length, | ||
| 185 | + dollarSignCount: (text.match(/\$/g) || []).length | ||
| 186 | + }; | ||
| 187 | + | ||
| 188 | + // Behavioral features | ||
| 189 | + if (metadata.responseTime) { | ||
| 190 | + features.behavioral.push({ | ||
| 191 | + type: 'response_speed', | ||
| 192 | + value: metadata.responseTime < 1000 ? 'instant' : 'normal' | ||
| 193 | + }); | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + if (metadata.messageCount) { | ||
| 197 | + features.behavioral.push({ | ||
| 198 | + type: 'message_frequency', | ||
| 199 | + value: metadata.messageCount > 10 ? 'high' : 'normal' | ||
| 200 | + }); | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + return features; | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + extractKeywords(text) { | ||
| 207 | + const keywords = []; | ||
| 208 | + const commonScamWords = [ | ||
| 209 | + 'urgent', 'verify', 'suspended', 'confirm', 'prize', 'winner', | ||
| 210 | + 'congratulations', 'claim', 'refund', 'irs', 'tax', 'arrest', | ||
| 211 | + 'legal', 'bitcoin', 'investment', 'guaranteed', 'risk free' | ||
| 212 | + ]; | ||
| 213 | + | ||
| 214 | + commonScamWords.forEach(word => { | ||
| 215 | + if (text.includes(word)) { | ||
| 216 | + keywords.push(word); | ||
| 217 | + } | ||
| 218 | + }); | ||
| 219 | + | ||
| 220 | + return keywords; | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + extractUrls(text) { | ||
| 224 | + const urlRegex = /(https?:\/\/[^\s]+)/g; | ||
| 225 | + const urls = text.match(urlRegex) || []; | ||
| 226 | + | ||
| 227 | + return urls.map(url => ({ | ||
| 228 | + url, | ||
| 229 | + shortened: this.isShortened(url), | ||
| 230 | + suspicious: this.isSuspiciousUrl(url) | ||
| 231 | + })); | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + isShortened(url) { | ||
| 235 | + const shorteners = ['bit.ly', 'tinyurl.com', 'short.link', 'ow.ly', 'goo.gl']; | ||
| 236 | + return shorteners.some(shortener => url.includes(shortener)); | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + isSuspiciousUrl(url) { | ||
| 240 | + // Check for typosquatting and suspicious patterns | ||
| 241 | + const suspicious = [ | ||
| 242 | + 'amaz0n', 'payp4l', 'mircosoft', 'goggle', | ||
| 243 | + 'faceb00k', 'app1e', 'netf1ix' | ||
| 244 | + ]; | ||
| 245 | + | ||
| 246 | + return suspicious.some(pattern => url.toLowerCase().includes(pattern)); | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + extractPatterns(text) { | ||
| 250 | + const patterns = []; | ||
| 251 | + | ||
| 252 | + // Phone number pattern | ||
| 253 | + if (/[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,5}[-\s\.]?[0-9]{1,5}/.test(text)) { | ||
| 254 | + patterns.push('phone_number'); | ||
| 255 | + } | ||
| 256 | + | ||
| 257 | + // Email pattern | ||
| 258 | + if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(text)) { | ||
| 259 | + patterns.push('email_address'); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + // Money amount pattern | ||
| 263 | + if (/\$[\d,]+(\.\d{2})?/.test(text)) { | ||
| 264 | + patterns.push('money_amount'); | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + // Verification code pattern | ||
| 268 | + if (/\b\d{4,6}\b/.test(text) && text.includes('code')) { | ||
| 269 | + patterns.push('verification_code'); | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + return patterns; | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + matchPattern(features, pattern) { | ||
| 276 | + let score = 0; | ||
| 277 | + let matches = 0; | ||
| 278 | + | ||
| 279 | + if (pattern.type === 'keyword_cluster') { | ||
| 280 | + // Check how many keywords from the pattern are in the message | ||
| 281 | + pattern.features.forEach(keyword => { | ||
| 282 | + if (features.keywords.includes(keyword.toLowerCase())) { | ||
| 283 | + matches++; | ||
| 284 | + } | ||
| 285 | + }); | ||
| 286 | + | ||
| 287 | + score = matches / pattern.features.length; | ||
| 288 | + } else if (pattern.type === 'url_pattern') { | ||
| 289 | + // Check URLs | ||
| 290 | + features.urls.forEach(urlInfo => { | ||
| 291 | + pattern.features.forEach(urlPattern => { | ||
| 292 | + if (urlInfo.url.includes(urlPattern)) { | ||
| 293 | + matches++; | ||
| 294 | + } | ||
| 295 | + }); | ||
| 296 | + }); | ||
| 297 | + | ||
| 298 | + score = matches > 0 ? 1 : 0; | ||
| 299 | + } else if (pattern.type === 'behavioral') { | ||
| 300 | + // Check behavioral patterns | ||
| 301 | + features.behavioral.forEach(behavior => { | ||
| 302 | + if (pattern.features.includes(behavior.type)) { | ||
| 303 | + matches++; | ||
| 304 | + } | ||
| 305 | + }); | ||
| 306 | + | ||
| 307 | + score = matches / pattern.features.length; | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + return { score, matches }; | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + calculateConfidence(matchedPatterns) { | ||
| 314 | + if (matchedPatterns.length === 0) return 0; | ||
| 315 | + | ||
| 316 | + // Confidence increases with more pattern matches | ||
| 317 | + const baseConfidence = matchedPatterns.reduce((sum, p) => sum + p.score, 0) / matchedPatterns.length; | ||
| 318 | + const diversityBonus = Math.min(matchedPatterns.length * 0.1, 0.3); | ||
| 319 | + | ||
| 320 | + return Math.min(baseConfidence + diversityBonus, 1); | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + async addPattern(messageData, scammerScore) { | ||
| 324 | + const features = this.extractFeatures(messageData); | ||
| 325 | + | ||
| 326 | + // Only learn from high-confidence scammer messages | ||
| 327 | + if (scammerScore < this.threshold) return; | ||
| 328 | + | ||
| 329 | + // Update weights for matched patterns (reinforcement) | ||
| 330 | + const analysis = this.analyzeMessage(messageData); | ||
| 331 | + analysis.matchedPatterns.forEach(pattern => { | ||
| 332 | + const currentWeight = this.weights.get(pattern.id) || 0.5; | ||
| 333 | + const newWeight = currentWeight + (this.learningRate * (scammerScore - currentWeight)); | ||
| 334 | + this.weights.set(pattern.id, Math.min(newWeight, 1)); | ||
| 335 | + }); | ||
| 336 | + | ||
| 337 | + // Check if this represents a new pattern | ||
| 338 | + const novelty = this.calculateNovelty(features); | ||
| 339 | + if (novelty > 0.3) { | ||
| 340 | + await this.createNewPattern(features, scammerScore); | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + // Save updated weights | ||
| 344 | + await this.saveWeights(); | ||
| 345 | + | ||
| 346 | + // Notify dashboard | ||
| 347 | + this.notifyLearning(features, scammerScore); | ||
| 348 | + } | ||
| 349 | + | ||
| 350 | + calculateNovelty(features) { | ||
| 351 | + // Check how different these features are from existing patterns | ||
| 352 | + let maxSimilarity = 0; | ||
| 353 | + | ||
| 354 | + this.patterns.forEach(pattern => { | ||
| 355 | + const match = this.matchPattern(features, pattern); | ||
| 356 | + maxSimilarity = Math.max(maxSimilarity, match.score); | ||
| 357 | + }); | ||
| 358 | + | ||
| 359 | + return 1 - maxSimilarity; | ||
| 360 | + } | ||
| 361 | + | ||
| 362 | + async createNewPattern(features, confidence) { | ||
| 363 | + const id = `learned_${Date.now()}`; | ||
| 364 | + | ||
| 365 | + const newPattern = { | ||
| 366 | + id, | ||
| 367 | + type: 'learned', | ||
| 368 | + features: { | ||
| 369 | + keywords: features.keywords.slice(0, 10), | ||
| 370 | + patterns: features.patterns, | ||
| 371 | + metrics: features.metrics | ||
| 372 | + }, | ||
| 373 | + weight: confidence * 0.7, // Start with lower weight | ||
| 374 | + confidence, | ||
| 375 | + learnedAt: new Date().toISOString(), | ||
| 376 | + occurrences: 1 | ||
| 377 | + }; | ||
| 378 | + | ||
| 379 | + this.patterns.set(id, newPattern); | ||
| 380 | + this.weights.set(id, newPattern.weight); | ||
| 381 | + | ||
| 382 | + await this.savePatterns(); | ||
| 383 | + | ||
| 384 | + console.log('[PatternLearning] New pattern learned:', id); | ||
| 385 | + | ||
| 386 | + return newPattern; | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + async updateFromServer(serverPatterns) { | ||
| 390 | + let updated = 0; | ||
| 391 | + | ||
| 392 | + serverPatterns.forEach(serverPattern => { | ||
| 393 | + const existing = this.patterns.get(serverPattern.id); | ||
| 394 | + | ||
| 395 | + if (!existing || serverPattern.confidence > existing.confidence) { | ||
| 396 | + this.patterns.set(serverPattern.id, serverPattern); | ||
| 397 | + this.weights.set(serverPattern.id, serverPattern.weight); | ||
| 398 | + updated++; | ||
| 399 | + } | ||
| 400 | + }); | ||
| 401 | + | ||
| 402 | + if (updated > 0) { | ||
| 403 | + await this.savePatterns(); | ||
| 404 | + await this.saveWeights(); | ||
| 405 | + console.log(`[PatternLearning] Updated ${updated} patterns from server`); | ||
| 406 | + } | ||
| 407 | + | ||
| 408 | + return updated; | ||
| 409 | + } | ||
| 410 | + | ||
| 411 | + async getPatterns() { | ||
| 412 | + return Array.from(this.patterns.values()).map(pattern => ({ | ||
| 413 | + ...pattern, | ||
| 414 | + weight: this.weights.get(pattern.id) || 0.5, | ||
| 415 | + effectiveness: this.calculateEffectiveness(pattern.id) | ||
| 416 | + })); | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + calculateEffectiveness(patternId) { | ||
| 420 | + // Track how effective each pattern is at detecting scammers | ||
| 421 | + // This would be based on true/false positive rates in production | ||
| 422 | + const weight = this.weights.get(patternId) || 0.5; | ||
| 423 | + const pattern = this.patterns.get(patternId); | ||
| 424 | + | ||
| 425 | + if (!pattern) return 0; | ||
| 426 | + | ||
| 427 | + // Simple effectiveness score based on weight and confidence | ||
| 428 | + return (weight * 0.7 + (pattern.confidence || 0.5) * 0.3); | ||
| 429 | + } | ||
| 430 | + | ||
| 431 | + startPeriodicTraining() { | ||
| 432 | + // Decay weights periodically to adapt to changing patterns | ||
| 433 | + setInterval(() => { | ||
| 434 | + this.decayWeights(); | ||
| 435 | + }, 6 * 60 * 60 * 1000); // Every 6 hours | ||
| 436 | + } | ||
| 437 | + | ||
| 438 | + async decayWeights() { | ||
| 439 | + let decayed = 0; | ||
| 440 | + | ||
| 441 | + this.weights.forEach((weight, patternId) => { | ||
| 442 | + // Don't decay core patterns below minimum | ||
| 443 | + const pattern = this.patterns.get(patternId); | ||
| 444 | + const minWeight = pattern && pattern.type !== 'learned' ? 0.5 : 0.1; | ||
| 445 | + | ||
| 446 | + const newWeight = Math.max(weight * this.decayRate, minWeight); | ||
| 447 | + if (newWeight !== weight) { | ||
| 448 | + this.weights.set(patternId, newWeight); | ||
| 449 | + decayed++; | ||
| 450 | + } | ||
| 451 | + }); | ||
| 452 | + | ||
| 453 | + if (decayed > 0) { | ||
| 454 | + await this.saveWeights(); | ||
| 455 | + console.log(`[PatternLearning] Decayed ${decayed} pattern weights`); | ||
| 456 | + } | ||
| 457 | + } | ||
| 458 | + | ||
| 459 | + async exportPatterns() { | ||
| 460 | + const patterns = await this.getPatterns(); | ||
| 461 | + | ||
| 462 | + return { | ||
| 463 | + version: '1.0', | ||
| 464 | + timestamp: new Date().toISOString(), | ||
| 465 | + patterns: patterns.sort((a, b) => b.effectiveness - a.effectiveness), | ||
| 466 | + statistics: { | ||
| 467 | + total: patterns.length, | ||
| 468 | + learned: patterns.filter(p => p.type === 'learned').length, | ||
| 469 | + averageConfidence: patterns.reduce((sum, p) => sum + (p.confidence || 0), 0) / patterns.length | ||
| 470 | + } | ||
| 471 | + }; | ||
| 472 | + } | ||
| 473 | + | ||
| 474 | + notifyLearning(features, score) { | ||
| 475 | + // Notify dashboard about learning event | ||
| 476 | + chrome.runtime.sendMessage({ | ||
| 477 | + type: 'DASHBOARD_UPDATE', | ||
| 478 | + data: { | ||
| 479 | + event: 'pattern_learned', | ||
| 480 | + features: features.keywords.slice(0, 5), | ||
| 481 | + score, | ||
| 482 | + timestamp: new Date().toISOString() | ||
| 483 | + } | ||
| 484 | + }).catch(() => { | ||
| 485 | + // Dashboard might not be open | ||
| 486 | + }); | ||
| 487 | + } | ||
| 488 | + | ||
| 489 | + async resetLearning() { | ||
| 490 | + // Reset to default patterns only | ||
| 491 | + this.patterns.clear(); | ||
| 492 | + this.weights.clear(); | ||
| 493 | + this.loadDefaultPatterns(); | ||
| 494 | + | ||
| 495 | + await this.savePatterns(); | ||
| 496 | + await this.saveWeights(); | ||
| 497 | + | ||
| 498 | + console.log('[PatternLearning] Reset to default patterns'); | ||
| 499 | + } | ||
| 500 | +} | ||
| 501 | + | ||
| 502 | +// Export for use in service worker | ||
| 503 | +if (typeof module !== 'undefined' && module.exports) { | ||
| 504 | + module.exports = PatternLearning; | ||
| 505 | +} | ||
extension-chrome/background/screenshot-manager.jsadded@@ -0,0 +1,408 @@ | |||
| 1 | +// Screenshot Manager for Evidence Collection | ||
| 2 | +// Captures and stores screenshots when scammers are detected | ||
| 3 | + | ||
| 4 | +export class ScreenshotManager { | ||
| 5 | + constructor() { | ||
| 6 | + this.storage = 'screenshots'; | ||
| 7 | + this.maxScreenshots = 100; | ||
| 8 | + this.compressionQuality = 0.8; | ||
| 9 | + this.init(); | ||
| 10 | + } | ||
| 11 | + | ||
| 12 | + async init() { | ||
| 13 | + // Create storage structure if doesn't exist | ||
| 14 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 15 | + if (!stored[this.storage]) { | ||
| 16 | + await chrome.storage.local.set({ [this.storage]: [] }); | ||
| 17 | + } | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + async capture(tab, metadata = {}) { | ||
| 21 | + try { | ||
| 22 | + // Capture visible tab | ||
| 23 | + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { | ||
| 24 | + format: 'jpeg', | ||
| 25 | + quality: Math.round(this.compressionQuality * 100) | ||
| 26 | + }); | ||
| 27 | + | ||
| 28 | + // Generate unique ID | ||
| 29 | + const id = `screenshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | ||
| 30 | + | ||
| 31 | + // Create screenshot object | ||
| 32 | + const screenshot = { | ||
| 33 | + id, | ||
| 34 | + timestamp: new Date().toISOString(), | ||
| 35 | + url: tab.url, | ||
| 36 | + title: tab.title, | ||
| 37 | + platform: this.detectPlatform(tab.url), | ||
| 38 | + dataUrl: await this.compressImage(dataUrl), | ||
| 39 | + thumbnail: await this.createThumbnail(dataUrl), | ||
| 40 | + metadata: { | ||
| 41 | + ...metadata, | ||
| 42 | + tabId: tab.id, | ||
| 43 | + windowId: tab.windowId, | ||
| 44 | + incognito: tab.incognito | ||
| 45 | + } | ||
| 46 | + }; | ||
| 47 | + | ||
| 48 | + // Store screenshot | ||
| 49 | + await this.store(screenshot); | ||
| 50 | + | ||
| 51 | + // Notify dashboard | ||
| 52 | + this.notifyDashboard(screenshot); | ||
| 53 | + | ||
| 54 | + return { | ||
| 55 | + success: true, | ||
| 56 | + id: screenshot.id, | ||
| 57 | + size: this.calculateSize(screenshot.dataUrl) | ||
| 58 | + }; | ||
| 59 | + } catch (error) { | ||
| 60 | + console.error('[ScreenshotManager] Capture error:', error); | ||
| 61 | + return { | ||
| 62 | + success: false, | ||
| 63 | + error: error.message | ||
| 64 | + }; | ||
| 65 | + } | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + detectPlatform(url) { | ||
| 69 | + if (url.includes('whatsapp.com')) return 'whatsapp'; | ||
| 70 | + if (url.includes('telegram.org')) return 'telegram'; | ||
| 71 | + if (url.includes('messenger.com')) return 'messenger'; | ||
| 72 | + return 'unknown'; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + async compressImage(dataUrl) { | ||
| 76 | + return new Promise((resolve) => { | ||
| 77 | + const img = new Image(); | ||
| 78 | + img.onload = () => { | ||
| 79 | + const canvas = document.createElement('canvas'); | ||
| 80 | + const ctx = canvas.getContext('2d'); | ||
| 81 | + | ||
| 82 | + // Set max dimensions | ||
| 83 | + const maxWidth = 1920; | ||
| 84 | + const maxHeight = 1080; | ||
| 85 | + | ||
| 86 | + let width = img.width; | ||
| 87 | + let height = img.height; | ||
| 88 | + | ||
| 89 | + // Calculate new dimensions | ||
| 90 | + if (width > maxWidth || height > maxHeight) { | ||
| 91 | + const ratio = Math.min(maxWidth / width, maxHeight / height); | ||
| 92 | + width *= ratio; | ||
| 93 | + height *= ratio; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + canvas.width = width; | ||
| 97 | + canvas.height = height; | ||
| 98 | + | ||
| 99 | + // Draw and compress | ||
| 100 | + ctx.drawImage(img, 0, 0, width, height); | ||
| 101 | + resolve(canvas.toDataURL('image/jpeg', this.compressionQuality)); | ||
| 102 | + }; | ||
| 103 | + | ||
| 104 | + img.src = dataUrl; | ||
| 105 | + }); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + async createThumbnail(dataUrl) { | ||
| 109 | + return new Promise((resolve) => { | ||
| 110 | + const img = new Image(); | ||
| 111 | + img.onload = () => { | ||
| 112 | + const canvas = document.createElement('canvas'); | ||
| 113 | + const ctx = canvas.getContext('2d'); | ||
| 114 | + | ||
| 115 | + // Thumbnail dimensions | ||
| 116 | + const thumbWidth = 320; | ||
| 117 | + const thumbHeight = 180; | ||
| 118 | + | ||
| 119 | + canvas.width = thumbWidth; | ||
| 120 | + canvas.height = thumbHeight; | ||
| 121 | + | ||
| 122 | + // Draw thumbnail | ||
| 123 | + ctx.drawImage(img, 0, 0, thumbWidth, thumbHeight); | ||
| 124 | + resolve(canvas.toDataURL('image/jpeg', 0.6)); | ||
| 125 | + }; | ||
| 126 | + | ||
| 127 | + img.src = dataUrl; | ||
| 128 | + }); | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + calculateSize(dataUrl) { | ||
| 132 | + // Estimate size in bytes | ||
| 133 | + const base64Length = dataUrl.length - 'data:image/jpeg;base64,'.length; | ||
| 134 | + const sizeInBytes = base64Length * 0.75; | ||
| 135 | + return Math.round(sizeInBytes / 1024); // Return in KB | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + async store(screenshot) { | ||
| 139 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 140 | + let screenshots = stored[this.storage] || []; | ||
| 141 | + | ||
| 142 | + // Add new screenshot | ||
| 143 | + screenshots.unshift(screenshot); | ||
| 144 | + | ||
| 145 | + // Enforce max limit | ||
| 146 | + if (screenshots.length > this.maxScreenshots) { | ||
| 147 | + screenshots = screenshots.slice(0, this.maxScreenshots); | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + // Update storage | ||
| 151 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | ||
| 152 | + | ||
| 153 | + // Also store metadata separately for quick access | ||
| 154 | + await this.storeMetadata(screenshot); | ||
| 155 | + | ||
| 156 | + return screenshot; | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + async storeMetadata(screenshot) { | ||
| 160 | + const metaKey = `${this.storage}_metadata`; | ||
| 161 | + const stored = await chrome.storage.local.get(metaKey); | ||
| 162 | + let metadata = stored[metaKey] || []; | ||
| 163 | + | ||
| 164 | + metadata.unshift({ | ||
| 165 | + id: screenshot.id, | ||
| 166 | + timestamp: screenshot.timestamp, | ||
| 167 | + platform: screenshot.platform, | ||
| 168 | + url: screenshot.url, | ||
| 169 | + scammerScore: screenshot.metadata.scammerScore || 0 | ||
| 170 | + }); | ||
| 171 | + | ||
| 172 | + // Keep only last 500 metadata entries | ||
| 173 | + if (metadata.length > 500) { | ||
| 174 | + metadata = metadata.slice(0, 500); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + await chrome.storage.local.set({ [metaKey]: metadata }); | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + async get(id) { | ||
| 181 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 182 | + const screenshots = stored[this.storage] || []; | ||
| 183 | + return screenshots.find(s => s.id === id); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + async getAll(options = {}) { | ||
| 187 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 188 | + let screenshots = stored[this.storage] || []; | ||
| 189 | + | ||
| 190 | + // Apply filters | ||
| 191 | + if (options.platform) { | ||
| 192 | + screenshots = screenshots.filter(s => s.platform === options.platform); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + if (options.startDate) { | ||
| 196 | + screenshots = screenshots.filter(s => | ||
| 197 | + new Date(s.timestamp) >= new Date(options.startDate) | ||
| 198 | + ); | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + if (options.endDate) { | ||
| 202 | + screenshots = screenshots.filter(s => | ||
| 203 | + new Date(s.timestamp) <= new Date(options.endDate) | ||
| 204 | + ); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + if (options.limit) { | ||
| 208 | + screenshots = screenshots.slice(0, options.limit); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + return screenshots; | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + async getMetadata() { | ||
| 215 | + const metaKey = `${this.storage}_metadata`; | ||
| 216 | + const stored = await chrome.storage.local.get(metaKey); | ||
| 217 | + return stored[metaKey] || []; | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + async delete(id) { | ||
| 221 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 222 | + let screenshots = stored[this.storage] || []; | ||
| 223 | + | ||
| 224 | + screenshots = screenshots.filter(s => s.id !== id); | ||
| 225 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | ||
| 226 | + | ||
| 227 | + // Also remove from metadata | ||
| 228 | + const metaKey = `${this.storage}_metadata`; | ||
| 229 | + const metaStored = await chrome.storage.local.get(metaKey); | ||
| 230 | + let metadata = metaStored[metaKey] || []; | ||
| 231 | + metadata = metadata.filter(m => m.id !== id); | ||
| 232 | + await chrome.storage.local.set({ [metaKey]: metadata }); | ||
| 233 | + | ||
| 234 | + return { success: true }; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + async cleanup(maxAge = 24 * 60 * 60 * 1000) { | ||
| 238 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 239 | + let screenshots = stored[this.storage] || []; | ||
| 240 | + | ||
| 241 | + const cutoff = Date.now() - maxAge; | ||
| 242 | + const before = screenshots.length; | ||
| 243 | + | ||
| 244 | + screenshots = screenshots.filter(s => | ||
| 245 | + new Date(s.timestamp).getTime() > cutoff | ||
| 246 | + ); | ||
| 247 | + | ||
| 248 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | ||
| 249 | + | ||
| 250 | + const deleted = before - screenshots.length; | ||
| 251 | + console.log(`[ScreenshotManager] Cleaned up ${deleted} old screenshots`); | ||
| 252 | + | ||
| 253 | + return { deleted }; | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + async export(ids = null) { | ||
| 257 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 258 | + let screenshots = stored[this.storage] || []; | ||
| 259 | + | ||
| 260 | + if (ids) { | ||
| 261 | + screenshots = screenshots.filter(s => ids.includes(s.id)); | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + // Create export data | ||
| 265 | + const exportData = { | ||
| 266 | + version: '1.0', | ||
| 267 | + timestamp: new Date().toISOString(), | ||
| 268 | + count: screenshots.length, | ||
| 269 | + screenshots: screenshots.map(s => ({ | ||
| 270 | + id: s.id, | ||
| 271 | + timestamp: s.timestamp, | ||
| 272 | + platform: s.platform, | ||
| 273 | + url: s.url, | ||
| 274 | + metadata: s.metadata, | ||
| 275 | + thumbnail: s.thumbnail // Include thumbnail only | ||
| 276 | + })) | ||
| 277 | + }; | ||
| 278 | + | ||
| 279 | + return exportData; | ||
| 280 | + } | ||
| 281 | + | ||
| 282 | + async getStatistics() { | ||
| 283 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 284 | + const screenshots = stored[this.storage] || []; | ||
| 285 | + | ||
| 286 | + const stats = { | ||
| 287 | + total: screenshots.length, | ||
| 288 | + byPlatform: {}, | ||
| 289 | + byDay: {}, | ||
| 290 | + totalSize: 0, | ||
| 291 | + oldestTimestamp: null, | ||
| 292 | + newestTimestamp: null | ||
| 293 | + }; | ||
| 294 | + | ||
| 295 | + screenshots.forEach(s => { | ||
| 296 | + // By platform | ||
| 297 | + stats.byPlatform[s.platform] = (stats.byPlatform[s.platform] || 0) + 1; | ||
| 298 | + | ||
| 299 | + // By day | ||
| 300 | + const day = new Date(s.timestamp).toDateString(); | ||
| 301 | + stats.byDay[day] = (stats.byDay[day] || 0) + 1; | ||
| 302 | + | ||
| 303 | + // Size | ||
| 304 | + stats.totalSize += this.calculateSize(s.dataUrl); | ||
| 305 | + | ||
| 306 | + // Timestamps | ||
| 307 | + const timestamp = new Date(s.timestamp).getTime(); | ||
| 308 | + if (!stats.oldestTimestamp || timestamp < stats.oldestTimestamp) { | ||
| 309 | + stats.oldestTimestamp = s.timestamp; | ||
| 310 | + } | ||
| 311 | + if (!stats.newestTimestamp || timestamp > stats.newestTimestamp) { | ||
| 312 | + stats.newestTimestamp = s.timestamp; | ||
| 313 | + } | ||
| 314 | + }); | ||
| 315 | + | ||
| 316 | + return stats; | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + notifyDashboard(screenshot) { | ||
| 320 | + // Send to dashboard if open | ||
| 321 | + chrome.runtime.sendMessage({ | ||
| 322 | + type: 'DASHBOARD_UPDATE', | ||
| 323 | + data: { | ||
| 324 | + event: 'screenshot_captured', | ||
| 325 | + screenshot: { | ||
| 326 | + id: screenshot.id, | ||
| 327 | + timestamp: screenshot.timestamp, | ||
| 328 | + platform: screenshot.platform, | ||
| 329 | + thumbnail: screenshot.thumbnail | ||
| 330 | + } | ||
| 331 | + } | ||
| 332 | + }).catch(() => { | ||
| 333 | + // Dashboard might not be open | ||
| 334 | + }); | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + async annotate(id, annotations) { | ||
| 338 | + const screenshot = await this.get(id); | ||
| 339 | + if (!screenshot) { | ||
| 340 | + throw new Error('Screenshot not found'); | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + // Add annotations | ||
| 344 | + screenshot.annotations = { | ||
| 345 | + ...screenshot.annotations, | ||
| 346 | + ...annotations, | ||
| 347 | + updatedAt: new Date().toISOString() | ||
| 348 | + }; | ||
| 349 | + | ||
| 350 | + // Update storage | ||
| 351 | + const stored = await chrome.storage.local.get(this.storage); | ||
| 352 | + let screenshots = stored[this.storage] || []; | ||
| 353 | + | ||
| 354 | + const index = screenshots.findIndex(s => s.id === id); | ||
| 355 | + if (index !== -1) { | ||
| 356 | + screenshots[index] = screenshot; | ||
| 357 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | ||
| 358 | + } | ||
| 359 | + | ||
| 360 | + return screenshot; | ||
| 361 | + } | ||
| 362 | + | ||
| 363 | + async createEvidence Report(screenshotIds, conversationId) { | ||
| 364 | + const screenshots = await this.getAll(); | ||
| 365 | + const selected = screenshots.filter(s => screenshotIds.includes(s.id)); | ||
| 366 | + | ||
| 367 | + const report = { | ||
| 368 | + id: `report_${Date.now()}`, | ||
| 369 | + timestamp: new Date().toISOString(), | ||
| 370 | + conversationId, | ||
| 371 | + screenshots: selected.map(s => ({ | ||
| 372 | + id: s.id, | ||
| 373 | + timestamp: s.timestamp, | ||
| 374 | + platform: s.platform, | ||
| 375 | + url: s.url, | ||
| 376 | + annotations: s.annotations | ||
| 377 | + })), | ||
| 378 | + summary: { | ||
| 379 | + totalScreenshots: selected.length, | ||
| 380 | + platforms: [...new Set(selected.map(s => s.platform))], | ||
| 381 | + timeRange: { | ||
| 382 | + start: selected[selected.length - 1]?.timestamp, | ||
| 383 | + end: selected[0]?.timestamp | ||
| 384 | + } | ||
| 385 | + } | ||
| 386 | + }; | ||
| 387 | + | ||
| 388 | + // Store report | ||
| 389 | + const reportKey = 'evidence_reports'; | ||
| 390 | + const stored = await chrome.storage.local.get(reportKey); | ||
| 391 | + let reports = stored[reportKey] || []; | ||
| 392 | + reports.unshift(report); | ||
| 393 | + | ||
| 394 | + // Keep only last 50 reports | ||
| 395 | + if (reports.length > 50) { | ||
| 396 | + reports = reports.slice(0, 50); | ||
| 397 | + } | ||
| 398 | + | ||
| 399 | + await chrome.storage.local.set({ [reportKey]: reports }); | ||
| 400 | + | ||
| 401 | + return report; | ||
| 402 | + } | ||
| 403 | +} | ||
| 404 | + | ||
| 405 | +// Export for use in service worker | ||
| 406 | +if (typeof module !== 'undefined' && module.exports) { | ||
| 407 | + module.exports = ScreenshotManager; | ||
| 408 | +} | ||
extension-chrome/background/service-worker.jsadded@@ -0,0 +1,393 @@ | |||
| 1 | +// Chrome Service Worker for LooseCannon (Manifest V3) | ||
| 2 | +console.log('[LooseCannon] Chrome Service Worker initialized'); | ||
| 3 | + | ||
| 4 | +// Import modules | ||
| 5 | +import { UnifiedMessageHandler } from './unified-handler-v3.js'; | ||
| 6 | +import { ScreenshotManager } from './screenshot-manager.js'; | ||
| 7 | +import { PatternLearning } from './pattern-learning.js'; | ||
| 8 | + | ||
| 9 | +class LooseCannonServiceWorker { | ||
| 10 | + constructor() { | ||
| 11 | + this.messageHandler = new UnifiedMessageHandler(); | ||
| 12 | + this.screenshotManager = new ScreenshotManager(); | ||
| 13 | + this.patternLearning = new PatternLearning(); | ||
| 14 | + this.serverUrl = 'http://localhost:8765'; | ||
| 15 | + this.analytics = { | ||
| 16 | + sessionsStarted: 0, | ||
| 17 | + messagesProcessed: 0, | ||
| 18 | + scammersDetected: 0, | ||
| 19 | + screenshotsCaptured: 0 | ||
| 20 | + }; | ||
| 21 | + this.init(); | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + init() { | ||
| 25 | + this.setupListeners(); | ||
| 26 | + this.setupAlarms(); | ||
| 27 | + this.connectToServer(); | ||
| 28 | + this.loadAnalytics(); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + setupListeners() { | ||
| 32 | + // Message listener for content scripts and popup | ||
| 33 | + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||
| 34 | + console.log('[ServiceWorker] Received message:', message.type); | ||
| 35 | + | ||
| 36 | + // Handle async operations properly in Manifest V3 | ||
| 37 | + this.handleMessage(message, sender).then(sendResponse); | ||
| 38 | + return true; // Keep channel open for async response | ||
| 39 | + }); | ||
| 40 | + | ||
| 41 | + // Tab events for tracking | ||
| 42 | + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { | ||
| 43 | + if (changeInfo.status === 'complete') { | ||
| 44 | + this.checkForSupportedSite(tab); | ||
| 45 | + } | ||
| 46 | + }); | ||
| 47 | + | ||
| 48 | + // Extension install/update events | ||
| 49 | + chrome.runtime.onInstalled.addListener((details) => { | ||
| 50 | + if (details.reason === 'install') { | ||
| 51 | + this.onFirstInstall(); | ||
| 52 | + } else if (details.reason === 'update') { | ||
| 53 | + this.onUpdate(details.previousVersion); | ||
| 54 | + } | ||
| 55 | + }); | ||
| 56 | + | ||
| 57 | + // Alarm listener for periodic tasks | ||
| 58 | + chrome.alarms.onAlarm.addListener((alarm) => { | ||
| 59 | + this.handleAlarm(alarm); | ||
| 60 | + }); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + async handleMessage(message, sender) { | ||
| 64 | + try { | ||
| 65 | + switch (message.type) { | ||
| 66 | + case 'NEW_MESSAGE': | ||
| 67 | + return await this.handleNewMessage(message.data, sender); | ||
| 68 | + | ||
| 69 | + case 'CAPTURE_SCREENSHOT': | ||
| 70 | + return await this.captureScreenshot(sender.tab); | ||
| 71 | + | ||
| 72 | + case 'GET_ANALYTICS': | ||
| 73 | + return await this.getAnalytics(); | ||
| 74 | + | ||
| 75 | + case 'EXPORT_DATA': | ||
| 76 | + return await this.exportData(message.data); | ||
| 77 | + | ||
| 78 | + case 'LEARN_PATTERN': | ||
| 79 | + return await this.learnPattern(message.data); | ||
| 80 | + | ||
| 81 | + case 'GET_SERVER_STATUS': | ||
| 82 | + return await this.checkServerStatus(); | ||
| 83 | + | ||
| 84 | + case 'TOGGLE_ACTIVE': | ||
| 85 | + return await this.handleToggleActive(message.data, sender); | ||
| 86 | + | ||
| 87 | + default: | ||
| 88 | + return { error: 'Unknown message type' }; | ||
| 89 | + } | ||
| 90 | + } catch (error) { | ||
| 91 | + console.error('[ServiceWorker] Error handling message:', error); | ||
| 92 | + return { error: error.message }; | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + async handleNewMessage(data, sender) { | ||
| 97 | + this.analytics.messagesProcessed++; | ||
| 98 | + | ||
| 99 | + // Add Chrome-specific tab info | ||
| 100 | + data.tabId = sender.tab.id; | ||
| 101 | + data.url = sender.url; | ||
| 102 | + | ||
| 103 | + // Process through unified handler | ||
| 104 | + const response = await this.messageHandler.processMessage(data); | ||
| 105 | + | ||
| 106 | + // If scammer detected, capture screenshot | ||
| 107 | + if (response.scammerScore > 0.7) { | ||
| 108 | + this.analytics.scammersDetected++; | ||
| 109 | + | ||
| 110 | + // Capture evidence | ||
| 111 | + const screenshot = await this.captureScreenshot(sender.tab); | ||
| 112 | + if (screenshot.success) { | ||
| 113 | + response.evidenceId = screenshot.id; | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + // Learn from pattern | ||
| 117 | + await this.patternLearning.addPattern(data, response.scammerScore); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + // Update analytics | ||
| 121 | + await this.saveAnalytics(); | ||
| 122 | + | ||
| 123 | + return response; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + async captureScreenshot(tab) { | ||
| 127 | + if (!tab) return { success: false, error: 'No tab provided' }; | ||
| 128 | + | ||
| 129 | + try { | ||
| 130 | + // Chrome-specific screenshot API | ||
| 131 | + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { | ||
| 132 | + format: 'png', | ||
| 133 | + quality: 90 | ||
| 134 | + }); | ||
| 135 | + | ||
| 136 | + // Store screenshot | ||
| 137 | + const screenshot = await this.screenshotManager.store({ | ||
| 138 | + dataUrl, | ||
| 139 | + tabId: tab.id, | ||
| 140 | + url: tab.url, | ||
| 141 | + timestamp: new Date().toISOString() | ||
| 142 | + }); | ||
| 143 | + | ||
| 144 | + this.analytics.screenshotsCaptured++; | ||
| 145 | + | ||
| 146 | + return { | ||
| 147 | + success: true, | ||
| 148 | + id: screenshot.id, | ||
| 149 | + size: screenshot.size | ||
| 150 | + }; | ||
| 151 | + } catch (error) { | ||
| 152 | + console.error('[ServiceWorker] Screenshot error:', error); | ||
| 153 | + return { success: false, error: error.message }; | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + async checkForSupportedSite(tab) { | ||
| 158 | + const supportedSites = [ | ||
| 159 | + 'web.whatsapp.com', | ||
| 160 | + 'web.telegram.org', | ||
| 161 | + 'messenger.com' | ||
| 162 | + ]; | ||
| 163 | + | ||
| 164 | + const isSupported = supportedSites.some(site => tab.url?.includes(site)); | ||
| 165 | + | ||
| 166 | + if (isSupported) { | ||
| 167 | + // Show page action or badge | ||
| 168 | + chrome.action.setBadgeText({ | ||
| 169 | + text: 'LC', | ||
| 170 | + tabId: tab.id | ||
| 171 | + }); | ||
| 172 | + | ||
| 173 | + chrome.action.setBadgeBackgroundColor({ | ||
| 174 | + color: '#4CAF50', | ||
| 175 | + tabId: tab.id | ||
| 176 | + }); | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + setupAlarms() { | ||
| 181 | + // Set up periodic tasks | ||
| 182 | + chrome.alarms.create('analyticsSync', { periodInMinutes: 5 }); | ||
| 183 | + chrome.alarms.create('patternSync', { periodInMinutes: 30 }); | ||
| 184 | + chrome.alarms.create('cleanup', { periodInMinutes: 60 }); | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + async handleAlarm(alarm) { | ||
| 188 | + switch (alarm.name) { | ||
| 189 | + case 'analyticsSync': | ||
| 190 | + await this.syncAnalytics(); | ||
| 191 | + break; | ||
| 192 | + | ||
| 193 | + case 'patternSync': | ||
| 194 | + await this.syncPatterns(); | ||
| 195 | + break; | ||
| 196 | + | ||
| 197 | + case 'cleanup': | ||
| 198 | + await this.cleanup(); | ||
| 199 | + break; | ||
| 200 | + } | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + async syncAnalytics() { | ||
| 204 | + try { | ||
| 205 | + // Send analytics to server | ||
| 206 | + const response = await fetch(`${this.serverUrl}/analytics/sync`, { | ||
| 207 | + method: 'POST', | ||
| 208 | + headers: { 'Content-Type': 'application/json' }, | ||
| 209 | + body: JSON.stringify({ | ||
| 210 | + analytics: this.analytics, | ||
| 211 | + timestamp: new Date().toISOString() | ||
| 212 | + }) | ||
| 213 | + }); | ||
| 214 | + | ||
| 215 | + if (response.ok) { | ||
| 216 | + console.log('[ServiceWorker] Analytics synced'); | ||
| 217 | + } | ||
| 218 | + } catch (error) { | ||
| 219 | + console.error('[ServiceWorker] Analytics sync failed:', error); | ||
| 220 | + } | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + async syncPatterns() { | ||
| 224 | + try { | ||
| 225 | + const patterns = await this.patternLearning.getPatterns(); | ||
| 226 | + | ||
| 227 | + // Send learned patterns to server | ||
| 228 | + const response = await fetch(`${this.serverUrl}/patterns/sync`, { | ||
| 229 | + method: 'POST', | ||
| 230 | + headers: { 'Content-Type': 'application/json' }, | ||
| 231 | + body: JSON.stringify({ patterns }) | ||
| 232 | + }); | ||
| 233 | + | ||
| 234 | + if (response.ok) { | ||
| 235 | + const serverPatterns = await response.json(); | ||
| 236 | + await this.patternLearning.updateFromServer(serverPatterns); | ||
| 237 | + console.log('[ServiceWorker] Patterns synced'); | ||
| 238 | + } | ||
| 239 | + } catch (error) { | ||
| 240 | + console.error('[ServiceWorker] Pattern sync failed:', error); | ||
| 241 | + } | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + async cleanup() { | ||
| 245 | + // Clean up old screenshots | ||
| 246 | + await this.screenshotManager.cleanup(24 * 60 * 60 * 1000); // 24 hours | ||
| 247 | + | ||
| 248 | + // Clean up old conversation data | ||
| 249 | + const storage = await chrome.storage.local.get(); | ||
| 250 | + const now = Date.now(); | ||
| 251 | + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days | ||
| 252 | + | ||
| 253 | + for (const key in storage) { | ||
| 254 | + if (key.startsWith('conversation_')) { | ||
| 255 | + const data = storage[key]; | ||
| 256 | + if (data.timestamp && (now - new Date(data.timestamp).getTime() > maxAge)) { | ||
| 257 | + await chrome.storage.local.remove(key); | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + console.log('[ServiceWorker] Cleanup completed'); | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + async connectToServer() { | ||
| 266 | + try { | ||
| 267 | + const response = await fetch(`${this.serverUrl}/status`); | ||
| 268 | + if (response.ok) { | ||
| 269 | + console.log('[ServiceWorker] Server connected'); | ||
| 270 | + chrome.action.setTitle({ title: 'LooseCannon - Connected' }); | ||
| 271 | + } else { | ||
| 272 | + throw new Error('Server not responding'); | ||
| 273 | + } | ||
| 274 | + } catch (error) { | ||
| 275 | + console.error('[ServiceWorker] Server connection failed:', error); | ||
| 276 | + chrome.action.setTitle({ title: 'LooseCannon - Server Offline' }); | ||
| 277 | + | ||
| 278 | + // Retry in 30 seconds | ||
| 279 | + setTimeout(() => this.connectToServer(), 30000); | ||
| 280 | + } | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + async getAnalytics() { | ||
| 284 | + const stored = await chrome.storage.local.get('analytics'); | ||
| 285 | + return { | ||
| 286 | + ...this.analytics, | ||
| 287 | + ...stored.analytics, | ||
| 288 | + uptime: Date.now() - this.startTime | ||
| 289 | + }; | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + async saveAnalytics() { | ||
| 293 | + await chrome.storage.local.set({ analytics: this.analytics }); | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + async loadAnalytics() { | ||
| 297 | + const stored = await chrome.storage.local.get('analytics'); | ||
| 298 | + if (stored.analytics) { | ||
| 299 | + this.analytics = { ...this.analytics, ...stored.analytics }; | ||
| 300 | + } | ||
| 301 | + this.startTime = Date.now(); | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + async exportData(options = {}) { | ||
| 305 | + const data = { | ||
| 306 | + version: '0.3.0', | ||
| 307 | + timestamp: new Date().toISOString(), | ||
| 308 | + analytics: this.analytics, | ||
| 309 | + patterns: await this.patternLearning.getPatterns(), | ||
| 310 | + screenshots: await this.screenshotManager.getAll() | ||
| 311 | + }; | ||
| 312 | + | ||
| 313 | + if (options.includeConversations) { | ||
| 314 | + const storage = await chrome.storage.local.get(); | ||
| 315 | + data.conversations = Object.entries(storage) | ||
| 316 | + .filter(([key]) => key.startsWith('conversation_')) | ||
| 317 | + .map(([key, value]) => value); | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + // Create download | ||
| 321 | + const blob = new Blob([JSON.stringify(data, null, 2)], { | ||
| 322 | + type: 'application/json' | ||
| 323 | + }); | ||
| 324 | + | ||
| 325 | + const url = URL.createObjectURL(blob); | ||
| 326 | + const filename = `loosecannon-export-${Date.now()}.json`; | ||
| 327 | + | ||
| 328 | + await chrome.downloads.download({ | ||
| 329 | + url, | ||
| 330 | + filename, | ||
| 331 | + saveAs: true | ||
| 332 | + }); | ||
| 333 | + | ||
| 334 | + return { success: true, filename }; | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + async handleToggleActive(data, sender) { | ||
| 338 | + this.analytics.sessionsStarted++; | ||
| 339 | + await this.saveAnalytics(); | ||
| 340 | + | ||
| 341 | + // Notify dashboard if open | ||
| 342 | + chrome.runtime.sendMessage({ | ||
| 343 | + type: 'DASHBOARD_UPDATE', | ||
| 344 | + data: { | ||
| 345 | + event: 'session_toggle', | ||
| 346 | + active: data.isActive, | ||
| 347 | + platform: data.platform, | ||
| 348 | + tabId: sender.tab?.id | ||
| 349 | + } | ||
| 350 | + }).catch(() => { | ||
| 351 | + // Dashboard might not be open | ||
| 352 | + }); | ||
| 353 | + | ||
| 354 | + return { success: true }; | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + async checkServerStatus() { | ||
| 358 | + try { | ||
| 359 | + const response = await fetch(`${this.serverUrl}/status`); | ||
| 360 | + const data = await response.json(); | ||
| 361 | + return { connected: true, ...data }; | ||
| 362 | + } catch (error) { | ||
| 363 | + return { connected: false, error: error.message }; | ||
| 364 | + } | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + onFirstInstall() { | ||
| 368 | + // Open welcome page | ||
| 369 | + chrome.tabs.create({ | ||
| 370 | + url: chrome.runtime.getURL('dashboard/welcome.html') | ||
| 371 | + }); | ||
| 372 | + } | ||
| 373 | + | ||
| 374 | + onUpdate(previousVersion) { | ||
| 375 | + console.log(`[ServiceWorker] Updated from ${previousVersion} to 0.3.0`); | ||
| 376 | + | ||
| 377 | + // Show update notification | ||
| 378 | + chrome.notifications.create({ | ||
| 379 | + type: 'basic', | ||
| 380 | + iconUrl: 'icons/icon-128.png', | ||
| 381 | + title: 'LooseCannon Updated', | ||
| 382 | + message: 'New features: Chrome support, Analytics Dashboard, and more!' | ||
| 383 | + }); | ||
| 384 | + } | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +// Initialize service worker | ||
| 388 | +const serviceWorker = new LooseCannonServiceWorker(); | ||
| 389 | + | ||
| 390 | +// Export for testing | ||
| 391 | +if (typeof module !== 'undefined' && module.exports) { | ||
| 392 | + module.exports = LooseCannonServiceWorker; | ||
| 393 | +} | ||
extension-chrome/background/unified-handler-v3.jsadded@@ -0,0 +1,293 @@ | |||
| 1 | +// Unified Message Handler for Chrome Manifest V3 | ||
| 2 | +// Handles messages from all supported platforms | ||
| 3 | + | ||
| 4 | +export class UnifiedMessageHandler { | ||
| 5 | + constructor() { | ||
| 6 | + this.serverUrl = 'http://localhost:8765'; | ||
| 7 | + this.activeConversations = new Map(); | ||
| 8 | + this.responseQueue = []; | ||
| 9 | + this.isProcessing = false; | ||
| 10 | + this.settings = { | ||
| 11 | + autoActivateOnScammer: true, | ||
| 12 | + scammerThreshold: 0.7, | ||
| 13 | + maxResponseDelay: 15000, | ||
| 14 | + minResponseDelay: 2000, | ||
| 15 | + personalityRotation: false | ||
| 16 | + }; | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + async processMessage(data) { | ||
| 20 | + try { | ||
| 21 | + // Add to conversation on server | ||
| 22 | + const contextResponse = await fetch(`${this.serverUrl}/conversation/add`, { | ||
| 23 | + method: 'POST', | ||
| 24 | + headers: { 'Content-Type': 'application/json' }, | ||
| 25 | + body: JSON.stringify({ | ||
| 26 | + chatId: data.chatId, | ||
| 27 | + platform: data.platform || this.detectPlatform(data.url), | ||
| 28 | + message: data | ||
| 29 | + }) | ||
| 30 | + }); | ||
| 31 | + | ||
| 32 | + const context = await contextResponse.json(); | ||
| 33 | + | ||
| 34 | + // Check for auto-activation | ||
| 35 | + if (this.settings.autoActivateOnScammer && | ||
| 36 | + context.scammerScore > this.settings.scammerThreshold && | ||
| 37 | + !this.isConversationActive(data.chatId, data.platform)) { | ||
| 38 | + | ||
| 39 | + this.activateConversation(data.chatId, data.platform, data.tabId); | ||
| 40 | + | ||
| 41 | + // Notify content script | ||
| 42 | + chrome.tabs.sendMessage(data.tabId, { | ||
| 43 | + type: 'SCAMMER_DETECTED', | ||
| 44 | + data: { | ||
| 45 | + score: context.scammerScore, | ||
| 46 | + autoActivated: true | ||
| 47 | + } | ||
| 48 | + }); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // Only generate response if active | ||
| 52 | + if (!this.isConversationActive(data.chatId, data.platform)) { | ||
| 53 | + return { | ||
| 54 | + processed: false, | ||
| 55 | + reason: 'Conversation not active', | ||
| 56 | + scammerScore: context.scammerScore | ||
| 57 | + }; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + // Get response suggestions | ||
| 61 | + const suggestionsResponse = await fetch(`${this.serverUrl}/suggestions`, { | ||
| 62 | + method: 'POST', | ||
| 63 | + headers: { 'Content-Type': 'application/json' }, | ||
| 64 | + body: JSON.stringify({ | ||
| 65 | + chatId: data.chatId, | ||
| 66 | + platform: data.platform, | ||
| 67 | + context | ||
| 68 | + }) | ||
| 69 | + }); | ||
| 70 | + | ||
| 71 | + const suggestions = await suggestionsResponse.json(); | ||
| 72 | + | ||
| 73 | + // Select personality | ||
| 74 | + const personality = await this.selectPersonality(data.chatId, context); | ||
| 75 | + | ||
| 76 | + // Generate response | ||
| 77 | + const generateResponse = await fetch(`${this.serverUrl}/generate`, { | ||
| 78 | + method: 'POST', | ||
| 79 | + headers: { 'Content-Type': 'application/json' }, | ||
| 80 | + body: JSON.stringify({ | ||
| 81 | + message: data.content || data.text, | ||
| 82 | + personality, | ||
| 83 | + chatId: data.chatId, | ||
| 84 | + platform: data.platform || this.detectPlatform(data.url), | ||
| 85 | + context, | ||
| 86 | + suggestions, | ||
| 87 | + timestamp: data.timestamp | ||
| 88 | + }) | ||
| 89 | + }); | ||
| 90 | + | ||
| 91 | + const result = await generateResponse.json(); | ||
| 92 | + | ||
| 93 | + // Calculate delay | ||
| 94 | + const delay = this.calculateResponseDelay(result.reply, context); | ||
| 95 | + | ||
| 96 | + // Queue response | ||
| 97 | + await this.queueResponse({ | ||
| 98 | + tabId: data.tabId, | ||
| 99 | + platform: data.platform || this.detectPlatform(data.url), | ||
| 100 | + chatId: data.chatId, | ||
| 101 | + reply: result.reply, | ||
| 102 | + delay, | ||
| 103 | + personality | ||
| 104 | + }); | ||
| 105 | + | ||
| 106 | + return { | ||
| 107 | + queued: true, | ||
| 108 | + reply: result.reply, | ||
| 109 | + delay, | ||
| 110 | + personality, | ||
| 111 | + scammerScore: context.scammerScore | ||
| 112 | + }; | ||
| 113 | + | ||
| 114 | + } catch (error) { | ||
| 115 | + console.error('[UnifiedHandler] Error:', error); | ||
| 116 | + | ||
| 117 | + const fallback = this.getFallbackResponse(data.platform); | ||
| 118 | + return { | ||
| 119 | + reply: fallback, | ||
| 120 | + delay: 3000, | ||
| 121 | + error: error.message | ||
| 122 | + }; | ||
| 123 | + } | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + detectPlatform(url) { | ||
| 127 | + if (!url) return 'unknown'; | ||
| 128 | + | ||
| 129 | + if (url.includes('whatsapp.com')) return 'whatsapp'; | ||
| 130 | + if (url.includes('telegram.org')) return 'telegram'; | ||
| 131 | + if (url.includes('messenger.com')) return 'messenger'; | ||
| 132 | + | ||
| 133 | + return 'unknown'; | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + async selectPersonality(chatId, context) { | ||
| 137 | + if (!this.settings.personalityRotation) { | ||
| 138 | + return context.personality || 'default'; | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + // Load available personalities | ||
| 142 | + const stored = await chrome.storage.local.get('personalities'); | ||
| 143 | + const personalities = stored.personalities || ['default']; | ||
| 144 | + | ||
| 145 | + // Rotate based on message count | ||
| 146 | + const messageCount = context.messageCount || 0; | ||
| 147 | + const index = Math.floor(messageCount / 5) % personalities.length; | ||
| 148 | + | ||
| 149 | + return personalities[index]; | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + calculateResponseDelay(text, context) { | ||
| 153 | + const wordCount = text.split(' ').length; | ||
| 154 | + const baseDelay = this.settings.minResponseDelay; | ||
| 155 | + const perWordDelay = 150; | ||
| 156 | + | ||
| 157 | + let delay = baseDelay + (wordCount * perWordDelay); | ||
| 158 | + | ||
| 159 | + // Adjust based on context | ||
| 160 | + if (context.conversationTone === 'urgent') { | ||
| 161 | + delay *= 1.5; // Be slower for urgent scammers | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + // Add random variation | ||
| 165 | + const variation = 0.3; | ||
| 166 | + const randomFactor = 1 + (Math.random() * 2 * variation - variation); | ||
| 167 | + delay *= randomFactor; | ||
| 168 | + | ||
| 169 | + return Math.min(delay, this.settings.maxResponseDelay); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + async queueResponse(response) { | ||
| 173 | + this.responseQueue.push({ | ||
| 174 | + ...response, | ||
| 175 | + queuedAt: Date.now() | ||
| 176 | + }); | ||
| 177 | + | ||
| 178 | + if (!this.isProcessing) { | ||
| 179 | + this.processQueue(); | ||
| 180 | + } | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + async processQueue() { | ||
| 184 | + if (this.responseQueue.length === 0) { | ||
| 185 | + this.isProcessing = false; | ||
| 186 | + return; | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + this.isProcessing = true; | ||
| 190 | + const response = this.responseQueue.shift(); | ||
| 191 | + | ||
| 192 | + // Calculate remaining delay | ||
| 193 | + const elapsed = Date.now() - response.queuedAt; | ||
| 194 | + const remainingDelay = Math.max(0, response.delay - elapsed); | ||
| 195 | + | ||
| 196 | + // Wait | ||
| 197 | + await this.sleep(remainingDelay); | ||
| 198 | + | ||
| 199 | + // Send response | ||
| 200 | + try { | ||
| 201 | + await chrome.tabs.sendMessage(response.tabId, { | ||
| 202 | + type: 'SEND_MESSAGE', | ||
| 203 | + data: { | ||
| 204 | + text: response.reply, | ||
| 205 | + delay: 0 | ||
| 206 | + } | ||
| 207 | + }); | ||
| 208 | + | ||
| 209 | + console.log(`[UnifiedHandler] Sent response to ${response.platform}:${response.chatId}`); | ||
| 210 | + } catch (error) { | ||
| 211 | + console.error('[UnifiedHandler] Failed to send response:', error); | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + // Process next | ||
| 215 | + this.processQueue(); | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + activateConversation(chatId, platform, tabId) { | ||
| 219 | + const key = `${platform}:${chatId}`; | ||
| 220 | + this.activeConversations.set(key, { | ||
| 221 | + chatId, | ||
| 222 | + platform, | ||
| 223 | + tabId, | ||
| 224 | + activatedAt: Date.now(), | ||
| 225 | + messageCount: 0 | ||
| 226 | + }); | ||
| 227 | + | ||
| 228 | + console.log(`[UnifiedHandler] Activated ${key}`); | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + deactivateConversation(chatId, platform) { | ||
| 232 | + const key = `${platform}:${chatId}`; | ||
| 233 | + this.activeConversations.delete(key); | ||
| 234 | + console.log(`[UnifiedHandler] Deactivated ${key}`); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + isConversationActive(chatId, platform) { | ||
| 238 | + const key = `${platform}:${chatId}`; | ||
| 239 | + return this.activeConversations.has(key); | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + getFallbackResponse(platform) { | ||
| 243 | + const fallbacks = { | ||
| 244 | + whatsapp: [ | ||
| 245 | + "Sorry, what was that? My WhatsApp is acting strange.", | ||
| 246 | + "Can you repeat? The message came through garbled.", | ||
| 247 | + "Hold on, my phone is being slow..." | ||
| 248 | + ], | ||
| 249 | + telegram: [ | ||
| 250 | + "Telegram is glitching for me, one second...", | ||
| 251 | + "Strange, I'm getting errors. What did you say?", | ||
| 252 | + "My Telegram is updating, please wait..." | ||
| 253 | + ], | ||
| 254 | + messenger: [ | ||
| 255 | + "Facebook is being weird, can you resend?", | ||
| 256 | + "Messenger crashed, what were you saying?", | ||
| 257 | + "Sorry, Facebook is slow today..." | ||
| 258 | + ], | ||
| 259 | + default: [ | ||
| 260 | + "I didn't catch that, can you repeat?", | ||
| 261 | + "Sorry, technical difficulties...", | ||
| 262 | + "One moment please..." | ||
| 263 | + ] | ||
| 264 | + }; | ||
| 265 | + | ||
| 266 | + const platformFallbacks = fallbacks[platform] || fallbacks.default; | ||
| 267 | + return platformFallbacks[Math.floor(Math.random() * platformFallbacks.length)]; | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + async getStatistics() { | ||
| 271 | + const stats = { | ||
| 272 | + activeConversations: this.activeConversations.size, | ||
| 273 | + queuedResponses: this.responseQueue.length, | ||
| 274 | + platformBreakdown: {} | ||
| 275 | + }; | ||
| 276 | + | ||
| 277 | + this.activeConversations.forEach(conv => { | ||
| 278 | + stats.platformBreakdown[conv.platform] = | ||
| 279 | + (stats.platformBreakdown[conv.platform] || 0) + 1; | ||
| 280 | + }); | ||
| 281 | + | ||
| 282 | + return stats; | ||
| 283 | + } | ||
| 284 | + | ||
| 285 | + sleep(ms) { | ||
| 286 | + return new Promise(resolve => setTimeout(resolve, ms)); | ||
| 287 | + } | ||
| 288 | +} | ||
| 289 | + | ||
| 290 | +// Export for use in service worker | ||
| 291 | +if (typeof module !== 'undefined' && module.exports) { | ||
| 292 | + module.exports = UnifiedMessageHandler; | ||
| 293 | +} | ||
extension-chrome/manifest.jsonadded@@ -0,0 +1,80 @@ | |||
| 1 | +{ | ||
| 2 | + "manifest_version": 3, | ||
| 3 | + "name": "LooseCannon", | ||
| 4 | + "version": "0.3.0", | ||
| 5 | + "description": "Automated scambaiting assistant - Now with Chrome support", | ||
| 6 | + | ||
| 7 | + "permissions": [ | ||
| 8 | + "storage", | ||
| 9 | + "tabs", | ||
| 10 | + "scripting", | ||
| 11 | + "webRequest", | ||
| 12 | + "declarativeNetRequest" | ||
| 13 | + ], | ||
| 14 | + | ||
| 15 | + "host_permissions": [ | ||
| 16 | + "*://web.whatsapp.com/*", | ||
| 17 | + "*://web.telegram.org/*", | ||
| 18 | + "*://webk.telegram.org/*", | ||
| 19 | + "*://webz.telegram.org/*", | ||
| 20 | + "*://www.messenger.com/*", | ||
| 21 | + "*://messenger.com/*", | ||
| 22 | + "http://localhost:8765/*" | ||
| 23 | + ], | ||
| 24 | + | ||
| 25 | + "background": { | ||
| 26 | + "service_worker": "background/service-worker.js", | ||
| 27 | + "type": "module" | ||
| 28 | + }, | ||
| 29 | + | ||
| 30 | + "content_scripts": [ | ||
| 31 | + { | ||
| 32 | + "matches": ["*://web.whatsapp.com/*"], | ||
| 33 | + "js": ["content-scripts/whatsapp-enhanced.js"], | ||
| 34 | + "css": ["content-scripts/whatsapp.css"], | ||
| 35 | + "run_at": "document_idle" | ||
| 36 | + }, | ||
| 37 | + { | ||
| 38 | + "matches": [ | ||
| 39 | + "*://web.telegram.org/*", | ||
| 40 | + "*://webk.telegram.org/*", | ||
| 41 | + "*://webz.telegram.org/*" | ||
| 42 | + ], | ||
| 43 | + "js": ["content-scripts/telegram.js"], | ||
| 44 | + "run_at": "document_idle" | ||
| 45 | + }, | ||
| 46 | + { | ||
| 47 | + "matches": [ | ||
| 48 | + "*://www.messenger.com/*", | ||
| 49 | + "*://messenger.com/*" | ||
| 50 | + ], | ||
| 51 | + "js": ["content-scripts/messenger.js"], | ||
| 52 | + "run_at": "document_idle" | ||
| 53 | + } | ||
| 54 | + ], | ||
| 55 | + | ||
| 56 | + "action": { | ||
| 57 | + "default_popup": "popup/popup.html", | ||
| 58 | + "default_icon": { | ||
| 59 | + "16": "icons/icon-16.png", | ||
| 60 | + "48": "icons/icon-48.png", | ||
| 61 | + "128": "icons/icon-128.png" | ||
| 62 | + } | ||
| 63 | + }, | ||
| 64 | + | ||
| 65 | + "icons": { | ||
| 66 | + "16": "icons/icon-16.png", | ||
| 67 | + "48": "icons/icon-48.png", | ||
| 68 | + "128": "icons/icon-128.png" | ||
| 69 | + }, | ||
| 70 | + | ||
| 71 | + "web_accessible_resources": [ | ||
| 72 | + { | ||
| 73 | + "resources": [ | ||
| 74 | + "icons/*", | ||
| 75 | + "content-scripts/*.css" | ||
| 76 | + ], | ||
| 77 | + "matches": ["<all_urls>"] | ||
| 78 | + } | ||
| 79 | + ] | ||
| 80 | +} | ||