seawolf2357 commited on
Commit
8b89133
·
verified ·
1 Parent(s): d142e85

Delete index.html

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