Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 673 lines 20 kB view raw
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>judge · Aesthetic Computer</title> 7 <link rel="icon" href="https://aesthetic.computer/icon/128x128/prompt.png" type="image/png" /> 8 <style> 9 * { 10 margin: 0; 11 padding: 0; 12 box-sizing: border-box; 13 } 14 15 ::-webkit-scrollbar { 16 display: none; 17 } 18 19 body { 20 font-family: monospace; 21 font-size: 14px; 22 line-height: 1.5; 23 background: #f5f5f5; 24 color: #000; 25 -webkit-text-size-adjust: none; 26 cursor: url('https://aesthetic.computer/aesthetic.computer/cursors/precise.svg') 12 12, auto; 27 } 28 29 .container { 30 padding: 1em; 31 padding-top: 4em; 32 } 33 34 header { 35 position: fixed; 36 top: 12px; 37 left: 16px; 38 z-index: 3; 39 background: transparent; 40 border: none; 41 padding: 0; 42 margin: 0; 43 } 44 45 header::before { 46 content: ""; 47 height: 3em; 48 width: 100vw; 49 position: absolute; 50 z-index: -1; 51 top: -12px; 52 left: -16px; 53 background: linear-gradient(to bottom, #f5f5f5 50%, transparent 100%); 54 } 55 56 .header-left { 57 flex: 1; 58 } 59 60 .header-right { 61 display: none; 62 } 63 64 h1 { 65 font-size: 22px; 66 font-weight: normal; 67 color: rgb(205, 92, 155); 68 margin: 0; 69 cursor: pointer; 70 user-select: none; 71 } 72 73 h1:hover { 74 color: purple; 75 } 76 77 h1:active { 78 color: gray; 79 } 80 81 .subtitle { 82 font-size: 0.85em; 83 opacity: 0.7; 84 margin-top: 0.2em; 85 } 86 87 .status { 88 display: flex; 89 align-items: center; 90 gap: 0.5em; 91 } 92 93 .status-dot { 94 width: 10px; 95 height: 10px; 96 border-radius: 50%; 97 background: #999; 98 } 99 100 .status-dot.connected { 101 background: #4ade80; 102 box-shadow: 0 0 8px rgba(74, 222, 128, 0.5); 103 animation: pulse 2s ease-in-out infinite; 104 } 105 106 @keyframes pulse { 107 0%, 100% { opacity: 1; } 108 50% { opacity: 0.6; } 109 } 110 111 .status-dot.disconnected { 112 background: #ef4444; 113 } 114 115 section { 116 margin: 2em 0; 117 padding: 0 1em; 118 } 119 120 h2 { 121 font-size: 1.2em; 122 font-weight: normal; 123 margin-bottom: 0.8em; 124 padding-bottom: 0.3em; 125 border-bottom: 1px solid #ddd; 126 } 127 128 .content-wrapper { 129 max-width: 1200px; 130 margin: 0 auto; 131 } 132 133 .form-group { 134 margin: 1em 0; 135 } 136 137 label { 138 display: block; 139 margin-bottom: 0.5em; 140 font-weight: normal; 141 } 142 143 textarea { 144 width: 100%; 145 padding: 0.8em; 146 font-family: monospace; 147 font-size: 14px; 148 border: 1px solid #ccc; 149 background: white; 150 resize: vertical; 151 min-height: 80px; 152 } 153 154 textarea:focus { 155 outline: none; 156 border-color: rgb(205, 92, 155); 157 } 158 159 .button-group { 160 display: flex; 161 gap: 0.5em; 162 flex-wrap: wrap; 163 margin: 1em 0; 164 } 165 166 button { 167 padding: 0.7em 1.2em; 168 font-family: monospace; 169 font-size: 14px; 170 border: 1px solid #000; 171 background: white; 172 cursor: pointer; 173 transition: all 0.2s; 174 position: relative; 175 overflow: hidden; 176 } 177 178 button:hover { 179 background: rgb(205, 92, 155); 180 color: white; 181 border-color: rgb(205, 92, 155); 182 } 183 184 button:active { 185 transform: translateY(1px); 186 } 187 188 button:disabled { 189 opacity: 0.5; 190 cursor: not-allowed; 191 background: #f0f0f0; 192 } 193 194 button.testing { 195 background: rgb(205, 92, 155); 196 color: white; 197 border-color: rgb(205, 92, 155); 198 } 199 200 button.testing::after { 201 content: ''; 202 position: absolute; 203 bottom: 0; 204 left: 0; 205 height: 3px; 206 background: rgba(255, 255, 255, 0.5); 207 animation: progress 2s ease-in-out infinite; 208 } 209 210 @keyframes progress { 211 0% { width: 0%; } 212 50% { width: 70%; } 213 100% { width: 100%; } 214 } 215 216 .result { 217 margin: 1em 0; 218 padding: 1em; 219 background: white; 220 border-left: 4px solid #ddd; 221 display: none; 222 animation: slideIn 0.3s ease-out; 223 } 224 225 @keyframes slideIn { 226 from { 227 opacity: 0; 228 transform: translateX(-10px); 229 } 230 to { 231 opacity: 1; 232 transform: translateX(0); 233 } 234 } 235 236 .result.show { 237 display: block; 238 } 239 240 .result.allowed { 241 border-color: #4ade80; 242 background: #f0fdf4; 243 } 244 245 .result.blocked { 246 border-color: #ef4444; 247 background: #fef2f2; 248 } 249 250 .result-header { 251 font-weight: bold; 252 margin-bottom: 0.5em; 253 font-size: 1.1em; 254 } 255 256 .result-details { 257 font-size: 0.9em; 258 opacity: 0.8; 259 margin: 0.3em 0; 260 } 261 262 .stream-output { 263 margin: 1em 0; 264 padding: 0.8em; 265 background: #fafafa; 266 border: 1px solid #eee; 267 min-height: 60px; 268 max-height: 200px; 269 overflow-y: auto; 270 font-size: 0.85em; 271 white-space: pre-wrap; 272 font-family: monospace; 273 display: none; 274 animation: fadeIn 0.3s ease-out; 275 } 276 277 @keyframes fadeIn { 278 from { opacity: 0; } 279 to { opacity: 1; } 280 } 281 282 .stream-output.active { 283 display: block; 284 } 285 286 .stream-output::before { 287 content: '💬 AI reasoning...'; 288 display: block; 289 margin-bottom: 0.5em; 290 opacity: 0.6; 291 font-size: 0.9em; 292 } 293 294 .history { 295 margin: 1em 0; 296 } 297 298 .history-item { 299 padding: 0.8em; 300 margin: 0.5em 0; 301 background: white; 302 border-left: 3px solid #ddd; 303 font-size: 0.9em; 304 } 305 306 .history-item.allowed { 307 border-color: #4ade80; 308 } 309 310 .history-item.blocked { 311 border-color: #ef4444; 312 } 313 314 .message-text { 315 margin-bottom: 0.5em; 316 word-wrap: break-word; 317 } 318 319 .message-meta { 320 font-size: 0.85em; 321 opacity: 0.6; 322 } 323 324 .stats { 325 display: grid; 326 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 327 gap: 0.8em; 328 margin: 1em 0; 329 } 330 331 .stat-box { 332 padding: 1em; 333 background: white; 334 border: 1px solid #ddd; 335 } 336 337 .stat-label { 338 font-size: 0.85em; 339 opacity: 0.7; 340 margin-bottom: 0.3em; 341 } 342 343 .stat-value { 344 font-size: 1.8em; 345 font-weight: normal; 346 color: rgb(205, 92, 155); 347 } 348 349 @media (max-width: 600px) { 350 .container { 351 padding: 0.5em; 352 } 353 354 header { 355 flex-direction: column; 356 align-items: flex-start; 357 } 358 359 .header-right { 360 width: 100%; 361 flex-direction: column; 362 align-items: flex-start; 363 gap: 0.5em; 364 } 365 366 h1 { 367 font-size: 1.2em; 368 } 369 370 section { 371 padding: 0 0.5em; 372 } 373 374 .stats { 375 grid-template-columns: 1fr; 376 } 377 378 .button-group { 379 flex-direction: column; 380 } 381 382 button { 383 width: 100%; 384 } 385 } 386 </style> 387</head> 388<body> 389 <div class="container"> 390 <header> 391 <div class="header-left"> 392 <h1>judge</h1> 393 </div> 394 <div class="header-right"> 395 <div class="status"> 396 <div class="status-dot" id="wsStatus"></div> 397 <span id="wsStatusText">Connecting...</span> 398 </div> 399 <div class="status"> 400 <span id="modelInfo">Model: gemma2:2b</span> 401 </div> 402 </div> 403 </header> 404 405 <div class="content-wrapper"> 406 <section> 407 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em; flex-wrap: wrap; gap: 1em;"> 408 <h2 style="margin: 0;">Test Message</h2> 409 <div style="display: flex; gap: 1.5em; align-items: center; font-size: 0.9em;"> 410 <div class="status"> 411 <div class="status-dot" id="wsStatus2"></div> 412 <span id="wsStatusText2">Connecting...</span> 413 </div> 414 <div class="status"> 415 <span>gemma2:2b</span> 416 </div> 417 </div> 418 </div> 419 <div class="form-group"> 420 <label for="messageInput">Enter a message to check:</label> 421 <textarea id="messageInput" placeholder="Type a message here..."></textarea> 422 </div> 423 424 <div class="button-group"> 425 <button id="testBtn" onclick="testMessage()">Test Message</button> 426 <button onclick="clearForm()">Clear</button> 427 </div> 428 429 <div id="streamOutput" class="stream-output"></div> 430 <div id="result" class="result"></div> 431 </section> 432 433 <section> 434 <h2>Statistics</h2> 435 <div class="stats"> 436 <div class="stat-box"> 437 <div class="stat-label">Total Tests</div> 438 <div class="stat-value" id="totalTests">0</div> 439 </div> 440 <div class="stat-box"> 441 <div class="stat-label">Allowed</div> 442 <div class="stat-value" id="allowedCount">0</div> 443 </div> 444 <div class="stat-box"> 445 <div class="stat-label">Blocked</div> 446 <div class="stat-value" id="blockedCount">0</div> 447 </div> 448 <div class="stat-box"> 449 <div class="stat-label">Avg Response</div> 450 <div class="stat-value" id="avgResponse">0s</div> 451 </div> 452 </div> 453 </section> 454 455 <section> 456 <h2>Recent Tests</h2> 457 <div id="history" class="history"></div> 458 </section> 459 </div> 460 </div> 461 462 <script> 463 // WebSocket connection 464 const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 465 const wsUrl = `${protocol}//${window.location.host}/ws`; 466 let ws = null; 467 let reconnectTimer = null; 468 469 console.log('📍 Attempting WebSocket connection to:', wsUrl); 470 471 function updateConnectionStatus(connected) { 472 const dot = document.getElementById('wsStatus'); 473 const text = document.getElementById('wsStatusText'); 474 const dot2 = document.getElementById('wsStatus2'); 475 const text2 = document.getElementById('wsStatusText2'); 476 477 if (connected) { 478 dot?.classList.add('connected'); 479 dot?.classList.remove('disconnected'); 480 if (text) text.textContent = 'Connected'; 481 dot2?.classList.add('connected'); 482 dot2?.classList.remove('disconnected'); 483 if (text2) text2.textContent = 'Connected'; 484 } else { 485 dot?.classList.remove('connected'); 486 dot?.classList.add('disconnected'); 487 if (text) text.textContent = 'Disconnected'; 488 dot2?.classList.remove('connected'); 489 dot2?.classList.add('disconnected'); 490 if (text2) text2.textContent = 'Disconnected'; 491 } 492 } 493 494 function connectWebSocket() { 495 try { 496 console.log('🔌 Connecting WebSocket...'); 497 ws = new WebSocket(wsUrl); 498 499 ws.onopen = () => { 500 console.log('🟢 WebSocket connected successfully'); 501 updateConnectionStatus(true); 502 }; 503 504 ws.onclose = (event) => { 505 console.log('🔴 WebSocket disconnected', event.code, event.reason); 506 updateConnectionStatus(false); 507 // Reconnect after 2 seconds 508 reconnectTimer = setTimeout(connectWebSocket, 2000); 509 }; 510 511 ws.onerror = (error) => { 512 console.error('❌ WebSocket error:', error); 513 updateConnectionStatus(false); 514 }; 515 516 ws.onmessage = (event) => { 517 const data = JSON.parse(event.data); 518 handleWebSocketMessage(data); 519 }; 520 } catch (error) { 521 console.error('❌ Failed to create WebSocket:', error); 522 updateConnectionStatus(false); 523 } 524 } 525 526 // Initialize on load 527 connectWebSocket(); 528 529 // Statistics tracking 530 let stats = { 531 total: 0, 532 allowed: 0, 533 blocked: 0, 534 responseTimes: [] 535 }; 536 537 function updateStats() { 538 document.getElementById('totalTests').textContent = stats.total; 539 document.getElementById('allowedCount').textContent = stats.allowed; 540 document.getElementById('blockedCount').textContent = stats.blocked; 541 542 if (stats.responseTimes.length > 0) { 543 const avg = stats.responseTimes.reduce((a, b) => a + b, 0) / stats.responseTimes.length; 544 document.getElementById('avgResponse').textContent = avg.toFixed(2) + 's'; 545 } 546 } 547 548 function handleWebSocketMessage(data) { 549 const streamOutput = document.getElementById('streamOutput'); 550 const testBtn = document.getElementById('testBtn'); 551 552 if (data.type === 'start') { 553 streamOutput.classList.add('active'); 554 streamOutput.textContent = ''; 555 testBtn.classList.add('testing'); 556 testBtn.disabled = true; 557 testBtn.textContent = 'Testing...'; 558 } else if (data.type === 'chunk') { 559 streamOutput.textContent = data.full || data.chunk; 560 streamOutput.scrollTop = streamOutput.scrollHeight; 561 } else if (data.type === 'complete') { 562 testBtn.classList.remove('testing'); 563 testBtn.disabled = false; 564 testBtn.textContent = 'Test Message'; 565 566 showResult(data); 567 568 // Update stats 569 stats.total++; 570 if (data.decision === 't') stats.allowed++; 571 else stats.blocked++; 572 if (data.responseTime) stats.responseTimes.push(data.responseTime); 573 updateStats(); 574 575 // Add to history 576 addToHistory(currentMessage, data); 577 578 // Hide stream output after showing result 579 setTimeout(() => { 580 streamOutput.classList.remove('active'); 581 }, 1000); 582 } 583 } 584 585 let currentMessage = ''; 586 587 function testMessage() { 588 const message = document.getElementById('messageInput').value.trim(); 589 if (!message) { 590 alert('Please enter a message'); 591 return; 592 } 593 594 currentMessage = message; 595 596 if (!ws || ws.readyState !== WebSocket.OPEN) { 597 alert('WebSocket not connected. Please wait...'); 598 return; 599 } 600 601 // Clear previous results 602 document.getElementById('result').classList.remove('show'); 603 604 ws.send(JSON.stringify({ message })); 605 } 606 607 function showResult(data) { 608 const resultDiv = document.getElementById('result'); 609 const allowed = data.decision === 't'; 610 611 resultDiv.className = 'result show ' + (allowed ? 'allowed' : 'blocked'); 612 613 const emoji = allowed ? '✓' : '✗'; 614 const status = allowed ? 'Allowed' : 'Blocked'; 615 616 resultDiv.innerHTML = ` 617 <div class="result-header">${emoji} ${status}</div> 618 <div class="result-details">Sentiment: ${data.sentiment || '(yes)'}</div> 619 ${data.reason ? `<div class="result-details">Reason: ${data.reason}</div>` : ''} 620 <div class="result-details">Response time: ${data.responseTime ? data.responseTime.toFixed(3) + 's' : 'N/A'}</div> 621 `; 622 } 623 624 function addToHistory(message, data) { 625 const historyDiv = document.getElementById('history'); 626 const allowed = data.decision === 't'; 627 628 const item = document.createElement('div'); 629 item.className = 'history-item ' + (allowed ? 'allowed' : 'blocked'); 630 item.innerHTML = ` 631 <div class="message-text">${escapeHtml(message)}</div> 632 <div class="message-meta"> 633 ${allowed ? '✓ Allowed' : '✗ Blocked'} · 634 ${data.responseTime ? data.responseTime.toFixed(2) + 's' : 'N/A'} · 635 ${new Date().toLocaleTimeString()} 636 </div> 637 `; 638 639 historyDiv.insertBefore(item, historyDiv.firstChild); 640 641 // Keep only last 10 items 642 while (historyDiv.children.length > 10) { 643 historyDiv.removeChild(historyDiv.lastChild); 644 } 645 } 646 647 function clearForm() { 648 document.getElementById('messageInput').value = ''; 649 document.getElementById('result').classList.remove('show'); 650 document.getElementById('streamOutput').classList.remove('active'); 651 const testBtn = document.getElementById('testBtn'); 652 testBtn.classList.remove('testing'); 653 testBtn.disabled = false; 654 testBtn.textContent = 'Test Message'; 655 } 656 657 function escapeHtml(text) { 658 const div = document.createElement('div'); 659 div.textContent = text; 660 return div.innerHTML; 661 } 662 663 // Wire up back button to go to aesthetic.computer 664 document.querySelector('h1').onclick = () => { 665 if (window.location.host !== 'judge.aesthetic.computer') { 666 window.location.href = '/'; 667 } else { 668 window.location.href = 'https://aesthetic.computer'; 669 } 670 }; 671 </script> 672</body> 673</html>