maitrang04 commited on
Commit
e1c13ba
·
verified ·
1 Parent(s): c606696

Update frontend/call_desktop.html

Browse files
Files changed (1) hide show
  1. frontend/call_desktop.html +518 -352
frontend/call_desktop.html CHANGED
@@ -5,199 +5,205 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>PINE AI - VNPT Smart Contact Center</title>
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
9
  <style>
10
- :root {
11
- --vnpt-blue: #00a1e4;
12
- --vnpt-dark: #0072bc;
13
- --bg-body: #f4f7fa;
14
- --chat-bot: #ffffff;
15
- --chat-user: #e3f2fd;
 
16
  }
17
-
18
- body {
19
- font-family: 'Inter', sans-serif;
20
- margin: 0; display: flex; height: 100vh; background: var(--bg-body);
21
- color: #333; overflow: hidden;
22
- }
23
-
24
  /* --- SIDEBAR --- */
25
- .sidebar {
26
- width: 320px;
27
- background: linear-gradient(160deg, var(--vnpt-dark) 0%, var(--vnpt-blue) 100%);
28
- display: flex; flex-direction: column; align-items: center;
29
- padding: 40px 20px; color: white;
30
- box-shadow: 4px 0 20px rgba(0,0,0,0.1);
31
- z-index: 10;
32
- overflow-y: auto; /* Cho phép cuộn nếu màn hình nhỏ */
33
- }
34
-
35
- .mascot-container {
36
- width: 120px; height: 120px; /* Thu nhỏ chút để nhường chỗ cho Metrics */
37
- background: rgba(255,255,255,0.1);
38
- border-radius: 50%; display: flex; align-items: center; justify-content: center;
39
- margin-bottom: 15px; border: 3px solid rgba(255,255,255,0.3);
40
- transition: 0.3s;
41
- position: relative;
42
- flex-shrink: 0;
43
- }
44
 
45
- .mascot-container.active {
46
- border-color: #fff;
47
- box-shadow: 0 0 30px rgba(255,255,255,0.6);
48
- transform: scale(1.05);
49
- }
50
-
51
  .mascot-container img { width: 70%; }
52
-
53
  .brand-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: 1px; margin-top: 5px; }
54
  .brand-sub { font-size: 12px; opacity: 0.8; margin-top: 2px; font-weight: 300; margin-bottom: 20px; }
55
-
56
- .timer-box {
57
- font-family: 'Courier New', monospace;
58
- font-size: 14px;
59
- color: #fff;
60
- background: rgba(0,0,0,0.2);
61
- padding: 5px 15px;
62
- border-radius: 20px;
63
- margin-top: 10px;
64
- display: none;
65
- }
66
-
67
- /* VISUALIZER */
68
- .visualizer {
69
- display: flex; gap: 4px; height: 40px; margin-top: 20px; align-items: center; flex-shrink: 0;
70
- }
71
- .bar {
72
- width: 5px; height: 5px; background: rgba(255,255,255,0.6);
73
- border-radius: 5px; transition: 0.1s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
-
76
- /* --- MAIN CONTENT --- */
77
- .main-content {
78
- flex: 1; display: flex; flex-direction: column;
79
- background: white; border-radius: 30px 0 0 30px;
80
- margin-left: -20px; z-index: 20; overflow: hidden;
81
- box-shadow: -5px 0 20px rgba(0,0,0,0.05);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
-
84
- .top-bar {
85
- padding: 20px 40px; display: flex; justify-content: space-between; align-items: center;
86
- border-bottom: 1px solid #eee; background: white;
 
 
 
 
 
 
 
 
87
  }
 
 
 
 
 
 
88
 
 
 
 
 
89
  .call-input { display: flex; gap: 15px; align-items: center; }
90
  .input-group { display: flex; align-items: center; gap: 10px; background: #f0f2f5; padding: 5px 15px; border-radius: 12px; }
91
  .input-group label { font-size: 13px; font-weight: 600; color: #555; }
92
- .input-group input {
93
- border: none; background: transparent; width: 50px; font-weight: bold;
94
- color: var(--vnpt-dark); outline: none; font-size: 16px; text-align: center;
95
- }
96
-
97
- #btnCall {
98
- background: #27ae60; color: white; border: none; padding: 12px 25px;
99
- border-radius: 12px; font-weight: 600; cursor: pointer; transition: 0.3s;
100
- display: flex; align-items: center; gap: 8px;
101
- }
102
- #btnCall:hover { background: #219150; transform: translateY(-2px); }
103
- #btnCall:disabled { background: #ccc; cursor: not-allowed; transform: none; }
104
-
105
- /* CHAT LOG */
106
- .chat-area {
107
- flex: 1; padding: 40px; overflow-y: auto; display: flex;
108
- flex-direction: column; gap: 15px; scroll-behavior: smooth;
109
- }
110
-
111
- .msg {
112
- max-width: 70%; padding: 12px 18px; border-radius: 18px;
113
- font-size: 15px; line-height: 1.5; position: relative;
114
- animation: fadeIn 0.3s ease;
115
- }
116
  @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
117
-
118
- .msg.bot {
119
- background: var(--chat-bot); border: 1px solid #eee;
120
- align-self: flex-start; border-bottom-left-radius: 2px; color: #444;
121
- box-shadow: 0 2px 10px rgba(0,0,0,0.03);
122
- }
123
- .msg.user {
124
- background: var(--chat-user); color: #0d47a1;
125
- align-self: flex-end; border-bottom-right-radius: 2px;
126
- }
127
- .msg.info {
128
- align-self: center; font-size: 12px; color: #999;
129
- background: #f5f5f5; padding: 5px 15px; border-radius: 20px;
130
- }
131
-
132
- /* CONTROLS */
133
- .control-panel {
134
- padding: 30px; display: flex; flex-direction: column;
135
- align-items: center; border-top: 1px solid #f0f0f0;
136
- }
137
-
138
- #recordBtn {
139
- width: 70px; height: 70px; border-radius: 50%; border: none;
140
- background: var(--vnpt-blue); color: white; font-size: 24px;
141
- cursor: pointer; box-shadow: 0 5px 15px rgba(0,161,228,0.3);
142
- transition: 0.2s; display: flex; align-items: center; justify-content: center;
143
- }
144
 
145
- #recordBtn.recording {
146
- background: #ff4757; transform: scale(1.1);
147
- box-shadow: 0 0 0 8px rgba(255, 71, 87, 0.2);
148
- animation: pulse 1.5s infinite;
149
- }
150
  @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.4); } 70% { box-shadow: 0 0 0 15px rgba(255, 71, 87, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 71, 87, 0); } }
