maitrang04 commited on
Commit
63661d0
·
verified ·
1 Parent(s): 0649d3e

Upload test_fr.html

Browse files
Files changed (1) hide show
  1. frontend/test_fr.html +524 -0
frontend/test_fr.html ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
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>
204
+
205
+ <div class="sidebar">
206
+ <div class="mascot-container" id="mascotBox">
207
+ <img src="https://github.com/chydua/PINE/blob/main/Screenshot_2025-12-18_at_19.19.29-removebg-preview.png?raw=true" alt="PINE Mascot">
208
+ </div>
209
+ <div class="brand-title">PINE AI</div>
210
+ <div class="brand-sub">VNPT Smart Contact Center</div>
211
+
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>
266
+ <div style="font-weight: 600; color: var(--vnpt-dark);">
267
+ <img src="https://github.com/chydua/PINE/blob/main/fda.png?raw=true" height="30">
268
+ </div>
269
+ </div>
270
+
271
+ <div class="chat-area" id="logArea">
272
+ <div class="msg info">Vui lòng chọn ID khách hàng và nhấn "Bắt đầu gọi"</div>
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 = "http://localhost:8000";
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);
298
+ analyser = audioContext.createAnalyser();
299
+ analyser.fftSize = 64;
300
+ source.connect(analyser);
301
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
302
+ drawVisualizer();
303
+ }
304
+
305
+ function drawVisualizer() {
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 có 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>
523
+ </body>
524
+ </html>