| 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 |
}); |