seawolf2357 commited on
Commit
4e77326
Β·
verified Β·
1 Parent(s): 2e913ce

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +584 -0
index.html ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>MOUSE μŒμ„± μ±—</title>
8
+ <style>
9
+ :root {
10
+ --primary-color: #6f42c1;
11
+ --secondary-color: #563d7c;
12
+ --dark-bg: #121212;
13
+ --card-bg: #1e1e1e;
14
+ --text-color: #f8f9fa;
15
+ --border-color: #333;
16
+ --hover-color: #8a5cf6;
17
+ }
18
+
19
+ body {
20
+ font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
21
+ background-color: var(--dark-bg);
22
+ color: var(--text-color);
23
+ margin: 0;
24
+ padding: 0;
25
+ height: 100vh;
26
+ display: flex;
27
+ flex-direction: column;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .container {
32
+ max-width: 900px;
33
+ margin: 0 auto;
34
+ padding: 20px;
35
+ flex-grow: 1;
36
+ display: flex;
37
+ flex-direction: column;
38
+ width: 100%;
39
+ }
40
+
41
+ .header {
42
+ text-align: center;
43
+ padding: 20px 0;
44
+ border-bottom: 1px solid var(--border-color);
45
+ margin-bottom: 20px;
46
+ }
47
+
48
+ .logo {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ gap: 10px;
53
+ }
54
+
55
+ .logo h1 {
56
+ margin: 0;
57
+ background: linear-gradient(135deg, var(--primary-color), #a78bfa);
58
+ -webkit-background-clip: text;
59
+ background-clip: text;
60
+ color: transparent;
61
+ font-size: 32px;
62
+ letter-spacing: 1px;
63
+ }
64
+
65
+ .chat-container {
66
+ border-radius: 12px;
67
+ background-color: var(--card-bg);
68
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
69
+ padding: 20px;
70
+ flex-grow: 1;
71
+ display: flex;
72
+ flex-direction: column;
73
+ border: 1px solid var(--border-color);
74
+ }
75
+
76
+ .chat-messages {
77
+ flex-grow: 1;
78
+ overflow-y: auto;
79
+ margin-bottom: 20px;
80
+ padding: 10px;
81
+ scrollbar-width: thin;
82
+ scrollbar-color: var(--primary-color) var(--card-bg);
83
+ }
84
+
85
+ .chat-messages::-webkit-scrollbar {
86
+ width: 6px;
87
+ }
88
+
89
+ .chat-messages::-webkit-scrollbar-thumb {
90
+ background-color: var(--primary-color);
91
+ border-radius: 6px;
92
+ }
93
+
94
+ .message {
95
+ margin-bottom: 20px;
96
+ padding: 14px;
97
+ border-radius: 8px;
98
+ font-size: 16px;
99
+ line-height: 1.6;
100
+ position: relative;
101
+ max-width: 80%;
102
+ animation: fade-in 0.3s ease-out;
103
+ }
104
+
105
+ @keyframes fade-in {
106
+ from {
107
+ opacity: 0;
108
+ transform: translateY(10px);
109
+ }
110
+ to {
111
+ opacity: 1;
112
+ transform: translateY(0);
113
+ }
114
+ }
115
+
116
+ .message.user {
117
+ background: linear-gradient(135deg, #2c3e50, #34495e);
118
+ margin-left: auto;
119
+ border-bottom-right-radius: 2px;
120
+ }
121
+
122
+ .message.assistant {
123
+ background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
124
+ margin-right: auto;
125
+ border-bottom-left-radius: 2px;
126
+ }
127
+
128
+ .controls {
129
+ text-align: center;
130
+ margin-top: 20px;
131
+ display: flex;
132
+ justify-content: center;
133
+ }
134
+
135
+ button {
136
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
137
+ color: white;
138
+ border: none;
139
+ padding: 14px 28px;
140
+ font-family: inherit;
141
+ font-size: 16px;
142
+ cursor: pointer;
143
+ transition: all 0.3s;
144
+ text-transform: uppercase;
145
+ letter-spacing: 1px;
146
+ border-radius: 50px;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ gap: 10px;
151
+ box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
152
+ }
153
+
154
+ button:hover {
155
+ transform: translateY(-2px);
156
+ box-shadow: 0 6px 15px rgba(111, 66, 193, 0.5);
157
+ background: linear-gradient(135deg, var(--hover-color), var(--primary-color));
158
+ }
159
+
160
+ button:active {
161
+ transform: translateY(1px);
162
+ }
163
+
164
+ #audio-output {
165
+ display: none;
166
+ }
167
+
168
+ .icon-with-spinner {
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ gap: 12px;
173
+ min-width: 180px;
174
+ }
175
+
176
+ .spinner {
177
+ width: 20px;
178
+ height: 20px;
179
+ border: 2px solid #ffffff;
180
+ border-top-color: transparent;
181
+ border-radius: 50%;
182
+ animation: spin 1s linear infinite;
183
+ flex-shrink: 0;
184
+ }
185
+
186
+ @keyframes spin {
187
+ to {
188
+ transform: rotate(360deg);
189
+ }
190
+ }
191
+
192
+ .audio-visualizer {
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ gap: 5px;
197
+ min-width: 80px;
198
+ height: 25px;
199
+ }
200
+
201
+ .visualizer-bar {
202
+ width: 4px;
203
+ height: 100%;
204
+ background-color: rgba(255, 255, 255, 0.7);
205
+ border-radius: 2px;
206
+ transform-origin: bottom;
207
+ transform: scaleY(0.1);
208
+ transition: transform 0.1s ease;
209
+ }
210
+
211
+ /* Add styles for toast notifications */
212
+ .toast {
213
+ position: fixed;
214
+ top: 20px;
215
+ left: 50%;
216
+ transform: translateX(-50%);
217
+ padding: 16px 24px;
218
+ border-radius: 8px;
219
+ font-size: 14px;
220
+ z-index: 1000;
221
+ display: none;
222
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
223
+ }
224
+
225
+ .toast.error {
226
+ background-color: #f44336;
227
+ color: white;
228
+ }
229
+
230
+ .toast.warning {
231
+ background-color: #ff9800;
232
+ color: white;
233
+ }
234
+
235
+ /* Status indicator */
236
+ .status-indicator {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ margin-top: 10px;
240
+ font-size: 14px;
241
+ color: #aaa;
242
+ }
243
+
244
+ .status-dot {
245
+ width: 8px;
246
+ height: 8px;
247
+ border-radius: 50%;
248
+ margin-right: 8px;
249
+ }
250
+
251
+ .status-dot.connected {
252
+ background-color: #4caf50;
253
+ }
254
+
255
+ .status-dot.disconnected {
256
+ background-color: #f44336;
257
+ }
258
+
259
+ .status-dot.connecting {
260
+ background-color: #ff9800;
261
+ animation: pulse 1.5s infinite;
262
+ }
263
+
264
+ @keyframes pulse {
265
+ 0% {
266
+ opacity: 0.6;
267
+ }
268
+ 50% {
269
+ opacity: 1;
270
+ }
271
+ 100% {
272
+ opacity: 0.6;
273
+ }
274
+ }
275
+
276
+ /* Mouse logo animation */
277
+ .mouse-logo {
278
+ position: relative;
279
+ width: 40px;
280
+ height: 40px;
281
+ }
282
+
283
+ .mouse-ears {
284
+ position: absolute;
285
+ width: 15px;
286
+ height: 15px;
287
+ background-color: var(--primary-color);
288
+ border-radius: 50%;
289
+ }
290
+
291
+ .mouse-ear-left {
292
+ top: 0;
293
+ left: 5px;
294
+ }
295
+
296
+ .mouse-ear-right {
297
+ top: 0;
298
+ right: 5px;
299
+ }
300
+
301
+ .mouse-face {
302
+ position: absolute;
303
+ top: 10px;
304
+ left: 5px;
305
+ width: 30px;
306
+ height: 30px;
307
+ background-color: var(--secondary-color);
308
+ border-radius: 50%;
309
+ }
310
+ </style>
311
+ </head>
312
+
313
+ <body>
314
+ <!-- Add toast element after body opening tag -->
315
+ <div id="error-toast" class="toast"></div>
316
+ <div class="container">
317
+ <div class="header">
318
+ <div class="logo">
319
+ <div class="mouse-logo">
320
+ <div class="mouse-ears mouse-ear-left"></div>
321
+ <div class="mouse-ears mouse-ear-right"></div>
322
+ <div class="mouse-face"></div>
323
+ </div>
324
+ <h1>MOUSE μŒμ„± μ±—</h1>
325
+ </div>
326
+ <div class="status-indicator">
327
+ <div id="status-dot" class="status-dot disconnected"></div>
328
+ <span id="status-text">μ—°κ²° λŒ€κΈ° 쀑</span>
329
+ </div>
330
+ </div>
331
+ <div class="chat-container">
332
+ <div class="chat-messages" id="chat-messages"></div>
333
+ </div>
334
+ <div class="controls">
335
+ <button id="start-button">λŒ€ν™” μ‹œμž‘</button>
336
+ </div>
337
+ </div>
338
+ <audio id="audio-output"></audio>
339
+
340
+ <script>
341
+ let peerConnection;
342
+ let webrtc_id;
343
+ const audioOutput = document.getElementById('audio-output');
344
+ const startButton = document.getElementById('start-button');
345
+ const chatMessages = document.getElementById('chat-messages');
346
+ const statusDot = document.getElementById('status-dot');
347
+ const statusText = document.getElementById('status-text');
348
+ let audioLevel = 0;
349
+ let animationFrame;
350
+ let audioContext, analyser, audioSource;
351
+
352
+ function updateStatus(state) {
353
+ statusDot.className = 'status-dot ' + state;
354
+ if (state === 'connected') {
355
+ statusText.textContent = '연결됨';
356
+ } else if (state === 'connecting') {
357
+ statusText.textContent = 'μ—°κ²° 쀑...';
358
+ } else {
359
+ statusText.textContent = 'μ—°κ²° λŒ€κΈ° 쀑';
360
+ }
361
+ }
362
+
363
+ function updateButtonState() {
364
+ const button = document.getElementById('start-button');
365
+ if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
366
+ button.innerHTML = `
367
+ <div class="icon-with-spinner">
368
+ <div class="spinner"></div>
369
+ <span>μ—°κ²° 쀑...</span>
370
+ </div>
371
+ `;
372
+ updateStatus('connecting');
373
+ } else if (peerConnection && peerConnection.connectionState === 'connected') {
374
+ button.innerHTML = `
375
+ <div class="icon-with-spinner">
376
+ <div class="audio-visualizer" id="audio-visualizer">
377
+ <div class="visualizer-bar"></div>
378
+ <div class="visualizer-bar"></div>
379
+ <div class="visualizer-bar"></div>
380
+ <div class="visualizer-bar"></div>
381
+ <div class="visualizer-bar"></div>
382
+ </div>
383
+ <span>λŒ€ν™” μ’…λ£Œ</span>
384
+ </div>
385
+ `;
386
+ updateStatus('connected');
387
+ } else {
388
+ button.innerHTML = 'λŒ€ν™” μ‹œμž‘';
389
+ updateStatus('disconnected');
390
+ }
391
+ }
392
+
393
+ function setupAudioVisualization(stream) {
394
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
395
+ analyser = audioContext.createAnalyser();
396
+ audioSource = audioContext.createMediaStreamSource(stream);
397
+ audioSource.connect(analyser);
398
+ analyser.fftSize = 256;
399
+ const bufferLength = analyser.frequencyBinCount;
400
+ const dataArray = new Uint8Array(bufferLength);
401
+
402
+ const visualizerBars = document.querySelectorAll('.visualizer-bar');
403
+ const barCount = visualizerBars.length;
404
+
405
+ function updateAudioLevel() {
406
+ analyser.getByteFrequencyData(dataArray);
407
+
408
+ // Calculate levels for each bar from different frequency ranges
409
+ for (let i = 0; i < barCount; i++) {
410
+ // Divide frequency data into segments for each bar
411
+ const start = Math.floor(i * (bufferLength / barCount));
412
+ const end = Math.floor((i + 1) * (bufferLength / barCount));
413
+
414
+ let sum = 0;
415
+ for (let j = start; j < end; j++) {
416
+ sum += dataArray[j];
417
+ }
418
+
419
+ const average = sum / (end - start) / 255;
420
+ // Add some minimum height and scale effect
421
+ const scaleY = 0.1 + average * 0.9;
422
+ visualizerBars[i].style.transform = `scaleY(${scaleY})`;
423
+ }
424
+
425
+ animationFrame = requestAnimationFrame(updateAudioLevel);
426
+ }
427
+
428
+ updateAudioLevel();
429
+ }
430
+
431
+ function showError(message) {
432
+ const toast = document.getElementById('error-toast');
433
+ toast.textContent = message;
434
+ toast.className = 'toast error';
435
+ toast.style.display = 'block';
436
+ // Hide toast after 5 seconds
437
+ setTimeout(() => {
438
+ toast.style.display = 'none';
439
+ }, 5000);
440
+ }
441
+
442
+ async function setupWebRTC() {
443
+ const config = __RTC_CONFIGURATION__;
444
+ peerConnection = new RTCPeerConnection(config);
445
+ const timeoutId = setTimeout(() => {
446
+ const toast = document.getElementById('error-toast');
447
+ toast.textContent = "연결이 ν‰μ†Œλ³΄λ‹€ 였래 걸리고 μžˆμŠ΅λ‹ˆλ‹€. VPN을 μ‚¬μš© μ€‘μ΄μ‹ κ°€μš”?";
448
+ toast.className = 'toast warning';
449
+ toast.style.display = 'block';
450
+ // Hide warning after 5 seconds
451
+ setTimeout(() => {
452
+ toast.style.display = 'none';
453
+ }, 5000);
454
+ }, 5000);
455
+
456
+ try {
457
+ const stream = await navigator.mediaDevices.getUserMedia({
458
+ audio: true
459
+ });
460
+ setupAudioVisualization(stream);
461
+ stream.getTracks().forEach(track => {
462
+ peerConnection.addTrack(track, stream);
463
+ });
464
+ peerConnection.addEventListener('track', (evt) => {
465
+ if (audioOutput.srcObject !== evt.streams[0]) {
466
+ audioOutput.srcObject = evt.streams[0];
467
+ audioOutput.play();
468
+ }
469
+ });
470
+ const dataChannel = peerConnection.createDataChannel('text');
471
+ dataChannel.onmessage = (event) => {
472
+ const eventJson = JSON.parse(event.data);
473
+ if (eventJson.type === "error") {
474
+ showError(eventJson.message);
475
+ }
476
+ };
477
+ const offer = await peerConnection.createOffer();
478
+ await peerConnection.setLocalDescription(offer);
479
+ await new Promise((resolve) => {
480
+ if (peerConnection.iceGatheringState === "complete") {
481
+ resolve();
482
+ } else {
483
+ const checkState = () => {
484
+ if (peerConnection.iceGatheringState === "complete") {
485
+ peerConnection.removeEventListener("icegatheringstatechange", checkState);
486
+ resolve();
487
+ }
488
+ };
489
+ peerConnection.addEventListener("icegatheringstatechange", checkState);
490
+ }
491
+ });
492
+ peerConnection.addEventListener('connectionstatechange', () => {
493
+ console.log('connectionstatechange', peerConnection.connectionState);
494
+ if (peerConnection.connectionState === 'connected') {
495
+ clearTimeout(timeoutId);
496
+ const toast = document.getElementById('error-toast');
497
+ toast.style.display = 'none';
498
+ }
499
+ updateButtonState();
500
+ });
501
+ webrtc_id = Math.random().toString(36).substring(7);
502
+ const response = await fetch('/webrtc/offer', {
503
+ method: 'POST',
504
+ headers: { 'Content-Type': 'application/json' },
505
+ body: JSON.stringify({
506
+ sdp: peerConnection.localDescription.sdp,
507
+ type: peerConnection.localDescription.type,
508
+ webrtc_id: webrtc_id
509
+ })
510
+ });
511
+ const serverResponse = await response.json();
512
+ if (serverResponse.status === 'failed') {
513
+ showError(serverResponse.meta.error === 'concurrency_limit_reached'
514
+ ? `λ„ˆλ¬΄ λ§Žμ€ μ—°κ²°μž…λ‹ˆλ‹€. μ΅œλŒ€ ν•œλ„λŠ” ${serverResponse.meta.limit} μž…λ‹ˆλ‹€.`
515
+ : serverResponse.meta.error);
516
+ stop();
517
+ return;
518
+ }
519
+ await peerConnection.setRemoteDescription(serverResponse);
520
+ const eventSource = new EventSource('/outputs?webrtc_id=' + webrtc_id);
521
+ eventSource.addEventListener("output", (event) => {
522
+ const eventJson = JSON.parse(event.data);
523
+ addMessage("assistant", eventJson.content);
524
+ });
525
+ } catch (err) {
526
+ clearTimeout(timeoutId);
527
+ console.error('Error setting up WebRTC:', err);
528
+ showError('연결을 μ„€μ •ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.');
529
+ stop();
530
+ }
531
+ }
532
+
533
+ function addMessage(role, content) {
534
+ const messageDiv = document.createElement('div');
535
+ messageDiv.classList.add('message', role);
536
+ messageDiv.textContent = content;
537
+ chatMessages.appendChild(messageDiv);
538
+ chatMessages.scrollTop = chatMessages.scrollHeight;
539
+ }
540
+
541
+ function stop() {
542
+ if (animationFrame) {
543
+ cancelAnimationFrame(animationFrame);
544
+ }
545
+ if (audioContext) {
546
+ audioContext.close();
547
+ audioContext = null;
548
+ analyser = null;
549
+ audioSource = null;
550
+ }
551
+ if (peerConnection) {
552
+ if (peerConnection.getTransceivers) {
553
+ peerConnection.getTransceivers().forEach(transceiver => {
554
+ if (transceiver.stop) {
555
+ transceiver.stop();
556
+ }
557
+ });
558
+ }
559
+ if (peerConnection.getSenders) {
560
+ peerConnection.getSenders().forEach(sender => {
561
+ if (sender.track && sender.track.stop) sender.track.stop();
562
+ });
563
+ }
564
+ console.log('closing');
565
+ peerConnection.close();
566
+ }
567
+ updateButtonState();
568
+ audioLevel = 0;
569
+ }
570
+
571
+ startButton.addEventListener('click', () => {
572
+ console.log('clicked');
573
+ console.log(peerConnection, peerConnection?.connectionState);
574
+ if (!peerConnection || peerConnection.connectionState !== 'connected') {
575
+ setupWebRTC();
576
+ } else {
577
+ console.log('stopping');
578
+ stop();
579
+ }
580
+ });
581
+ </script>
582
+ </body>
583
+
584
+ </html>