Monorepo for Aesthetic.Computer
aesthetic.computer
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>