PINE-AI-Amdocs / frontend /call_desktop.html
MinhDo123's picture
Update frontend/call_desktop.html
af240a6 verified
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PINE AI - VNPT Smart Contact Center</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
/* --- CORE VARIABLES --- */
:root {
--vnpt-blue: #00a1e4;
--vnpt-dark: #0072bc;
--bg-body: #f4f7fa;
--chat-bot: #ffffff;
--chat-user: #e3f2fd;
}
body { font-family: 'Inter', sans-serif; margin: 0; display: flex; height: 100vh; background: var(--bg-body); color: #333; overflow: hidden; }
/* --- SIDEBAR --- */
.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; }
.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; }
.mascot-container.active { border-color: #fff; box-shadow: 0 0 30px rgba(255,255,255,0.6); transform: scale(1.05); }
.mascot-container img { width: 70%; }
.brand-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: 1px; margin-top: 5px; }
.brand-sub { font-size: 12px; opacity: 0.8; margin-top: 2px; font-weight: 300; margin-bottom: 20px; }
.visualizer { display: flex; gap: 4px; height: 40px; margin-top: 20px; align-items: center; flex-shrink: 0; }
.bar { width: 5px; height: 5px; background: rgba(255,255,255,0.6); border-radius: 5px; transition: 0.1s; }
/* --- UX DASHBOARD --- */
.ux-dashboard {
width: 100%;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
padding: 20px;
box-sizing: border-box;
margin-top: 15px;
margin-bottom: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.5s ease;
}
.ux-header {
font-size: 13px; font-weight: 800; color: #fff; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.2);
display: flex; justify-content: space-between; align-items: center; letter-spacing: 0.5px; text-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.metric-item { margin-bottom: 18px; }
.metric-label { font-size: 11px; color: rgba(255, 255, 255, 0.9); margin-bottom: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.metric-value { font-size: 26px; font-weight: 800; color: #fff; display: flex; justify-content: space-between; align-items: baseline; text-shadow: 0 2px 5px rgba(0,0,0,0.15); }
/* CAPSULE INTENT STYLE */
#ux-intent {
background: rgba(0, 161, 228, 0.15); border: 1px solid rgba(0, 229, 255, 0.4); color: #ffffff;
font-size: 17px; font-weight: 700; text-align: center; padding: 10px 15px; border-radius: 30px; margin-top: 8px;
text-transform: capitalize; box-shadow: 0 0 15px rgba(0, 161, 228, 0.15); transition: all 0.3s ease;
display: flex; align-items: center; justify-content: center; min-height: 24px;
}
@keyframes pop { 50% { transform: scale(1.05); } }
.pop-anim { animation: pop 0.2s ease; }
.metric-unit { font-size: 12px; font-weight: 500; color: rgba(255, 255, 255, 0.7); margin-left: 5px; }
.sentiment-track { width: 100%; height: 10px; background: rgba(0, 0, 0, 0.25); border-radius: 10px; overflow: hidden; margin-top: 8px; box-shadow: inset 0 1px 3px rgba(0,0,0,0.2); }
.sentiment-fill { height: 100%; width: 50%; background: #f1c40f; border-radius: 10px; transition: width 0.5s cubic-bezier(0.25, 0.8, 0.25, 1), background 0.5s ease; box-shadow: 0 0 10px rgba(241, 196, 15, 0.4); }
.latency-group { display: flex; gap: 10px; margin-top: 10px; }
.latency-box { flex: 1; background: rgba(0,0,0,0.2); padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05); display: flex; flex-direction: column; }
.latency-title { font-size: 10px; color: rgba(255,255,255,0.7); text-transform: uppercase; font-weight: 600; }
.latency-num { font-size: 16px; font-weight: 700; margin-top: 4px; display: block; }
.stt-color { color: #4fc3f7; text-shadow: 0 0 8px rgba(79, 195, 247, 0.3); }
.ai-color { color: #f1c40f; text-shadow: 0 0 8px rgba(241, 196, 15, 0.3); }
/* --- MAIN CONTENT & CHAT --- */
.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); }
.top-bar { padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; background: white; }
.call-input { display: flex; gap: 15px; align-items: center; }
/* [NEW] STYLE CHO BỘ ĐẾM GIỜ CUỘC GỌI */
.call-timer {
font-family: 'Courier New', monospace; font-size: 18px; font-weight: 700; color: var(--vnpt-dark);
background: #e3f2fd; padding: 8px 15px; border-radius: 8px; display: flex; align-items: center; gap: 8px;
margin-right: 10px; border: 1px solid rgba(0, 114, 188, 0.1);
}
.input-group { display: flex; align-items: center; gap: 10px; background: #f0f2f5; padding: 5px 15px; border-radius: 12px; }
.input-group label { font-size: 13px; font-weight: 600; color: #555; }
.input-group input { border: none; background: transparent; width: 50px; font-weight: bold; color: var(--vnpt-dark); outline: none; font-size: 16px; text-align: center; }
#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; }
#btnCall:hover { transform: translateY(-2px); } #btnCall:disabled { background: #ccc; cursor: not-allowed; transform: none; }
.chat-area { flex: 1; padding: 40px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; scroll-behavior: smooth; }
.msg { max-width: 70%; padding: 12px 18px; border-radius: 18px; font-size: 15px; line-height: 1.5; position: relative; animation: fadeIn 0.3s ease; }
.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); }
.msg.user { background: var(--chat-user); color: #0d47a1; align-self: flex-end; border-bottom-right-radius: 2px; }
.msg.info { align-self: center; font-size: 12px; color: #999; background: #f5f5f5; padding: 5px 15px; border-radius: 20px; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.control-panel { padding: 30px; display: flex; flex-direction: column; align-items: center; border-top: 1px solid #f0f0f0; }
#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; }
#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; }
@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); } }
.status-text { margin-top: 15px; font-weight: 600; color: #888; font-size: 14px; } .status-text.error { color: #ff4757; }
/* RATING */
.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); }
.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); }
@keyframes popup { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.rating-box h3 { margin: 0 0 10px; color: var(--vnpt-dark); } .rating-box p { font-size: 13px; color: #666; margin-bottom: 20px; }
.star-group { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
.star-group i { font-size: 32px; color: #ddd; cursor: pointer; transition: 0.2s; }
.star-group i:hover, .star-group i.active { color: #f1c40f; transform: scale(1.2); }
#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; }
.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; }
.btn-skip { background: #f0f2f5; color: #666; } .btn-submit { background: var(--vnpt-blue); color: white; } .btn-submit:hover { background: var(--vnpt-dark); }
</style>
</head>
<body>
<div class="sidebar">
<div class="mascot-container" id="mascotBox">
<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">
</div>
<div class="brand-title">PINE AI</div>
<div class="brand-sub">VNPT Smart Contact Center</div>
<div class="ux-dashboard">
<div class="ux-header">
<span><i class="fas fa-chart-line"></i> UX METRICS</span>
<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>
</div>
<div class="metric-item">
<div class="metric-label">TỔNG ĐỘ TRỄ (LATENCY)</div>
<div class="metric-value">
<span id="ux-latency-total">0.00</span>
<span class="metric-unit">s</span>
</div>
<div class="latency-group">
<div class="latency-box">
<span class="latency-title">STT</span>
<span id="ux-latency-stt" class="latency-num stt-color">0.00</span>
</div>
<div class="latency-box">
<span class="latency-title">AI LOGIC</span>
<span id="ux-latency-ai" class="latency-num ai-color">0.00</span>
</div>
</div>
</div>
<div class="metric-item">
<div class="metric-label">CẢM XÚC (SENTIMENT)</div>
<div class="metric-value" id="ux-sentiment-text" style="font-size: 16px;">---</div>
<div class="sentiment-track">
<div class="sentiment-fill" id="ux-sentiment-bar" style="width: 50%;"></div>
</div>
</div>
<div class="metric-item" style="margin-bottom: 0;">
<div class="metric-label">Ý ĐỊNH (INTENT)</div>
<div id="ux-intent">---</div>
</div>
</div>
<div style="flex: 1;"></div>
<div class="visualizer" id="visualizer">
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
</div>
<div style="margin-top: 20px; font-size: 11px; opacity: 0.6; text-align: center;">© 2025 VNPT GROUP</div>
</div>
<div class="main-content">
<div class="top-bar">
<div class="call-input">
<div class="call-timer">
<i class="far fa-clock"></i>
<span id="callTimerDisplay">00:00</span>
</div>
<div class="input-group">
<label><i class="fas fa-user"></i> ID KHÁCH:</label>
<input type="number" id="customerId" value="1" min="1" max="10">
</div>
<button id="btnCall" type="button" onclick="startCall()">
<i class="fas fa-phone-alt"></i> BẮT ĐẦU GỌI
</button>
</div>
<div style="font-weight: 600; color: var(--vnpt-dark);">
<img src="https://github.com/chydua/PINE/blob/main/fda.png?raw=true" height="30">
</div>
</div>
<div class="chat-area" id="logArea">
<div class="msg info">Vui lòng chọn ID khách hàng và nhấn "Bắt đầu gọi"</div>
</div>
<div class="control-panel" id="chatArea">
<button id="recordBtn" type="button" onmousedown="startRecording()" onmouseup="stopRecording()" onmouseleave="stopRecording()" disabled>
<i class="fas fa-microphone"></i>
</button>
<div class="status-text" id="status">Sẵn sàng kết nối</div>
</div>
</div>
<div id="ratingOverlay" class="rating-overlay">
<div class="rating-box">
<h3>Đánh giá trải nghiệm</h3>
<p>Bạn hài lòng với cuộc trò chuyện này chứ?</p>
<div class="star-group">
<i class="fas fa-star" data-val="1" onclick="selectStar(1)"></i>
<i class="fas fa-star" data-val="2" onclick="selectStar(2)"></i>
<i class="fas fa-star" data-val="3" onclick="selectStar(3)"></i>
<i class="fas fa-star" data-val="4" onclick="selectStar(4)"></i>
<i class="fas fa-star" data-val="5" onclick="selectStar(5)"></i>
</div>
<textarea id="ratingNote" placeholder="Góp ý thêm (tùy chọn)..."></textarea>
<div class="rating-actions">
<button class="btn-skip" type="button" onclick="closeRating()">Đóng</button>
<button class="btn-submit" type="button" onclick="submitRating()">Gửi đánh giá</button>
</div>
</div>
</div>
<script>
// --- CẤU HÌNH API THÔNG MINH (AUTO-DETECT) ---
const getBaseUrl = () => {
const host = window.location.hostname;
const protocol = window.location.protocol;
// 1. Chạy Localhost (Port 8000 hoặc 7860) hoặc mở file HTML trực tiếp
if (protocol === 'file:' || host === 'localhost' || host === '127.0.0.1') {
return "http://localhost:7860"; // Sửa port 7860 nếu cần
}
// 2. Chạy trên Hugging Face
return "";
};
const API_URL = getBaseUrl();
let mediaRecorder;
let audioChunks = [];
let audioQueue = [];
let isPlaying = false;
let isSessionEnded = false;
let currentAudioObject = null;
let audioContext, analyser, dataArray;
let currentBotBubble = null;
let abortController = null;
// --- TIMER VARS (Latency Timer) ---
let timerInterval = null;
let t0_record = 0;
let t1_stt_end = 0;
let currentPhase = 'idle';
let isLogicReady = false;
// --- [NEW] CALL DURATION TIMER VARS ---
let callDurationInterval = null;
let callSeconds = 0;
function startCallTimer() {
stopCallTimer();
callSeconds = 0;
updateCallTimerDisplay();
callDurationInterval = setInterval(() => {
callSeconds++;
updateCallTimerDisplay();
}, 1000);
}
function stopCallTimer() {
if (callDurationInterval) {
clearInterval(callDurationInterval);
callDurationInterval = null;
}
}
function resetCallTimer() {
stopCallTimer();
callSeconds = 0;
updateCallTimerDisplay();
}
function updateCallTimerDisplay() {
const m = Math.floor(callSeconds / 60).toString().padStart(2, '0');
const s = (callSeconds % 60).toString().padStart(2, '0');
document.getElementById('callTimerDisplay').innerText = `${m}:${s}`;
}
// --- 1. VISUALIZER & UI ---
function initVisualizer(stream) {
if(!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 64;
source.connect(analyser);
dataArray = new Uint8Array(analyser.frequencyBinCount);
drawVisualizer();
}
function drawVisualizer() {
if (!analyser) return;
requestAnimationFrame(drawVisualizer);
analyser.getByteFrequencyData(dataArray);
document.querySelectorAll('.bar').forEach((bar, i) => {
const h = Math.max(5, dataArray[i] / 255 * 40);
bar.style.height = h + 'px';
});
}
function log(msg, type='info') {
const logArea = document.getElementById('logArea');
if (type === 'bot' && currentBotBubble) {
currentBotBubble.innerHTML += msg;
} else {
const div = document.createElement('div');
div.className = `msg ${type}`;
div.innerHTML = msg;
logArea.appendChild(div);
if (type === 'bot') currentBotBubble = div;
else currentBotBubble = null;
}
logArea.scrollTo({ top: logArea.scrollHeight, behavior: 'smooth' });
}
// --- 2. RATING ---
let currentRating = 0;
function showRating() { document.getElementById('ratingOverlay').style.display = 'flex'; currentRating = 0; updateStars(0); }
function closeRating() { document.getElementById('ratingOverlay').style.display = 'none'; resetCallUI(); }
function selectStar(n) { currentRating = n; updateStars(n); }
function updateStars(n) { document.querySelectorAll('.star-group i').forEach((star, index) => { if (index < n) star.classList.add('active'); else star.classList.remove('active'); }); }
async function submitRating() {
if (currentRating === 0) { alert("Vui lòng chọn số sao!"); return; }
const cid = document.getElementById('customerId').value;
const note = document.getElementById('ratingNote').value;
document.querySelector('.btn-submit').innerText = "Đang gửi...";
try {
await fetch(`${API_URL}/submit-feedback`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: cid, stars: currentRating, note: note }) });
alert("Cảm ơn đánh giá của bạn!");
} catch (e) { alert("Đã lưu đánh giá (Offline Mode)!"); }
document.querySelector('.btn-submit').innerText = "Gửi đánh giá";
closeRating();
}
// --- 3. CALL CONTROL ---
function resetCallUI() {
document.getElementById('btnCall').disabled = false;
document.getElementById('btnCall').innerHTML = '<i class="fas fa-phone-alt"></i> BẮT ĐẦU GỌI';
document.getElementById('btnCall').style.background = '#27ae60';
document.getElementById('btnCall').onclick = startCall;
document.getElementById('recordBtn').disabled = true;
document.getElementById('status').innerText = "Sẵn sàng kết nối";
if (mediaRecorder && mediaRecorder.state === "recording") mediaRecorder.stop();
currentBotBubble = null;
stopTimer(); // Dừng latency timer
resetCallTimer(); // [NEW] Reset call timer
}
function endCall() {
stopBotResponse();
stopCallTimer(); // [NEW] Dừng call timer
isSessionEnded = true;
document.getElementById('mascotBox').classList.remove('active');
showRating();
}
async function startCall() {
const cid = document.getElementById('customerId').value;
const btnCall = document.getElementById('btnCall');
btnCall.disabled = true; btnCall.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ĐANG KẾT NỐI...';
document.getElementById('logArea').innerHTML = '';
document.getElementById('status').innerText = "Đang khởi tạo...";
isSessionEnded = false; audioQueue = []; isPlaying = false; currentBotBubble = null;
stopBotResponse();
startCallTimer(); // [NEW] Bắt đầu đếm giờ cuộc gọi
try {
abortController = new AbortController();
const formData = new FormData();
formData.append('customer_id', cid);
const res = await fetch(`${API_URL}/start-call`, {
method: 'POST',
body: formData,
signal: abortController.signal
});
btnCall.disabled = false; btnCall.innerHTML = '<i class="fas fa-phone-slash"></i> KẾT THÚC';
btnCall.style.background = '#e74c3c'; btnCall.onclick = endCall;
document.getElementById('recordBtn').disabled = false;
document.getElementById('status').innerText = "Nhấn giữ Mic để nói";
await readStream(res);
} catch (e) {
if (e.name === 'AbortError') return;
console.error(e); log("Lỗi kết nối Server!", 'info'); resetCallUI();
}
}
function stopBotResponse() {
if (currentAudioObject) { currentAudioObject.pause(); currentAudioObject = null; }
audioQueue = []; isPlaying = false;
if (abortController) { abortController.abort(); abortController = null; }
document.getElementById('mascotBox').classList.remove('active');
stopTimer();
}
// --- 4. STREAM & AUDIO ---
async function readStream(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
isLogicReady = false;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (!line.trim()) continue;
try {
const data = JSON.parse(line);
if (data.user_text) {
currentBotBubble = null;
log(data.user_text, 'user');
switchToAiPhase();
}
if (data.type === "metrics_update") {
isLogicReady = true;
updateUXDashboard(data.data);
}
if (data.bot_text) {
log(data.bot_text, 'bot');
if (isLogicReady) { stopTimer(); }
}
if (data.audio_base64) {
audioQueue.push(data.audio_base64);
processAudioQueue();
}
if (data.end_session) {
isSessionEnded = true;
stopTimer();
if (audioQueue.length === 0 && !isPlaying) showRating();
}
} catch (e) { console.error("JSON Error:", e); }
}
}
} catch (err) {
if (err.name === 'AbortError') console.log("Stream aborted by user (Barge-in).");
else console.error("Stream Error:", err);
}
}
async function processAudioQueue() {
if (isPlaying || audioQueue.length === 0) return;
isPlaying = true;
document.getElementById('mascotBox').classList.add('active');
document.getElementById('status').innerText = "PINE đang trả lời...";
const base64Data = audioQueue.shift();
const audio = new Audio("data:audio/wav;base64," + base64Data);
currentAudioObject = audio;
audio.onended = () => {
isPlaying = false;
if (audioQueue.length === 0) {
document.getElementById('mascotBox').classList.remove('active');
if (isSessionEnded) { document.getElementById('status').innerText = "Kết thúc phiên."; setTimeout(showRating, 500); }
else { document.getElementById('status').innerText = "Đến lượt bạn nói..."; }
}
processAudioQueue();
};
audio.onerror = () => { isPlaying = false; processAudioQueue(); };
try { await audio.play(); } catch (e) { isPlaying = false; }
}
function updateUXDashboard(metrics) {
if (metrics.latency_total) document.getElementById("ux-latency-total").innerText = metrics.latency_total;
const score = metrics.sentiment;
const bar = document.getElementById("ux-sentiment-bar");
const text = document.getElementById("ux-sentiment-text");
const percent = ((score + 1) / 2) * 100;
bar.style.width = `${percent}%`;
if (score > 0.3) { text.innerText = "Tích cực 😄"; text.style.color = "#2ecc71"; bar.style.background = "#2ecc71"; }
else if (score < -0.3) { text.innerText = "Tiêu cực 😡"; text.style.color = "#ff6b6b"; bar.style.background = "#ff6b6b"; }
else { text.innerText = "Trung tính 😐"; text.style.color = "#f1c40f"; bar.style.background = "#f1c40f"; }
// UPDATE INTENT (CẬP NHẬT TRỰC TIẾP)
const intentBox = document.getElementById("ux-intent");
intentBox.innerText = metrics.intent;
// Hiệu ứng "pop"
intentBox.classList.remove("pop-anim");
void intentBox.offsetWidth; // Trigger reflow
intentBox.classList.add("pop-anim");
}
// --- 6. TIMER LOGIC (REAL-TIME UI) ---
function startRecording() {
if (!mediaRecorder) return;
stopBotResponse(); // BARGE-IN
// Reset UI
document.getElementById("ux-latency-total").innerText = "0.00";
document.getElementById("ux-latency-stt").innerText = "0.00";
document.getElementById("ux-latency-ai").innerText = "0.00";
currentBotBubble = null;
audioChunks = [];
mediaRecorder.start();
document.getElementById('recordBtn').classList.add('recording');
document.getElementById('status').innerText = "Đang nghe bạn nói...";
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === "recording") {
mediaRecorder.stop();
document.getElementById('recordBtn').classList.remove('recording');
document.getElementById('status').innerHTML = '<i class="fas fa-brain fa-pulse"></i> Đang suy nghĩ...';
// BẮT ĐẦU ĐẾM STT
t0_record = performance.now();
currentPhase = 'stt';
t1_stt_end = 0;
isLogicReady = false;
if(timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(updateTimerUI, 30);
}
}
function switchToAiPhase() {
if (currentPhase === 'stt') {
t1_stt_end = performance.now(); // Chốt STT time
currentPhase = 'ai'; // Chuyển sang AI running
}
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
currentPhase = 'idle';
updateTimerUI(); // Render lần cuối
}
}
function updateTimerUI() {
const now = performance.now();
let stt_val = 0;
let ai_val = 0;
if (currentPhase === 'stt') {
stt_val = (now - t0_record) / 1000;
ai_val = 0;
} else if (currentPhase === 'ai') {
stt_val = (t1_stt_end - t0_record) / 1000;
ai_val = (now - t1_stt_end) / 1000;
} else if (currentPhase === 'idle' && t1_stt_end > 0) {
// Đã dừng, giữ số liệu cũ (không làm gì)
return;
}
document.getElementById("ux-latency-stt").innerText = stt_val.toFixed(2);
document.getElementById("ux-latency-ai").innerText = ai_val.toFixed(2);
document.getElementById("ux-latency-total").innerText = (stt_val + ai_val).toFixed(2);
}
// --- 7. MICROPHONE SETUP ---
navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
initVisualizer(stream);
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = event => audioChunks.push(event.data);
mediaRecorder.onstop = sendAudio;
}).catch(err => {
log("Lỗi: Không thể truy cập Micro", 'info');
document.getElementById('status').classList.add('error');
});
async function sendAudio() {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
if (audioBlob.size < 1000) { document.getElementById('status').innerText = "Nói quá ngắn, thử lại."; return; }
const cid = document.getElementById('customerId').value;
const formData = new FormData();
formData.append('file', audioBlob, 'voice.webm');
formData.append('customer_id', cid);
document.getElementById('status').innerHTML = '<i class="fas fa-brain fa-pulse"></i> Đang suy nghĩ...';
try {
abortController = new AbortController();
const res = await fetch(`${API_URL}/chat-voice`, { method: 'POST', body: formData, signal: abortController.signal });
await readStream(res);
} catch (e) {
if (e.name !== 'AbortError') {
stopTimer();
log("Lỗi xử lý AI", 'info');
document.getElementById('status').innerText = "Lỗi kết nối!";
}
}
}
</script>
</body>
</html>