151
-
152
- .status-text { margin-top: 15px; font-weight: 600; color: #888; font-size: 14px; }
153
- .status-text.error { color: #ff4757; }
154
-
155
- #stopAiBtn { display: none; }
156
-
157
- /* ========================================= */
158
- /* --- CHANGED: UX DASHBOARD (INSIDE SIDEBAR) --- */
159
- /* ========================================= */
160
- .ux-dashboard {
161
- width: 100%;
162
- background: rgba(255, 255, 255, 0.1); /* Glassmorphism */
163
- border: 1px solid rgba(255, 255, 255, 0.2);
164
- border-radius: 16px;
165
- padding: 15px;
166
- box-sizing: border-box;
167
- margin-top: 10px;
168
- margin-bottom: auto; /* Đẩy xuống dưới nếu có khoảng trống */
169
- animation: fadeIn 0.5s ease;
170
- }
171
-
172
- .ux-header {
173
- font-size: 12px; font-weight: 700; color: #fff;
174
- margin-bottom: 12px; padding-bottom: 8px;
175
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
176
- display: flex; justify-content: space-between; align-items: center;
177
- }
178
-
179
- .metric-item { margin-bottom: 12px; }
180
- .metric-label { font-size: 11px; color: rgba(255, 255, 255, 0.7); margin-bottom: 3px; font-weight: 500; text-transform: uppercase; }
181
- .metric-value { font-size: 16px; font-weight: 700; color: #fff; display: flex; justify-content: space-between; align-items: baseline; }
182
- .metric-unit { font-size: 11px; font-weight: 400; color: rgba(255, 255, 255, 0.6); }
183
-
184
- /* Sentiment Bar (Dark theme compatible) */
185
- .sentiment-track {
186
- width: 100%; height: 6px; background: rgba(0, 0, 0, 0.3); border-radius: 3px; overflow: hidden; margin-top: 5px;
187
- }
188
- .sentiment-fill {
189
- height: 100%; width: 50%; background: #f1c40f; transition: width 0.5s ease, background 0.5s ease;
190
- }
191
-
192
- .detail-row {
193
- display: flex; justify-content: space-between; font-size: 10px; color: rgba(255, 255, 255, 0.6); margin-top: 2px;
194
  }
195
-
196
- /* Override Colors for Sidebar Context */
197
- .val-good { color: #2ecc71 !important; }
198
- .val-avg { color: #f1c40f !important; }
199
- .val-bad { color: #ff6b6b !important; }
200
-
201
  </style>
202
  </head>
203
  <body>
@@ -212,54 +218,62 @@
212
  <div class="ux-dashboard">
213
  <div class="ux-header">
214
  <span><i class="fas fa-chart-line"></i> UX METRICS</span>
215
- <span style="font-size: 9px; background: rgba(255,255,255,0.2); padding: 2px 6px; border-radius: 4px;">LIVE</span>
216
  </div>
217
-
218
  <div class="metric-item">
219
- <div class="metric-label">ĐỘ TRỄ (LATENCY)</div>
220
- <div class="metric-value" id="ux-latency">0.00 <span class="metric-unit">s</span></div>
221
- <div class="detail-row">
222
- <span>STT: <b id="ux-stt">0</b>s</span>
223
- <span>Logic: <b id="ux-logic">0</b>s</span>
 
 
 
 
 
 
 
 
 
 
224
  </div>
225
  </div>
226
 
227
  <div class="metric-item">
228
- <div class="metric-label">CẢM XÚC</div>
229
- <div class="metric-value" id="ux-sentiment-text" style="font-size: 14px;">---</div>
230
  <div class="sentiment-track">
231
  <div class="sentiment-fill" id="ux-sentiment-bar" style="width: 50%;"></div>
232
  </div>
233
  </div>
234
-
235
  <div class="metric-item" style="margin-bottom: 0;">
236
- <div class="metric-label">Ý ĐỊNH</div>
237
- <div class="metric-value" id="ux-intent" style="color: #4fc3f7; font-size: 14px;">---</div>
238
  </div>
239
  </div>
240
  <div style="flex: 1;"></div>
241
-
242
  <div class="visualizer" id="visualizer">
243
  <div class="bar"></div><div class="bar"></div><div class="bar"></div>
244
  <div class="bar"></div><div class="bar"></div><div class="bar"></div>
245
  <div class="bar"></div><div class="bar"></div><div class="bar"></div>
246
  </div>
247
-
248
- <div style="margin-top: 20px; font-size: 11px; opacity: 0.6; text-align: center;">
249
- © 2024 VNPT GROUP
250
- </div>
251
- <div class="timer-box" id="aiTimer"></div>
252
- <button id="stopAiBtn"></button>
253
  </div>
254
 
255
  <div class="main-content">
256
  <div class="top-bar">
257
  <div class="call-input">
 
 
 
 
 
258
  <div class="input-group">
259
  <label><i class="fas fa-user"></i> ID KHÁCH:</label>
260
  <input type="number" id="customerId" value="1" min="1" max="10">
261
  </div>
262
- <button id="btnCall" onclick="startCall()">
263
  <i class="fas fa-phone-alt"></i> BẮT ĐẦU GỌI
264
  </button>
265
  </div>
@@ -273,25 +287,86 @@
273
  </div>
274
 
275
  <div class="control-panel" id="chatArea">
276
- <button id="recordBtn" onmousedown="startRecording()" onmouseup="stopRecording()" onmouseleave="stopRecording()" disabled>
277
  <i class="fas fa-microphone"></i>
278
  </button>
279
  <div class="status-text" id="status">Sẵn sàng kết nối</div>
280
  </div>
281
  </div>
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  <script>
284
- const API_URL = "";
 
285
  let mediaRecorder;
286
  let audioChunks = [];
287
-
288
- // --- LOGIC STREAMING (QUEUE) ---
289
  let audioQueue = [];
290
  let isPlaying = false;
 
291
  let currentAudioObject = null;
292
- let audioContext, analyser, dataArray, animationId;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
- // --- 1. VISUALIZER ---
295
  function initVisualizer(stream) {
296
  if(!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)();
297
  const source = audioContext.createMediaStreamSource(stream);
@@ -306,217 +381,308 @@
306
  if (!analyser) return;
307
  requestAnimationFrame(drawVisualizer);
308
  analyser.getByteFrequencyData(dataArray);
309
- const bars = document.querySelectorAll('.bar');
310
- bars.forEach((bar, i) => {
311
  const h = Math.max(5, dataArray[i] / 255 * 40);
312
  bar.style.height = h + 'px';
313
  });
314
  }
315
 
316
- // --- 2. AUDIO PLAYER ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  async function processAudioQueue() {
318
  if (isPlaying || audioQueue.length === 0) return;
319
  isPlaying = true;
320
-
321
  document.getElementById('mascotBox').classList.add('active');
322
  document.getElementById('status').innerText = "PINE đang trả lời...";
323
-
324
  const base64Data = audioQueue.shift();
325
  const audio = new Audio("data:audio/wav;base64," + base64Data);
326
  currentAudioObject = audio;
327
-
328
  audio.onended = () => {
329
  isPlaying = false;
330
  if (audioQueue.length === 0) {
331
  document.getElementById('mascotBox').classList.remove('active');
332
- document.getElementById('status').innerText = "Đến lượt bạn nói...";
 
333
  }
334
  processAudioQueue();
335
  };
336
-
337
- audio.onerror = (e) => {
338
- console.error("Lỗi phát Audio:", e);
339
- isPlaying = false;
340
- processAudioQueue();
341
- };
342
-
343
  try { await audio.play(); } catch (e) { isPlaying = false; }
344
  }
345
 
346
- // --- 3. STREAM READER ---
347
- async function readStream(response) {
348
- const reader = response.body.getReader();
349
- const decoder = new TextDecoder();
350
- let buffer = "";
351
-
352
- while (true) {
353
- const { done, value } = await reader.read();
354
- if (done) break;
355
-
356
- buffer += decoder.decode(value, { stream: true });
357
- const lines = buffer.split("\n");
358
- buffer = lines.pop();
359
-
360
- for (const line of lines) {
361
- if (!line.trim()) continue;
362
- try {
363
- const data = JSON.parse(line);
364
-
365
- if (data.user_text) log(data.user_text, 'user');
366
- if (data.bot_text) log(data.bot_text, 'bot');
367
-
368
- if (data.audio_base64) {
369
- audioQueue.push(data.audio_base64);
370
- processAudioQueue();
371
- }
372
-
373
- if (data.type === "metrics_update") {
374
- updateUXDashboard(data.data);
375
- }
376
-
377
- } catch (e) { console.error("Parse JSON Lỗi:", e); }
378
- }
379
- }
380
- }
381
-
382
- // --- NEW: HÀM CẬP NHẬT UI METRICS ---
383
  function updateUXDashboard(metrics) {
384
- // 1. Latency
385
- const latVal = document.getElementById("ux-latency");
386
- latVal.innerText = metrics.latency_total;
387
 
388
- latVal.className = "metric-value " + (metrics.latency_total < 1.5 ? "val-good" : (metrics.latency_total < 3.0 ? "val-avg" : "val-bad"));
389
-
390
- document.getElementById("ux-stt").innerText = metrics.latency_stt;
391
- document.getElementById("ux-logic").innerText = metrics.latency_logic;
392
-
393
- // 2. Sentiment
394
  const score = metrics.sentiment;
395
  const bar = document.getElementById("ux-sentiment-bar");
396
  const text = document.getElementById("ux-sentiment-text");
397
-
398
  const percent = ((score + 1) / 2) * 100;
399
  bar.style.width = `${percent}%`;
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
- if (score > 0.3) {
402
- text.innerText = "Tích cực 😄";
403
- text.style.color = "#2ecc71";
404
- bar.style.background = "#2ecc71";
405
- } else if (score < -0.3) {
406
- text.innerText = "Tiêu cực 😡";
407
- text.style.color = "#ff6b6b";
408
- bar.style.background = "#ff6b6b";
409
- } else {
410
- text.innerText = "Trung tính 😐";
411
- text.style.color = "#f1c40f";
412
- bar.style.background = "#f1c40f";
413
- }
414
-
415
- // 3. Intent
416
- document.getElementById("ux-intent").innerText = metrics.intent;
417
  }
418
 
419
- // --- 4. GIAO DIỆN ---
420
- function log(msg, type='info') {
421
- const logArea = document.getElementById('logArea');
422
- if (document.querySelector('.msg.info')) {
423
- // document.getElementById('logArea').innerHTML = '';
 
 
 
 
 
 
 
 
424
  }
 
425
 
426
- const div = document.createElement('div');
427
- div.className = `msg ${type}`;
428
- div.innerHTML = msg;
429
- logArea.appendChild(div);
430
- logArea.scrollTo({ top: logArea.scrollHeight, behavior: 'smooth' });
431
  }
432
 
433
- // --- 5. HÀM START CALL ---
434
- async function startCall() {
435
- const cid = document.getElementById('customerId').value;
436
- const btnCall = document.getElementById('btnCall');
437
-
438
- btnCall.disabled = true;
439
- btnCall.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ĐANG KẾT NỐI...';
440
- document.getElementById('logArea').innerHTML = '';
441
- document.getElementById('status').innerText = "Đang khởi tạo...";
442
 
443
- audioQueue = [];
444
- isPlaying = false;
445
- if(currentAudioObject) currentAudioObject.pause();
 
446
 
447
- try {
448
- const formData = new FormData();
449
- formData.append('customer_id', cid);
450
-
451
- const res = await fetch(`${API_URL}/start-call`, { method: 'POST', body: formData });
452
- await readStream(res);
453
-
454
- btnCall.innerHTML = '<i class="fas fa-phone-slash"></i> ĐANG GỌI';
455
- btnCall.style.background = '#95a5a6';
456
- document.getElementById('recordBtn').disabled = false;
457
- document.getElementById('status').innerText = "Nhấn giữ Mic để nói";
458
-
459
- } catch (e) {
460
- console.error(e);
461
- log("Lỗi kết nối Server! Kiểm tra Terminal.", 'info');
462
- btnCall.disabled = false;
463
- btnCall.innerHTML = '<i class="fas fa-redo"></i> THỬ LẠI';
464
  }
 
 
 
 
465
  }
466
 
467
- // --- 6. HÀM GHI ÂM ---
468
  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
469
  initVisualizer(stream);
470
  mediaRecorder = new MediaRecorder(stream);
471
  mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
472
  mediaRecorder.onstop = sendAudio;
473
  }).catch(err => {
474
- log("Lỗi: Không quyền truy cập Micro", 'info');
475
  document.getElementById('status').classList.add('error');
476
  });
477
 
478
- function startRecording() {
479
- if (!mediaRecorder) return;
480
-
481
- if(currentAudioObject) {
482
- currentAudioObject.pause();
483
- currentAudioObject = null;
484
- }
485
- audioQueue = [];
486
- isPlaying = false;
487
- document.getElementById('mascotBox').classList.remove('active');
488
-
489
- audioChunks = [];
490
- mediaRecorder.start();
491
- document.getElementById('recordBtn').classList.add('recording');
492
- document.getElementById('status').innerText = "Đang nghe bạn nói...";
493
- }
494
-
495
- function stopRecording() {
496
- if (mediaRecorder && mediaRecorder.state === "recording") {
497
- mediaRecorder.stop();
498
- document.getElementById('recordBtn').classList.remove('recording');
499
- document.getElementById('status').innerText = "Đang xử lý...";
500
- }
501
- }
502
-
503
  async function sendAudio() {
504
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
505
- if (audioBlob.size < 1000) {
506
- document.getElementById('status').innerText = "Nói quá ngắn, thử lại.";
507
- return;
508
- }
509
-
510
  const cid = document.getElementById('customerId').value;
511
  const formData = new FormData();
512
  formData.append('file', audioBlob, 'voice.webm');
513
  formData.append('customer_id', cid);
514
-
 
515
  try {
516
- const res = await fetch(`${API_URL}/chat-voice`, { method: 'POST', body: formData });
 
517
  await readStream(res);
518
  } catch (e) {
519
- log("Lỗi xử lý AI", 'info');
 
 
 
 
520
  }
521
  }
522
  </script>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>PINE AI - VNPT Smart Contact Center</title>
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
9
  <style>
10
+ /* --- CORE VARIABLES --- */
11
+ :root {
12
+ --vnpt-blue: #00a1e4;
13
+ --vnpt-dark: #0072bc;
14
+ --bg-body: #f4f7fa;
15
+ --chat-bot: #ffffff;
16
+ --chat-user: #e3f2fd;
17
  }
18
+
19
+ body { font-family: 'Inter', sans-serif; margin: 0; display: flex; height: 100vh; background: var(--bg-body); color: #333; overflow: hidden; }
20
+
 
 
 
 
21
  /* --- SIDEBAR --- */
22
+ .sidebar { width: 320px; background: linear-gradient(160deg, var(--vnpt-dark) 0%, var(--vnpt-blue) 100%); display: flex; flex-direction: column; align-items: center; padding: 40px 20px; color: white; box-shadow: 4px 0 20px rgba(0,0,0,0.1); z-index: 10; overflow-y: auto; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ .mascot-container { width: 120px; height: 120px; background: rgba(255,255,255,0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 15px; border: 3px solid rgba(255,255,255,0.3); transition: 0.3s; position: relative; flex-shrink: 0; }
25
+ .mascot-container.active { border-color: #fff; box-shadow: 0 0 30px rgba(255,255,255,0.6); transform: scale(1.05); }
 
 
 
 
26
  .mascot-container img { width: 70%; }
27
+
28
  .brand-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: 1px; margin-top: 5px; }
29
  .brand-sub { font-size: 12px; opacity: 0.8; margin-top: 2px; font-weight: 300; margin-bottom: 20px; }
30
+
31
+ .visualizer { display: flex; gap: 4px; height: 40px; margin-top: 20px; align-items: center; flex-shrink: 0; }
32
+ .bar { width: 5px; height: 5px; background: rgba(255,255,255,0.6); border-radius: 5px; transition: 0.1s; }
33
+
34
+ /* --- UX DASHBOARD (NÂNG CẤP GIAO DIỆN) --- */
35
+ .ux-dashboard {
36
+ width: 100%;
37
+ background: rgba(255, 255, 255, 0.15); /* Nền sáng hơn */
38
+ backdrop-filter: blur(12px);
39
+ -webkit-backdrop-filter: blur(12px);
40
+ border: 1px solid rgba(255, 255, 255, 0.3);
41
+ border-radius: 20px;
42
+ padding: 20px;
43
+ box-sizing: border-box;
44
+ margin-top: 15px;
45
+ margin-bottom: auto;
46
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
47
+ animation: fadeIn 0.5s ease;
48
+ }
49
+
50
+ .ux-header {
51
+ font-size: 13px;
52
+ font-weight: 800;
53
+ color: #fff;
54
+ margin-bottom: 15px;
55
+ padding-bottom: 10px;
56
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
57
+ display: flex;
58
+ justify-content: space-between;
59
+ align-items: center;
60
+ letter-spacing: 0.5px;
61
+ text-shadow: 0 1px 3px rgba(0,0,0,0.2);
62
+ }
63
+
64
+ .metric-item { margin-bottom: 18px; }
65
+
66
+ .metric-label {
67
+ font-size: 11px;
68
+ color: rgba(255, 255, 255, 0.9);
69
+ margin-bottom: 4px;
70
+ font-weight: 600;
71
+ text-transform: uppercase;
72
+ letter-spacing: 0.5px;
73
+ }
74
+
75
+ .metric-value {
76
+ font-size: 26px;
77
+ font-weight: 800;
78
+ color: #fff;
79
+ display: flex;
80
+ justify-content: space-between;
81
+ align-items: baseline;
82
+ text-shadow: 0 2px 5px rgba(0,0,0,0.15);
83
+ }
84
+
85
+ /* --- STYLE RIÊNG CHO PHẦN Ý ĐỊNH (CAPSULE STYLE - NO ICON) --- */
86
+ #ux-intent {
87
+ background: rgba(0, 161, 228, 0.15); /* Nền xanh rất nhạt */
88
+ border: 1px solid rgba(0, 229, 255, 0.4); /* Viền sáng */
89
+ color: #ffffff;
90
+ font-size: 17px;
91
+ font-weight: 700;
92
+ text-align: center;
93
+ padding: 10px 15px;
94
+ border-radius: 30px; /* Bo góc tròn như viên thuốc */
95
+ margin-top: 8px;
96
+ text-transform: capitalize;
97
+ box-shadow: 0 0 15px rgba(0, 161, 228, 0.15); /* Bóng phát sáng nhẹ */
98
+ transition: all 0.3s ease;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ min-height: 24px;
103
  }
104
+
105
+ /* Hiệu ứng rung nhẹ khi đổi ý định */
106
+ @keyframes pop { 50% { transform: scale(1.05); } }
107
+ .pop-anim { animation: pop 0.2s ease; }
108
+
109
+ .metric-unit {
110
+ font-size: 12px;
111
+ font-weight: 500;
112
+ color: rgba(255, 255, 255, 0.7);
113
+ margin-left: 5px;
114
+ }
115
+
116
+ /* Thanh cảm xúc đẹp hơn */
117
+ .sentiment-track {
118
+ width: 100%;
119
+ height: 10px;
120
+ background: rgba(0, 0, 0, 0.25);
121
+ border-radius: 10px;
122
+ overflow: hidden;
123
+ margin-top: 8px;
124
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
125
+ }
126
+
127
+ .sentiment-fill {
128
+ height: 100%;
129
+ width: 50%;
130
+ background: #f1c40f;
131
+ border-radius: 10px;
132
+ transition: width 0.5s cubic-bezier(0.25, 0.8, 0.25, 1), background 0.5s ease;
133
+ box-shadow: 0 0 10px rgba(241, 196, 15, 0.4);
134
  }
135
+
136
+ /* Hộp Latency con */
137
+ .latency-group { display: flex; gap: 10px; margin-top: 10px; }
138
+
139
+ .latency-box {
140
+ flex: 1;
141
+ background: rgba(0,0,0,0.2);
142
+ padding: 8px 12px;
143
+ border-radius: 10px;
144
+ border: 1px solid rgba(255,255,255,0.05);
145
+ display: flex;
146
+ flex-direction: column;
147
  }
148
+
149
+ .latency-title { font-size: 10px; color: rgba(255,255,255,0.7); text-transform: uppercase; font-weight: 600; }
150
+ .latency-num { font-size: 16px; font-weight: 700; margin-top: 4px; display: block; }
151
+
152
+ .stt-color { color: #4fc3f7; text-shadow: 0 0 8px rgba(79, 195, 247, 0.3); }
153
+ .ai-color { color: #f1c40f; text-shadow: 0 0 8px rgba(241, 196, 15, 0.3); }
154
 
155
+ /* --- MAIN CONTENT & CHAT --- */
156
+ .main-content { flex: 1; display: flex; flex-direction: column; background: white; border-radius: 30px 0 0 30px; margin-left: -20px; z-index: 20; overflow: hidden; box-shadow: -5px 0 20px rgba(0,0,0,0.05); }
157
+ .top-bar { padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; background: white; }
158
+
159
  .call-input { display: flex; gap: 15px; align-items: center; }
160
  .input-group { display: flex; align-items: center; gap: 10px; background: #f0f2f5; padding: 5px 15px; border-radius: 12px; }
161
  .input-group label { font-size: 13px; font-weight: 600; color: #555; }
162
+ .input-group input { border: none; background: transparent; width: 50px; font-weight: bold; color: var(--vnpt-dark); outline: none; font-size: 16px; text-align: center; }
163
+
164
+ #btnCall { background: #27ae60; color: white; border: none; padding: 12px 25px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: 0.3s; display: flex; align-items: center; gap: 8px; }
165
+ #btnCall:hover { transform: translateY(-2px); } #btnCall:disabled { background: #ccc; cursor: not-allowed; transform: none; }
166
+
167
+ .chat-area { flex: 1; padding: 40px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; scroll-behavior: smooth; }
168
+ .msg { max-width: 70%; padding: 12px 18px; border-radius: 18px; font-size: 15px; line-height: 1.5; position: relative; animation: fadeIn 0.3s ease; }
169
+ .msg.bot { background: var(--chat-bot); border: 1px solid #eee; align-self: flex-start; border-bottom-left-radius: 2px; color: #444; box-shadow: 0 2px 10px rgba(0,0,0,0.03); }
170
+ .msg.user { background: var(--chat-user); color: #0d47a1; align-self: flex-end; border-bottom-right-radius: 2px; }
171
+ .msg.info { align-self: center; font-size: 12px; color: #999; background: #f5f5f5; padding: 5px 15px; border-radius: 20px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ .control-panel { padding: 30px; display: flex; flex-direction: column; align-items: center; border-top: 1px solid #f0f0f0; }
175
+ #recordBtn { width: 70px; height: 70px; border-radius: 50%; border: none; background: var(--vnpt-blue); color: white; font-size: 24px; cursor: pointer; box-shadow: 0 5px 15px rgba(0,161,228,0.3); transition: 0.2s; display: flex; align-items: center; justify-content: center; }
176
+ #recordBtn.recording { background: #ff4757; transform: scale(1.1); box-shadow: 0 0 0 8px rgba(255, 71, 87, 0.2); animation: pulse 1.5s infinite; }
 
 
177
  @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.4); } 70% { box-shadow: 0 0 0 15px rgba(255, 71, 87, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 71, 87, 0); } }
178
+ .status-text { margin-top: 15px; font-weight: 600; color: #888; font-size: 14px; } .status-text.error { color: #ff4757; }
179
+
180
+ /* RATING */
181
+ .rating-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); z-index: 1000; display: none; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
182
+ .rating-box { background: white; width: 350px; padding: 30px; border-radius: 20px; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: popup 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
183
+ @keyframes popup { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
184
+ .rating-box h3 { margin: 0 0 10px; color: var(--vnpt-dark); } .rating-box p { font-size: 13px; color: #666; margin-bottom: 20px; }
185
+ .star-group { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
186
+ .star-group i { font-size: 32px; color: #ddd; cursor: pointer; transition: 0.2s; }
187
+ .star-group i:hover, .star-group i.active { color: #f1c40f; transform: scale(1.2); }
188
+ #ratingNote { width: 100%; padding: 10px; border: 1px solid #eee; border-radius: 10px; font-family: inherit; font-size: 13px; box-sizing: border-box; margin-bottom: 20px; resize: none; height: 60px; }
189
+ .rating-actions { display: flex; gap: 10px; justify-content: center; } .rating-actions button { padding: 10px 20px; border-radius: 8px; border: none; cursor: pointer; font-weight: 600; font-size: 13px; }
190
+ .btn-skip { background: #f0f2f5; color: #666; } .btn-submit { background: var(--vnpt-blue); color: white; } .btn-submit:hover { background: var(--vnpt-dark); }
191
+
192
+ /* TIMER STYLE */
193
+ .call-timer {
194
+ font-family: 'Courier New', monospace;
195
+ font-size: 18px;
196
+ font-weight: 700;
197
+ color: var(--vnpt-dark);
198
+ background: #e3f2fd;
199
+ padding: 8px 15px;
200
+ border-radius: 8px;
201
+ display: flex;
202
+ align-items: center;
203
+ gap: 8px;
204
+ margin-right: 20px;
205
+ border: 1px solid rgba(0, 114, 188, 0.1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
 
 
 
 
 
 
207
  </style>
208
  </head>
209
  <body>
 
218
  <div class="ux-dashboard">
219
  <div class="ux-header">
220
  <span><i class="fas fa-chart-line"></i> UX METRICS</span>
221
+ <span style="font-size: 9px; background: #e74c3c; color: white; padding: 3px 8px; border-radius: 4px; box-shadow: 0 0 8px rgba(231,76,60,0.6);">LIVE</span>
222
  </div>
223
+
224
  <div class="metric-item">
225
+ <div class="metric-label">TỔNG ĐỘ TRỄ (LATENCY)</div>
226
+ <div class="metric-value">
227
+ <span id="ux-latency-total">0.00</span>
228
+ <span class="metric-unit">s</span>
229
+ </div>
230
+
231
+ <div class="latency-group">
232
+ <div class="latency-box">
233
+ <span class="latency-title">STT</span>
234
+ <span id="ux-latency-stt" class="latency-num stt-color">0.00</span>
235
+ </div>
236
+ <div class="latency-box">
237
+ <span class="latency-title">AI LOGIC</span>
238
+ <span id="ux-latency-ai" class="latency-num ai-color">0.00</span>
239
+ </div>
240
  </div>
241
  </div>
242
 
243
  <div class="metric-item">
244
+ <div class="metric-label">CẢM XÚC (SENTIMENT)</div>
245
+ <div class="metric-value" id="ux-sentiment-text" style="font-size: 16px;">---</div>
246
  <div class="sentiment-track">
247
  <div class="sentiment-fill" id="ux-sentiment-bar" style="width: 50%;"></div>
248
  </div>
249
  </div>
 
250
  <div class="metric-item" style="margin-bottom: 0;">
251
+ <div class="metric-label">Ý ĐỊNH (INTENT)</div>
252
+ <div id="ux-intent">---</div>
253
  </div>
254
  </div>
255
  <div style="flex: 1;"></div>
 
256
  <div class="visualizer" id="visualizer">
257
  <div class="bar"></div><div class="bar"></div><div class="bar"></div>
258
  <div class="bar"></div><div class="bar"></div><div class="bar"></div>
259
  <div class="bar"></div><div class="bar"></div><div class="bar"></div>
260
  </div>
261
+ <div style="margin-top: 20px; font-size: 11px; opacity: 0.6; text-align: center;">© 2025 VNPT GROUP</div>
 
 
 
 
 
262
  </div>
263
 
264
  <div class="main-content">
265
  <div class="top-bar">
266
  <div class="call-input">
267
+ <div class="call-timer">
268
+ <i class="far fa-clock"></i>
269
+ <span id="callTimerDisplay">00:00</span>
270
+ </div>
271
+
272
  <div class="input-group">
273
  <label><i class="fas fa-user"></i> ID KHÁCH:</label>
274
  <input type="number" id="customerId" value="1" min="1" max="10">
275
  </div>
276
+ <button id="btnCall" type="button" onclick="startCall()">
277
  <i class="fas fa-phone-alt"></i> BẮT ĐẦU GỌI
278
  </button>
279
  </div>
 
287
  </div>
288
 
289
  <div class="control-panel" id="chatArea">
290
+ <button id="recordBtn" type="button" onmousedown="startRecording()" onmouseup="stopRecording()" onmouseleave="stopRecording()" disabled>
291
  <i class="fas fa-microphone"></i>
292
  </button>
293
  <div class="status-text" id="status">Sẵn sàng kết nối</div>
294
  </div>
295
  </div>
296
 
297
+ <div id="ratingOverlay" class="rating-overlay">
298
+ <div class="rating-box">
299
+ <h3>Đánh giá trải nghiệm</h3>
300
+ <p>Bạn hài lòng với cuộc trò chuyện này chứ?</p>
301
+ <div class="star-group">
302
+ <i class="fas fa-star" data-val="1" onclick="selectStar(1)"></i>
303
+ <i class="fas fa-star" data-val="2" onclick="selectStar(2)"></i>
304
+ <i class="fas fa-star" data-val="3" onclick="selectStar(3)"></i>
305
+ <i class="fas fa-star" data-val="4" onclick="selectStar(4)"></i>
306
+ <i class="fas fa-star" data-val="5" onclick="selectStar(5)"></i>
307
+ </div>
308
+ <textarea id="ratingNote" placeholder="Góp ý thêm (tùy chọn)..."></textarea>
309
+ <div class="rating-actions">
310
+ <button class="btn-skip" type="button" onclick="closeRating()">Đóng</button>
311
+ <button class="btn-submit" type="button" onclick="submitRating()">Gửi đánh giá</button>
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
  <script>
317
+ const API_URL = "http://localhost:8000"; // Thay IP nếu cần
318
+
319
  let mediaRecorder;
320
  let audioChunks = [];
 
 
321
  let audioQueue = [];
322
  let isPlaying = false;
323
+ let isSessionEnded = false;
324
  let currentAudioObject = null;
325
+ let audioContext, analyser, dataArray;
326
+ let currentBotBubble = null;
327
+ let abortController = null;
328
+
329
+ // --- TIMER VARS ---
330
+ let timerInterval = null;
331
+ let t0_record = 0;
332
+ let t1_stt_end = 0;
333
+ let currentPhase = 'idle';
334
+ let isLogicReady = false;
335
+
336
+ // --- CALL DURATION TIMER VARS ---
337
+ let callDurationInterval = null;
338
+ let callSeconds = 0;
339
+
340
+ function startCallTimer() {
341
+ stopCallTimer();
342
+ callSeconds = 0;
343
+ updateCallTimerDisplay();
344
+ callDurationInterval = setInterval(() => {
345
+ callSeconds++;
346
+ updateCallTimerDisplay();
347
+ }, 1000);
348
+ }
349
+
350
+ function stopCallTimer() {
351
+ if (callDurationInterval) {
352
+ clearInterval(callDurationInterval);
353
+ callDurationInterval = null;
354
+ }
355
+ }
356
+
357
+ function resetCallTimer() {
358
+ stopCallTimer();
359
+ callSeconds = 0;
360
+ updateCallTimerDisplay();
361
+ }
362
+
363
+ function updateCallTimerDisplay() {
364
+ const m = Math.floor(callSeconds / 60).toString().padStart(2, '0');
365
+ const s = (callSeconds % 60).toString().padStart(2, '0');
366
+ document.getElementById('callTimerDisplay').innerText = `${m}:${s}`;
367
+ }
368
 
369
+ // --- 1. VISUALIZER & UI ---
370
  function initVisualizer(stream) {
371
  if(!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)();
372
  const source = audioContext.createMediaStreamSource(stream);
 
381
  if (!analyser) return;
382
  requestAnimationFrame(drawVisualizer);
383
  analyser.getByteFrequencyData(dataArray);
384
+ document.querySelectorAll('.bar').forEach((bar, i) => {
 
385
  const h = Math.max(5, dataArray[i] / 255 * 40);
386
  bar.style.height = h + 'px';
387
  });
388
  }
389
 
390
+ function log(msg, type='info') {
391
+ const logArea = document.getElementById('logArea');
392
+ if (type === 'bot' && currentBotBubble) {
393
+ currentBotBubble.innerHTML += msg;
394
+ } else {
395
+ const div = document.createElement('div');
396
+ div.className = `msg ${type}`;
397
+ div.innerHTML = msg;
398
+ logArea.appendChild(div);
399
+ if (type === 'bot') currentBotBubble = div;
400
+ else currentBotBubble = null;
401
+ }
402
+ logArea.scrollTo({ top: logArea.scrollHeight, behavior: 'smooth' });
403
+ }
404
+
405
+ // --- 2. RATING ---
406
+ let currentRating = 0;
407
+ function showRating() { document.getElementById('ratingOverlay').style.display = 'flex'; currentRating = 0; updateStars(0); }
408
+ function closeRating() { document.getElementById('ratingOverlay').style.display = 'none'; resetCallUI(); }
409
+ function selectStar(n) { currentRating = n; updateStars(n); }
410
+ function updateStars(n) { document.querySelectorAll('.star-group i').forEach((star, index) => { if (index < n) star.classList.add('active'); else star.classList.remove('active'); }); }
411
+
412
+ async function submitRating() {
413
+ if (currentRating === 0) { alert("Vui lòng chọn số sao!"); return; }
414
+ const cid = document.getElementById('customerId').value;
415
+ const note = document.getElementById('ratingNote').value;
416
+ document.querySelector('.btn-submit').innerText = "Đang gửi...";
417
+ try {
418
+ await fetch(`${API_URL}/submit-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: cid, stars: currentRating, note: note }) });
419
+ alert("Cảm ơn đánh giá của bạn!");
420
+ } catch (e) { alert("Đã lưu đánh giá (Offline Mode)!"); }
421
+ document.querySelector('.btn-submit').innerText = "Gửi đánh giá";
422
+ closeRating();
423
+ }
424
+
425
+ // --- 3. CALL CONTROL ---
426
+ function resetCallUI() {
427
+ document.getElementById('btnCall').disabled = false;
428
+ document.getElementById('btnCall').innerHTML = '<i class="fas fa-phone-alt"></i> BẮT ĐẦU GỌI';
429
+ document.getElementById('btnCall').style.background = '#27ae60';
430
+ document.getElementById('btnCall').onclick = startCall;
431
+ document.getElementById('recordBtn').disabled = true;
432
+ document.getElementById('status').innerText = "Sẵn sàng kết nối";
433
+ if (mediaRecorder && mediaRecorder.state === "recording") mediaRecorder.stop();
434
+ currentBotBubble = null;
435
+ stopTimer();
436
+ resetCallTimer();
437
+ }
438
+
439
+ function endCall() {
440
+ stopBotResponse();
441
+ stopCallTimer();
442
+ isSessionEnded = true;
443
+ document.getElementById('mascotBox').classList.remove('active');
444
+ showRating();
445
+ }
446
+
447
+ async function startCall() {
448
+ const cid = document.getElementById('customerId').value;
449
+ const btnCall = document.getElementById('btnCall');
450
+ btnCall.disabled = true; btnCall.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ĐANG KẾT NỐI...';
451
+ document.getElementById('logArea').innerHTML = '';
452
+ document.getElementById('status').innerText = "Đang khởi tạo...";
453
+ isSessionEnded = false; audioQueue = []; isPlaying = false; currentBotBubble = null;
454
+
455
+ stopBotResponse();
456
+ startCallTimer();
457
+
458
+ try {
459
+ abortController = new AbortController();
460
+ const formData = new FormData();
461
+ formData.append('customer_id', cid);
462
+
463
+ const res = await fetch(`${API_URL}/start-call`, {
464
+ method: 'POST',
465
+ body: formData,
466
+ signal: abortController.signal
467
+ });
468
+
469
+ btnCall.disabled = false; btnCall.innerHTML = '<i class="fas fa-phone-slash"></i> KẾT THÚC';
470
+ btnCall.style.background = '#e74c3c'; btnCall.onclick = endCall;
471
+ document.getElementById('recordBtn').disabled = false;
472
+ document.getElementById('status').innerText = "Nhấn giữ Mic để nói";
473
+ await readStream(res);
474
+ } catch (e) {
475
+ if (e.name === 'AbortError') return;
476
+ console.error(e); log("Lỗi kết nối Server!", 'info'); resetCallUI();
477
+ }
478
+ }
479
+
480
+ function stopBotResponse() {
481
+ if (currentAudioObject) { currentAudioObject.pause(); currentAudioObject = null; }
482
+ audioQueue = []; isPlaying = false;
483
+ if (abortController) { abortController.abort(); abortController = null; }
484
+ document.getElementById('mascotBox').classList.remove('active');
485
+ stopTimer();
486
+ }
487
+
488
+ // --- 4. STREAM & AUDIO ---
489
+ async function readStream(response) {
490
+ const reader = response.body.getReader();
491
+ const decoder = new TextDecoder();
492
+ let buffer = "";
493
+ isLogicReady = false;
494
+
495
+ try {
496
+ while (true) {
497
+ const { done, value } = await reader.read();
498
+ if (done) break;
499
+
500
+ buffer += decoder.decode(value, { stream: true });
501
+ const lines = buffer.split("\n");
502
+ buffer = lines.pop();
503
+
504
+ for (const line of lines) {
505
+ if (!line.trim()) continue;
506
+ try {
507
+ const data = JSON.parse(line);
508
+
509
+ if (data.user_text) {
510
+ currentBotBubble = null;
511
+ log(data.user_text, 'user');
512
+ switchToAiPhase();
513
+ }
514
+
515
+ if (data.type === "metrics_update") {
516
+ isLogicReady = true;
517
+ updateUXDashboard(data.data);
518
+ }
519
+
520
+ if (data.bot_text) {
521
+ log(data.bot_text, 'bot');
522
+ if (isLogicReady) { stopTimer(); }
523
+ }
524
+
525
+ if (data.audio_base64) {
526
+ audioQueue.push(data.audio_base64);
527
+ processAudioQueue();
528
+ }
529
+
530
+ if (data.end_session) {
531
+ isSessionEnded = true;
532
+ stopTimer();
533
+ if (audioQueue.length === 0 && !isPlaying) showRating();
534
+ }
535
+
536
+ } catch (e) { console.error("JSON Error:", e); }
537
+ }
538
+ }
539
+ } catch (err) {
540
+ if (err.name === 'AbortError') console.log("Stream aborted by user (Barge-in).");
541
+ else console.error("Stream Error:", err);
542
+ }
543
+ }
544
+
545
  async function processAudioQueue() {
546
  if (isPlaying || audioQueue.length === 0) return;
547
  isPlaying = true;
 
548
  document.getElementById('mascotBox').classList.add('active');
549
  document.getElementById('status').innerText = "PINE đang trả lời...";
 
550
  const base64Data = audioQueue.shift();
551
  const audio = new Audio("data:audio/wav;base64," + base64Data);
552
  currentAudioObject = audio;
 
553
  audio.onended = () => {
554
  isPlaying = false;
555
  if (audioQueue.length === 0) {
556
  document.getElementById('mascotBox').classList.remove('active');
557
+ if (isSessionEnded) { document.getElementById('status').innerText = "Kết thúc phiên."; setTimeout(showRating, 500); }
558
+ else { document.getElementById('status').innerText = "Đến lượt bạn nói..."; }
559
  }
560
  processAudioQueue();
561
  };
562
+ audio.onerror = () => { isPlaying = false; processAudioQueue(); };
 
 
 
 
 
 
563
  try { await audio.play(); } catch (e) { isPlaying = false; }
564
  }
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  function updateUXDashboard(metrics) {
567
+ if (metrics.latency_total) document.getElementById("ux-latency-total").innerText = metrics.latency_total;
 
 
568
 
 
 
 
 
 
 
569
  const score = metrics.sentiment;
570
  const bar = document.getElementById("ux-sentiment-bar");
571
  const text = document.getElementById("ux-sentiment-text");
 
572
  const percent = ((score + 1) / 2) * 100;
573
  bar.style.width = `${percent}%`;
574
+ if (score > 0.3) { text.innerText = "Tích cực 😄"; text.style.color = "#2ecc71"; bar.style.background = "#2ecc71"; }
575
+ else if (score < -0.3) { text.innerText = "Tiêu cực 😡"; text.style.color = "#ff6b6b"; bar.style.background = "#ff6b6b"; }
576
+ else { text.innerText = "Trung tính 😐"; text.style.color = "#f1c40f"; bar.style.background = "#f1c40f"; }
577
+
578
+ // UPDATE INTENT (CẬP NHẬT TRỰC TIẾP)
579
+ const intentBox = document.getElementById("ux-intent");
580
+ intentBox.innerText = metrics.intent;
581
+
582
+ // Hiệu ứng "pop"
583
+ intentBox.classList.remove("pop-anim");
584
+ void intentBox.offsetWidth; // Trigger reflow
585
+ intentBox.classList.add("pop-anim");
586
+ }
587
 
588
+ // --- 6. TIMER LOGIC ---
589
+ function startRecording() {
590
+ if (!mediaRecorder) return;
591
+ stopBotResponse();
592
+
593
+ document.getElementById("ux-latency-total").innerText = "0.00";
594
+ document.getElementById("ux-latency-stt").innerText = "0.00";
595
+ document.getElementById("ux-latency-ai").innerText = "0.00";
596
+
597
+ currentBotBubble = null;
598
+ audioChunks = [];
599
+ mediaRecorder.start();
600
+ document.getElementById('recordBtn').classList.add('recording');
601
+ document.getElementById('status').innerText = "Đang nghe bạn nói...";
 
 
602
  }
603
 
604
+ function stopRecording() {
605
+ if (mediaRecorder && mediaRecorder.state === "recording") {
606
+ mediaRecorder.stop();
607
+ document.getElementById('recordBtn').classList.remove('recording');
608
+ document.getElementById('status').innerHTML = '<i class="fas fa-brain fa-pulse"></i> Đang suy nghĩ...';
609
+
610
+ t0_record = performance.now();
611
+ currentPhase = 'stt';
612
+ t1_stt_end = 0;
613
+ isLogicReady = false;
614
+
615
+ if(timerInterval) clearInterval(timerInterval);
616
+ timerInterval = setInterval(updateTimerUI, 30);
617
  }
618
+ }
619
 
620
+ function switchToAiPhase() {
621
+ if (currentPhase === 'stt') {
622
+ t1_stt_end = performance.now();
623
+ currentPhase = 'ai';
624
+ }
625
  }
626
 
627
+ function stopTimer() {
628
+ if (timerInterval) {
629
+ clearInterval(timerInterval);
630
+ timerInterval = null;
631
+ currentPhase = 'idle';
632
+ updateTimerUI();
633
+ }
634
+ }
 
635
 
636
+ function updateTimerUI() {
637
+ const now = performance.now();
638
+ let stt_val = 0;
639
+ let ai_val = 0;
640
 
641
+ if (currentPhase === 'stt') {
642
+ stt_val = (now - t0_record) / 1000;
643
+ ai_val = 0;
644
+ } else if (currentPhase === 'ai') {
645
+ stt_val = (t1_stt_end - t0_record) / 1000;
646
+ ai_val = (now - t1_stt_end) / 1000;
647
+ } else if (currentPhase === 'idle' && t1_stt_end > 0) {
648
+ return;
 
 
 
 
 
 
 
 
 
649
  }
650
+
651
+ document.getElementById("ux-latency-stt").innerText = stt_val.toFixed(2);
652
+ document.getElementById("ux-latency-ai").innerText = ai_val.toFixed(2);
653
+ document.getElementById("ux-latency-total").innerText = (stt_val + ai_val).toFixed(2);
654
  }
655
 
656
+ // --- 7. MICROPHONE SETUP ---
657
  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
658
  initVisualizer(stream);
659
  mediaRecorder = new MediaRecorder(stream);
660
  mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
661
  mediaRecorder.onstop = sendAudio;
662
  }).catch(err => {
663
+ log("Lỗi: Không thể truy cập Micro", 'info');
664
  document.getElementById('status').classList.add('error');
665
  });
666
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  async function sendAudio() {
668
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
669
+ if (audioBlob.size < 1000) { document.getElementById('status').innerText = "Nói quá ngắn, thử lại."; return; }
 
 
 
 
670
  const cid = document.getElementById('customerId').value;
671
  const formData = new FormData();
672
  formData.append('file', audioBlob, 'voice.webm');
673
  formData.append('customer_id', cid);
674
+ document.getElementById('status').innerHTML = '<i class="fas fa-brain fa-pulse"></i> Đang suy nghĩ...';
675
+
676
  try {
677
+ abortController = new AbortController();
678
+ const res = await fetch(`${API_URL}/chat-voice`, { method: 'POST', body: formData, signal: abortController.signal });
679
  await readStream(res);
680
  } catch (e) {
681
+ if (e.name !== 'AbortError') {
682
+ stopTimer();
683
+ log("Lỗi xử lý AI", 'info');
684
+ document.getElementById('status').innerText = "Lỗi kết nối!";
685
+ }
686
  }
687
  }
688
  </script>