seawolf2357 commited on
Commit
f5422a1
Β·
verified Β·
1 Parent(s): 2bc8e7a

Update index-backup.html

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