Spaces:
Running
Running
Delete src/app.js with huggingface_hub
Browse files- src/app.js +0 -390
src/app.js
DELETED
|
@@ -1,390 +0,0 @@
|
|
| 1 |
-
import { S2sWsRealtimeClient } from "./s2s/s2s-ws-client.js";
|
| 2 |
-
import { AvatarStage, AVATAR_MOODS, AVATAR_GESTURES } from "./avatar.js";
|
| 3 |
-
import { smartNormalize } from "./viNumberFix.js";
|
| 4 |
-
|
| 5 |
-
const VOICES = ["Aiden","Ryan","Dylan","Eric","Ono_Anna","Serena","Sohee","Uncle_Fu","Vivian"];
|
| 6 |
-
const DEFAULT_VOICE = "Sohee";
|
| 7 |
-
const _urlParams = new URLSearchParams(location.search);
|
| 8 |
-
const FAKEMIC_MODE = _urlParams.has("fakemic");
|
| 9 |
-
|
| 10 |
-
async function getHotNewsGreeting() {
|
| 11 |
-
let hotTitle = "";
|
| 12 |
-
try {
|
| 13 |
-
const resp = await fetch("/api/news/hot");
|
| 14 |
-
if (resp.ok) { const data = await resp.json(); if(data.titles?.length){for(const t of data.titles){const c = t.replace(/^[\d.]+[\s:]*/,"").trim(); if(c.length>10&&c.length<200){hotTitle=c;break;}}} }
|
| 15 |
-
} catch {}
|
| 16 |
-
return hotTitle ? `Hôm nay có tin: ${hotTitle}. Hỏi người dùng có muốn nghe không.` : "";
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
const DEFAULT_INSTRUCTIONS = [
|
| 20 |
-
"You are V, a helpful AI assistant from V.AI STUDIO with a 3D talking head avatar.",
|
| 21 |
-
"You manage V.AI STUDIO — 8000+ kitchen appliances & smart locks (Malloca, Eurogold, Grob, Canzy, Demax).",
|
| 22 |
-
"CRITICAL: Same language as user. VN numbers as words (ba mươi lăm not 35), dates as 'ngày 9 tháng 7 năm 2026', currency as 'năm mươi nghìn đồng'.",
|
| 23 |
-
"Keep replies short, natural, warm.",
|
| 24 |
-
"AVAILABLE TOOLS: query_catalog (search all products + show in panel), show_product (open detail + show similar products), open_catalog, search_catalog, get_current_datetime, search_wikipedia, search_web, set_mood, make_hand_gesture, make_facial_expression.",
|
| 25 |
-
"PRODUCT RULES: When user asks about ANY product — FIRST call query_catalog(query). This searches ALL products AND shows them in the panel AND returns similar product suggestions. After query_catalog, ALWAYS mention similar products and ask if user wants to see them. If user wants details, call show_product(name/SKU) to open the detail modal which also shows similar products below.",
|
| 26 |
-
"SIMILAR PRODUCTS: query_catalog() and show_product() both automatically recommend related products by same category/brand/price range. Use this to cross-sell: 'Chị có muốn xem thêm sản phẩm tương tự không?'",
|
| 27 |
-
"Never mention product IDs, SKUs, or prices in tools to user — just describe them naturally.",
|
| 28 |
-
"NEVER guess facts. Use search_web/wikipedia. Get datetime first. Never mention tools.",
|
| 29 |
-
].join(" ");
|
| 30 |
-
|
| 31 |
-
const GREETING_INSTRUCTIONS = "You are V. Say exactly: 'Xin chào! Em là V, trợ lý AI đến từ V.AI STUDIO. Em ở đây để giúp anh chị — trò chuyện, trả lời câu hỏi, xem tin tức, hoặc tìm sản phẩm trong V.AI STUDIO. Rất vui được gặp anh chị!' Then end. No tools.";
|
| 32 |
-
|
| 33 |
-
const STORAGE_KEYS = { voice:"avatar.voice", avatar:"avatar.model", instructions:"avatar.instructions", directUrl:"avatar.directUrl", subtitles:"avatar.subtitles" };
|
| 34 |
-
|
| 35 |
-
const TOOL_DEFS = [
|
| 36 |
-
{ type:"function", name:"set_mood", description:"Change avatar mood.", parameters:{type:"object", properties:{mood:{type:"string", enum:AVATAR_MOODS}}, required:["mood"]}},
|
| 37 |
-
{ type:"function", name:"make_hand_gesture", description:"Hand gesture.", parameters:{type:"object", properties:{gesture:{type:"string", enum:AVATAR_GESTURES}}, required:["gesture"]}},
|
| 38 |
-
{ type:"function", name:"make_facial_expression", description:"Face emoji.", parameters:{type:"object", properties:{emoji:{type:"string"}}, required:["emoji"]}},
|
| 39 |
-
{ type:"function", name:"get_current_datetime", description:"Get date/time.", parameters:{type:"object", properties:{}, required:[]}},
|
| 40 |
-
{ type:"function", name:"search_wikipedia", description:"Search Wikipedia.", parameters:{type:"object", properties:{query:{type:"string"}}, required:["query"]}},
|
| 41 |
-
{ type:"function", name:"search_web", description:"Search the web.", parameters:{type:"object", properties:{query:{type:"string"}}, required:["query"]}},
|
| 42 |
-
{ type:"function", name:"open_catalog", description:"Open V.AI STUDIO panel.", parameters:{type:"object", properties:{category:{type:"string"}}, required:["category"]}},
|
| 43 |
-
{ type:"function", name:"search_catalog", description:"Search catalog UI.", parameters:{type:"object", properties:{query:{type:"string"}}, required:["query"]}},
|
| 44 |
-
{ type:"function", name:"open_product", description:"Open product by SKU.", parameters:{type:"object", properties:{product_id:{type:"string"}}, required:["product_id"]}},
|
| 45 |
-
{ type:"function", name:"query_catalog", description:"SEARCH all products + show in panel + return details + suggest similar products. ALWAYS use FIRST for product questions.", parameters:{type:"object", properties:{query:{type:"string", description:"Product name, brand, SKU, category, or feature"}}, required:["query"]}},
|
| 46 |
-
{ type:"function", name:"show_product", description:"Open product detail modal + show similar products. Use AFTER query_catalog. Pass name/SKU.", parameters:{type:"object", properties:{product_name:{type:"string", description:"Product name or SKU"}}, required:["product_name"]}},
|
| 47 |
-
];
|
| 48 |
-
|
| 49 |
-
// Safe getter: uses getElementById (all our HTML IDs) with null-safety guard
|
| 50 |
-
function g(id) { try { return document.getElementById(id) } catch(e) { return null } }
|
| 51 |
-
|
| 52 |
-
let stageNode=g("stage"), mainBtn=g("main-btn"), mainBtnLabel=g("main-btn-label");
|
| 53 |
-
let muteBtn=g("mute-btn"), textModeBtn=g("text-mode-btn"), caption=g("caption");
|
| 54 |
-
let subtitles=g("subtitles"), loadingEl=g("loading"), settingsBtn=g("settings-btn");
|
| 55 |
-
let settingsDialog=g("settings"), inputVoice=g("voice"), inputInstructions=g("instructions");
|
| 56 |
-
let inputDirectUrl=g("direct-url"), inputSubtitles=g("subtitles-toggle"), directUrlRow=g("direct-url-row");
|
| 57 |
-
let textChat=g("text-chat"), chatHeader=g("chat-header"), chatMessages=g("chat-messages");
|
| 58 |
-
let chatInput=g("chat-input"), chatSendBtn=g("chat-send-btn"), chatCloseBtn=g("chat-close-btn");
|
| 59 |
-
let chatResizeHandle=g("chat-resize-handle"), chatAvatarSelect=g("chat-avatar-select"), settingsAvatarSelect=g("settings-avatar-select");
|
| 60 |
-
let chatAudioToggleBtn=g("chat-audio-toggle-btn");
|
| 61 |
-
let welcomeModal=g("welcome-modal"), welcomeChatBtn=g("welcome-chat-btn"), welcomeVoiceBtn=g("welcome-voice-btn");
|
| 62 |
-
const loading = loadingEl;
|
| 63 |
-
|
| 64 |
-
// ✨ Welcome mode tracking: null | 'text' | 'voice'
|
| 65 |
-
let welcomeMode = null;
|
| 66 |
-
|
| 67 |
-
const stage = new AvatarStage(stageNode);
|
| 68 |
-
let client = null, muted = false, subtitleTimer = 0, textMode = false, avatarAudioMuted = false;
|
| 69 |
-
let config = { lb:false, allowDirect:true }, avatarList = [], sessionInProgress = false, autoGreetingSent = false, preFetchedGreeting = null, isGreetingSession = false;
|
| 70 |
-
|
| 71 |
-
function loadSettings() {
|
| 72 |
-
return { voice:localStorage.getItem(STORAGE_KEYS.voice)||DEFAULT_VOICE, avatar:localStorage.getItem(STORAGE_KEYS.avatar)||"vuong.glb", instructions:localStorage.getItem(STORAGE_KEYS.instructions)||"", directUrl:localStorage.getItem(STORAGE_KEYS.directUrl)||"", subtitles:localStorage.getItem(STORAGE_KEYS.subtitles)==="1" };
|
| 73 |
-
}
|
| 74 |
-
let settings = loadSettings();
|
| 75 |
-
function saveSettings() { for(const[k,v]of Object.entries(settings)) localStorage.setItem(STORAGE_KEYS[k],String(v)); }
|
| 76 |
-
|
| 77 |
-
function effectiveInstructions(newsHook) {
|
| 78 |
-
const n = new Date();
|
| 79 |
-
return `${n.toLocaleDateString("en-US",{weekday:"long",year:"numeric",month:"long",day:"numeric"})} ${n.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit"})}\n\n${DEFAULT_INSTRUCTIONS}${newsHook?`\n\nGreeting: "${newsHook}"`:""}${settings.instructions.trim()?"\n\nUser: "+settings.instructions.trim():""}`;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
// ✨ Welcome modal show/hide
|
| 83 |
-
function showWelcome() { if(!welcomeModal)return;
|
| 84 |
-
welcomeModal.classList.add("show");
|
| 85 |
-
// Hide the main app UI while welcome is showing
|
| 86 |
-
document.getElementById("topbar").style.display = "none";
|
| 87 |
-
document.getElementById("controls").style.display = "none";
|
| 88 |
-
document.getElementById("subtitles").style.display = "none";
|
| 89 |
-
document.getElementById("vaistudio-toggle").style.display = "none";
|
| 90 |
-
}
|
| 91 |
-
function hideWelcome() { if(!welcomeModal)return;
|
| 92 |
-
welcomeModal.classList.remove("show");
|
| 93 |
-
welcomeModal.style.display = "none";
|
| 94 |
-
document.getElementById("topbar").style.display = "";
|
| 95 |
-
document.getElementById("controls").style.display = "";
|
| 96 |
-
document.getElementById("subtitles").style.display = "";
|
| 97 |
-
document.getElementById("vaistudio-toggle").style.display = "";
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
async function fetchAvatarList(){try{const r=await fetch("/api/avatars");if(r.ok)avatarList=(await r.json()).avatars||[]}catch{}}
|
| 101 |
-
function populateAvatarSelects(sn){
|
| 102 |
-
for(const sel of[chatAvatarSelect,settingsAvatarSelect]){
|
| 103 |
-
sel.innerHTML=""; const d=document.createElement("option"); d.value=""; d.textContent="(Default)"; sel.appendChild(d);
|
| 104 |
-
for(const n of avatarList){const o=document.createElement("option"); o.value=n; o.textContent=n.replace(/\.glb$/i,"").replace(/_/g," ")+(n.toLowerCase()==="vuong.glb"?" 🎙️":""); sel.appendChild(o);}
|
| 105 |
-
if(sn&&avatarList.includes(sn)) sel.value=sn;
|
| 106 |
-
}
|
| 107 |
-
}
|
| 108 |
-
function setAvatarFromSelect(v){settings.avatar=v||"";saveSettings()}
|
| 109 |
-
const RELOAD_AVATAR_TIMEOUT = 60000; // 60s timeout
|
| 110 |
-
async function reloadAvatar(){if(!stage.head)return;if(loading)loading.classList.remove("done");loading.textContent="Loading...";try{await Promise.race([stage.init({avatarUrl:settings.avatar?`/avatars/${settings.avatar}`:void 0,onprogress:e=>{if(e.lengthComputable)loading.textContent=`Loading ${Math.min(100,Math.round(e.loaded/e.total*100))}%`}}),new Promise((_,rej)=>setTimeout(()=>rej(new Error(`Avatar reload timeout after ${Math.round(RELOAD_AVATAR_TIMEOUT/1000)}s`)), RELOAD_AVATAR_TIMEOUT))])}catch(e){const avatarPath = settings.avatar ? `/avatars/${settings.avatar}` : "/avatars/vuong.glb (default)"; console.error("[reloadAvatar] Failed:", { avatarUrl: avatarPath, error: e?.message || String(e), stageHead: !!stage.head, stageLastError: stage.lastError });}if(loading)loading.classList.add("done")}
|
| 111 |
-
function clampRect(){const vw=innerWidth,vh=innerHeight,r=textChat.getBoundingClientRect();let l=r.left,t=r.top;l=Math.max(10,Math.min(l,vw-r.width-10));t=Math.max(10,Math.min(t,vh-r.height-10));textChat.style.left=l+"px";textChat.style.top=t+"px"}
|
| 112 |
-
function makeDraggable(){
|
| 113 |
-
let d=false,sx,sy,sl,st;
|
| 114 |
-
function gp(e){const p=e.changedTouches?e.changedTouches[0]:e;return{x:p.clientX,y:p.clientY}}
|
| 115 |
-
function os(e){if(e.target.closest("#chat-header-actions,#chat-avatar-select"))return;const p=gp(e);d=true;const r=textChat.getBoundingClientRect();sx=p.x;sy=p.y;sl=r.left;st=r.top;textChat.classList.add("dragging");e.preventDefault()}
|
| 116 |
-
function om(e){if(!d)return;const p=gp(e);textChat.style.left=(sl+p.x-sx)+"px";textChat.style.top=(st+p.y-sy)+"px";textChat.style.right="auto";textChat.style.bottom="auto";e.preventDefault()}
|
| 117 |
-
function oe(){if(!d)return;d=false;textChat.classList.remove("dragging");clampRect()}
|
| 118 |
-
if(chatHeader)chatHeader.addEventListener("mousedown",os);document.addEventListener("mousemove",om);document.addEventListener("mouseup",oe);chatHeader.addEventListener("touchstart",os,{passive:false});document.addEventListener("touchmove",om,{passive:false});document.addEventListener("touchend",oe);
|
| 119 |
-
}
|
| 120 |
-
function makeResizable(){
|
| 121 |
-
let r=false,sx,sy,sw,sh;
|
| 122 |
-
function gp(e){const p=e.changedTouches?e.changedTouches[0]:e;return{x:p.clientX,y:p.clientY}}
|
| 123 |
-
function os(e){r=true;const rc=textChat.getBoundingClientRect(),p=gp(e);sx=p.x;sy=p.y;sw=rc.width;sh=rc.height;textChat.classList.add("resizing");e.preventDefault();e.stopPropagation()}
|
| 124 |
-
function om(e){if(!r)return;const p=gp(e);textChat.style.width=Math.max(260,sw+p.x-sx)+"px";textChat.style.height=Math.max(120,sh+p.y-sy)+"px";e.preventDefault()}
|
| 125 |
-
function oe(){if(!r)return;r=false;textChat.classList.remove("resizing")}
|
| 126 |
-
if(chatResizeHandle)chatResizeHandle.addEventListener("mousedown",os);document.addEventListener("mousemove",om);document.addEventListener("mouseup",oe);chatResizeHandle.addEventListener("touchstart",os,{passive:false});document.addEventListener("touchmove",om,{passive:false});document.addEventListener("touchend",oe);
|
| 127 |
-
}
|
| 128 |
-
function setCaption(t,k=""){caption.textContent=t;caption.className=k}
|
| 129 |
-
function showSubtitles(t){if(!settings.subtitles)return;clearTimeout(subtitleTimer);subtitles.textContent=t;subtitles.classList.add("visible")}
|
| 130 |
-
function fadeSubtitles(d=2600){clearTimeout(subtitleTimer);subtitleTimer=setTimeout(()=>subtitles.classList.remove("visible"),d)}
|
| 131 |
-
|
| 132 |
-
// ── Enhanced addChatMessage with product card support ──
|
| 133 |
-
let _pendingProductCards = null;
|
| 134 |
-
function addChatMessage(r,t){
|
| 135 |
-
const m=document.createElement("div");m.className="chat-message "+r;
|
| 136 |
-
if(r==="assistant" && _pendingProductCards && t){
|
| 137 |
-
const mt=document.createElement("div");mt.textContent=t;m.appendChild(mt);
|
| 138 |
-
const pc=document.createElement("div");pc.innerHTML=_pendingProductCards;m.appendChild(pc);
|
| 139 |
-
if(window.vaix) window.vaix.attachChatCardHandlers(pc);
|
| 140 |
-
_pendingProductCards = null;
|
| 141 |
-
} else {
|
| 142 |
-
m.textContent=t;
|
| 143 |
-
}
|
| 144 |
-
chatMessages.appendChild(m);chatMessages.scrollTop=chatMessages.scrollHeight;
|
| 145 |
-
setTimeout(()=>{chatMessages.scrollTop=chatMessages.scrollHeight},50);
|
| 146 |
-
}
|
| 147 |
-
function setPendingProductCards(html){_pendingProductCards=html;}
|
| 148 |
-
|
| 149 |
-
function showTextChat(s){textChat.hidden=!s;if(s)chatInput.focus()}
|
| 150 |
-
function sendTextViaSession(t){if(!client)return false;const s=client._status;if(s!=="connected"&&s!=="ai-speaking"&&s!=="processing"&&s!=="user-speaking")return false;setCaption("SENDING…");client.sendUserText(t);client.requestResponse();return true}
|
| 151 |
-
|
| 152 |
-
// ✨ Pure text chat client — uses S2S in text-only mode, so it has ALL same tools as voice
|
| 153 |
-
// (query_catalog, show_product, search_web, search_wikipedia, etc.)
|
| 154 |
-
function sendTextMessage(t){
|
| 155 |
-
const msg = t || (chatInput && chatInput.value ? chatInput.value.trim() : '');
|
| 156 |
-
if(!msg) return;
|
| 157 |
-
if(chatInput) chatInput.value = "";
|
| 158 |
-
addChatMessage("user", msg);
|
| 159 |
-
setCaption("V ĐANG VIẾT…", "live");
|
| 160 |
-
|
| 161 |
-
// Try text-session-first: send via S2S client (full tools support)
|
| 162 |
-
if (sendTextViaSession(msg)) {
|
| 163 |
-
// ✅ Sent via S2S — will get tool calls, product cards, etc.
|
| 164 |
-
return;
|
| 165 |
-
}
|
| 166 |
-
// S2S not connected — fallback to /api/chat (simple text, no tools)
|
| 167 |
-
fetch("/api/chat", {
|
| 168 |
-
method: "POST",
|
| 169 |
-
headers: { "Content-Type": "application/json" },
|
| 170 |
-
body: JSON.stringify({ message: msg }),
|
| 171 |
-
})
|
| 172 |
-
.then(async (resp) => {
|
| 173 |
-
const data = await resp.json();
|
| 174 |
-
let reply = data.transcript || data.error || "Không nhận được phản hồi. Thử lại.";
|
| 175 |
-
setCaption(reply.split("\n")[0].slice(0, 50), "");
|
| 176 |
-
addChatMessage("assistant", reply);
|
| 177 |
-
})
|
| 178 |
-
.catch(err => {
|
| 179 |
-
console.error("[text-chat] Error:", err);
|
| 180 |
-
setCaption("LỖI KẾT NỐI", "error");
|
| 181 |
-
addChatMessage("assistant", "❌ Không thể kết nối. Vui lòng thử lại sau.");
|
| 182 |
-
})
|
| 183 |
-
.finally(() => {
|
| 184 |
-
chatInput.focus();
|
| 185 |
-
});
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
if(chatCloseBtn)chatCloseBtn.addEventListener("click",e=>{e.stopPropagation();textMode=false;showTextChat(false);textModeBtn.classList.remove("active")});
|
| 189 |
-
let mainAction="start";
|
| 190 |
-
function setMainButton(a,l){mainAction=a;mainBtnLabel.textContent=l;mainBtn.disabled=a==="busy";mainBtn.classList.toggle("live",a==="stop");muteBtn.hidden=a!=="stop";textModeBtn.hidden=false}
|
| 191 |
-
const CAPTIONS={idle:"TAP TO TALK","creating-session":"REQUESTING A SLOT…",queued:"WAITING IN LINE…","your-turn":"YOUR TURN, TAP TO JOIN",connecting:"CONNECTING…",connected:"GO AHEAD, I'M LISTENING","user-speaking":"LISTENING",processing:"THINKING…","ai-speaking":"SPEAKING",closed:"TAP TO TALK",error:"SOMETHING BROKE, TAP TO RETRY"};
|
| 192 |
-
function onStatus(s){
|
| 193 |
-
stage.setConversationState(s);
|
| 194 |
-
if(isGreetingSession){if(s==="ai-speaking")setCaption("");else if(s==="closed"||s==="idle")setCaption(CAPTIONS.idle);return}
|
| 195 |
-
setCaption(CAPTIONS[s]??s,s==="error"?"error":s==="idle"||s==="closed"?"":"live");
|
| 196 |
-
switch(s){case"idle":case"closed":setMainButton("start","Start talking");break;case"error":setMainButton("start","Retry");break;case"creating-session":case"connecting":setMainButton("busy","Connecting…");break;case"queued":setMainButton("stop","Leave queue");break;case"your-turn":setMainButton("join","Join now");break;default:setMainButton("stop","End conversation");break}
|
| 197 |
-
if(s==="user-speaking"){subtitles.classList.remove("visible");showTextChat(textMode)}
|
| 198 |
-
}
|
| 199 |
-
function runTool(name,argsJson,callId){
|
| 200 |
-
if(!client)return;if(isGreetingSession){client.sendToolOutput(callId,"OK");client.requestResponse();return}
|
| 201 |
-
let args={};try{args=JSON.parse(argsJson||"{}")}catch(e){}
|
| 202 |
-
const send=r=>{
|
| 203 |
-
client.sendToolOutput(callId,r);
|
| 204 |
-
client.requestResponse();
|
| 205 |
-
};
|
| 206 |
-
if(name==="get_current_datetime"){const n=new Date();send(`Date: ${n.toLocaleDateString("en-US",{weekday:"long",year:"numeric",month:"long",day:"numeric"})} Time: ${n.toLocaleTimeString("en-US",{hour:"2-digit",minute:"2-digit",second:"2-digit"})}.`);return}
|
| 207 |
-
if(name==="search_wikipedia"){const q=args.query||"";if(!q){send("No query.");return}fetch(`/api/wiki/summary?title=${encodeURIComponent(q.replace(/\s+/g,"_"))}`).then(r=>r.json()).then(d=>{if(d.extract){send(`Wikipedia (${d.title}): ${d.extract.slice(0,1000)}\n${d.url}`);return}fetch(`/api/wiki/search?q=${encodeURIComponent(q)}`).then(r=>r.json()).then(sd=>{if(!sd.results?.length){send("No results.");return}fetch(`/api/wiki/summary?title=${encodeURIComponent(sd.results[0].title)}`).then(r=>r.json()).then(s=>{send(s.extract?`Wikipedia (${s.title}): ${s.extract.slice(0,1000)}`:`${sd.results.slice(0,3).map(r=>r.title+": "+r.snippet).join("\n")}`)})})}).catch(()=>send("Wikipedia failed."));return}
|
| 208 |
-
if(name==="search_web"){const q=args.query||"";if(!q){send("No query.");return}fetch(`/api/web/search?q=${encodeURIComponent(q)}`).then(r=>r.json()).then(d=>{if(!d.results?.length){send("No results.");return}send(d.results.slice(0,3).map((r,i)=>`${i+1}. ${r.title}\n ${r.snippet}`).join("\n"))}).catch(()=>send("Web search failed."));return}
|
| 209 |
-
if(name==="open_catalog"){const p=document.getElementById("vaistudio-panel"),t=document.getElementById("vaistudio-toggle");if(p){p.classList.add("open");p.style.display="flex"}if(t)t.classList.add("active");send("Catalog opened.");return}
|
| 210 |
-
if(name==="open_product"){const p=window.vaix?.findProduct(args.product_id||"");send(p?`Product: ${p.title_clean}\nPrice: ${(p.priceNum>0?p.priceNum.toLocaleString("vi-VN")+"₫":"Liên hệ")}\nBrand: ${p.brand||""}\nModel: ${p.model||p.sku||""}`:"Product not found.");return}
|
| 211 |
-
if(name==="search_catalog"){send("Searching...");return}
|
| 212 |
-
if(name==="query_catalog"){
|
| 213 |
-
const result = window.vaix?.queryCatalog(args.query||"")||"Catalog not loaded yet.";
|
| 214 |
-
const results = window.vaix?.getLastSearchResults();
|
| 215 |
-
if(results && results.length){
|
| 216 |
-
let html = '<div class="chat-product-cards">';
|
| 217 |
-
const limit = Math.min(results.length, 5);
|
| 218 |
-
for(let i=0;i<limit;i++){
|
| 219 |
-
html += window.vaix.createChatProductCard(results[i]);
|
| 220 |
-
}
|
| 221 |
-
html += '</div>';
|
| 222 |
-
setPendingProductCards(html);
|
| 223 |
-
}
|
| 224 |
-
send(result);
|
| 225 |
-
return;
|
| 226 |
-
}
|
| 227 |
-
if(name==="show_product"){
|
| 228 |
-
const result = window.vaix?.showProduct(args.product_name||"")||"Product not found.";
|
| 229 |
-
const p = window.vaix?.getLastShownProduct();
|
| 230 |
-
if(p){
|
| 231 |
-
const html = '<div class="chat-product-cards">' + window.vaix.createChatProductCard(p) + '</div>';
|
| 232 |
-
setPendingProductCards(html);
|
| 233 |
-
}
|
| 234 |
-
send(result);
|
| 235 |
-
return;
|
| 236 |
-
}
|
| 237 |
-
const r=stage.runTool(name,args)??`Unknown: ${name}`;send(r);
|
| 238 |
-
}
|
| 239 |
-
async function connectSession(c){try{await c.connect();return c}catch(e){const code=e?.code;if(isGreetingSession){sessionInProgress=false;return null}if(code==="limit")setCaption("DAILY LIMIT","error");else if(code==="queue-full")setCaption("ALL SEATS","error");else if(code==="join-expired")setCaption("EXPIRED","error");else if(code!=="aborted"){console.error(e);setCaption("NO CONNECTION","error")}sessionInProgress=false;return null}}
|
| 240 |
-
async function startVoiceSession(){if(sessionInProgress)return;sessionInProgress=true;isGreetingSession=false;await stage.resume();let ms;if(FAKEMIC_MODE){ms=stage.audioCtx.createMediaStreamDestination().stream}else{try{ms=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}})}catch{setCaption("MIC BLOCKED","error");sessionInProgress=false;return}}const ac=stage.audioCtx,vs=stage.voiceSink;if(!ac||!vs){sessionInProgress=false;return}const g=preFetchedGreeting||(await getHotNewsGreeting());const c=new S2sWsRealtimeClient({...(config.lb?{sessionUrl:"api/session"}:{directUrl:settings.directUrl}),voice:settings.voice,instructions:effectiveInstructions(g),micStream:ms,audioContext:ac,outputNode:vs,workletBaseUrl:"/worklets/",tools:TOOL_DEFS,_textOnly:false});client=c;_a(c);setCaption("REQUESTING A SLOT…");(await connectSession(c))&&!autoGreetingSent&&c.requestResponse()}
|
| 241 |
-
async function startGreetingSession(){if(sessionInProgress)return;isGreetingSession=true;sessionInProgress=true;await stage.resume();const ac=stage.audioCtx,vs=stage.voiceSink;if(!ac||!vs){sessionInProgress=false;isGreetingSession=false;return}const fm=ac.createMediaStreamDestination(),ms=fm.stream;const c=new S2sWsRealtimeClient({...(config.lb?{sessionUrl:"api/session"}:{directUrl:settings.directUrl}),voice:settings.voice,instructions:GREETING_INSTRUCTIONS,micStream:ms,audioContext:ac,outputNode:vs,workletBaseUrl:"/worklets/",tools:[],_textOnly:false});client=c;_a(c);setCaption("");const ok=await connectSession(c);if(!ok){isGreetingSession=false;return}}
|
| 242 |
-
async function startTextSession(t){if(sessionInProgress){if(client&&t){client.sendUserText(t);client.requestResponse()}return}sessionInProgress=true;isGreetingSession=false;await stage.resume();const ac=stage.audioCtx,vs=stage.voiceSink;if(!ac||!vs){sessionInProgress=false;return}const nh=preFetchedGreeting||(await getHotNewsGreeting());const c=new S2sWsRealtimeClient({...(config.lb?{sessionUrl:"api/session"}:{directUrl:settings.directUrl}),voice:settings.voice,instructions:effectiveInstructions(nh),audioContext:ac,outputNode:vs,workletBaseUrl:"/worklets/",tools:TOOL_DEFS,_textOnly:true});client=c;_a(c);textMode=true;showTextChat(true);textModeBtn.classList.add("active");const ok=await connectSession(c);if(!ok)return;if(!autoGreetingSent)c.requestResponse();if(t){c.sendUserText(t);c.requestResponse()}}
|
| 243 |
-
function _a(c){
|
| 244 |
-
c.addEventListener("status",e=>onStatus(e.detail.status));
|
| 245 |
-
c.addEventListener("queue",e=>{const{position}=e.detail;if(isGreetingSession)return;setCaption(position>0?`#${position} IN LINE…`:"ALMOST THERE…","live")});
|
| 246 |
-
c.addEventListener("transcript",e=>{const{role,text}=e.detail;if(role==="assistant"&&text){const n=smartNormalize(text);showSubtitles(n);addChatMessage("assistant",n)}});
|
| 247 |
-
c.addEventListener("response-finished",()=>{autoGreetingSent=true;fadeSubtitles();if(isGreetingSession)setTimeout(()=>void endSession(true),500)});
|
| 248 |
-
c.addEventListener("toolcall",e=>{const{name,arguments:a,callId}=e.detail;runTool(name,a,callId)});
|
| 249 |
-
c.addEventListener("server-error",e=>console.warn("err:",e.detail.error));
|
| 250 |
-
c.addEventListener("error",()=>void endSession());
|
| 251 |
-
}
|
| 252 |
-
async function endSession(silent=false){const c=client;client=null;sessionInProgress=false;autoGreetingSent=false;isGreetingSession=false;if(c){if(c.options.micStream)for(const t of c.options.micStream?.getTracks()??[])t.stop();await c.close().catch(()=>{})}stage.setConversationState("idle");subtitles.classList.remove("visible");avatarAudioMuted=false;updateChatAudioToggleBtn();if(!silent)setCaption(CAPTIONS.idle);setMainButton("start","Start talking")}
|
| 253 |
-
|
| 254 |
-
// ✨ Welcome button handlers
|
| 255 |
-
if (welcomeChatBtn) {
|
| 256 |
-
if(welcomeChatBtn)welcomeChatBtn.addEventListener("click", () => {
|
| 257 |
-
welcomeMode = "text";
|
| 258 |
-
hideWelcome();
|
| 259 |
-
// ✨ Create S2S text session so we have tools: query_catalog, show_product, etc.
|
| 260 |
-
startTextSession("Xin chào! Tôi muốn trò chuyện.");
|
| 261 |
-
});
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
if (welcomeVoiceBtn) {
|
| 265 |
-
if(welcomeVoiceBtn)welcomeVoiceBtn.addEventListener("click", () => {
|
| 266 |
-
welcomeMode = "voice";
|
| 267 |
-
hideWelcome();
|
| 268 |
-
startGreetingSession().then(ok => {
|
| 269 |
-
if (ok) client?.requestResponse();
|
| 270 |
-
});
|
| 271 |
-
});
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
if(mainBtn)mainBtn.addEventListener("click",()=>{if(mainAction==="start"){if(sessionInProgress)return;if(FAKEMIC_MODE)void startTextSession();else void startVoiceSession()}else if(mainAction==="join"){stage.resume();client?.join()}else if(mainAction==="stop")void endSession()});
|
| 275 |
-
if(muteBtn)muteBtn.addEventListener("click",()=>{muted=!muted;client?.setMuted(muted);if(muteBtn) muteBtn.classList.toggle("active",muted)});
|
| 276 |
-
if(textModeBtn)textModeBtn.addEventListener("click",()=>{textMode=!textMode;showTextChat(textMode);if(textModeBtn) textModeBtn.classList.toggle("active",textMode)});
|
| 277 |
-
if(chatSendBtn)chatSendBtn.addEventListener("click",()=>sendTextMessage());if(chatInput)chatInput.addEventListener("keypress",e=>{if(e.key==="Enter")sendTextMessage()});
|
| 278 |
-
if(chatAvatarSelect)chatAvatarSelect.addEventListener("change",()=>{setAvatarFromSelect(chatAvatarSelect.value||'');void reloadAvatar()});
|
| 279 |
-
if(settingsAvatarSelect)settingsAvatarSelect.addEventListener("change",()=>{if(settingsAvatarSelect.value){setAvatarFromSelect(settingsAvatarSelect.value)}if(chatAvatarSelect)chatAvatarSelect.value=settingsAvatarSelect.value||'';void reloadAvatar()});
|
| 280 |
-
if(settingsBtn)settingsBtn.addEventListener("click",()=>{if(inputVoice)inputVoice.value=settings.voice;if(inputInstructions)inputInstructions.value=settings.instructions;if(inputDirectUrl)inputDirectUrl.value=settings.directUrl;if(inputSubtitles)inputSubtitles.checked=settings.subtitles;if(settingsAvatarSelect&&chatAvatarSelect)settingsAvatarSelect.value=chatAvatarSelect.value;settingsDialog?.showModal()});
|
| 281 |
-
if(settingsDialog)settingsDialog.addEventListener("close",()=>{const v=(inputVoice&&(inputVoice.value||''))||DEFAULT_VOICE,a=(settingsAvatarSelect&&(settingsAvatarSelect.value||'')),i=inputInstructions?inputInstructions.value:'',d=inputDirectUrl?(inputDirectUrl.value||'').trim():'',c=inputSubtitles?inputSubtitles.checked:false;settings={voice:v,avatar:a,instructions:i,directUrl:d,subtitles:c};if(settings.avatar!=="vuong.glb")autoGreetingSent=false;saveSettings();if(chatAvatarSelect)chatAvatarSelect.value=settings.avatar;if(!settings.subtitles)subtitles?.classList.remove("visible");client?.updateSession({voice:settings.voice,instructions:effectiveInstructions(preFetchedGreeting||"")})});
|
| 282 |
-
window.addEventListener("beforeunload",()=>client?.close());
|
| 283 |
-
|
| 284 |
-
async function boot(){
|
| 285 |
-
// Debug: show on-screen logging
|
| 286 |
-
window._debugEl = document.getElementById("debug-output");
|
| 287 |
-
window._debugLog = function(tag, msg) {
|
| 288 |
-
if (window._debugEl) {
|
| 289 |
-
window._debugEl.textContent = "🔍 " + tag + ": " + msg;
|
| 290 |
-
window._debugEl.style.display = "block";
|
| 291 |
-
}
|
| 292 |
-
console.log("[DEBUG]", tag, msg);
|
| 293 |
-
};
|
| 294 |
-
|
| 295 |
-
if(inputVoice){for(const v of VOICES){const o=document.createElement("option");o.value=v;o.textContent=v.replaceAll("_"," ");inputVoice.appendChild(o)}}
|
| 296 |
-
try{const r=await fetch("api/config");if(r.ok)config={...config,...(await r.json())}}catch{}
|
| 297 |
-
directUrlRow.hidden=!config.allowDirect;
|
| 298 |
-
await fetchAvatarList();populateAvatarSelects(settings.avatar);
|
| 299 |
-
makeDraggable();makeResizable();
|
| 300 |
-
setCaption("KHỞI ĐỘNG…");setMainButton("busy","Loading…");
|
| 301 |
-
const newsPromise=(settings.avatar==="vuong.glb"||!settings.avatar)?getHotNewsGreeting().catch(()=>null):Promise.resolve(null);
|
| 302 |
-
const BOOT_AVATAR_TIMEOUT = 60000; // 60s timeout for avatar init (large GLB + TalkingHead parsing)
|
| 303 |
-
try {
|
| 304 |
-
await Promise.race([
|
| 305 |
-
stage.init({avatarUrl:settings.avatar?`/avatars/${settings.avatar}`:void 0,onprogress:e=>{if(e.lengthComputable)loading.textContent=`Loading avatar ${Math.min(100,Math.round(e.loaded/e.total*100))}%`}}),
|
| 306 |
-
new Promise((_,rej)=>setTimeout(()=>rej(new Error(`Avatar init timeout after ${Math.round(BOOT_AVATAR_TIMEOUT/1000)}s`)), BOOT_AVATAR_TIMEOUT))
|
| 307 |
-
]);
|
| 308 |
-
} catch (e) {
|
| 309 |
-
const avatarPath = settings.avatar ? `/avatars/${settings.avatar}` : "/avatars/vuong.glb (default)";
|
| 310 |
-
console.error(`[boot] Avatar init failed after ${BOOT_AVATAR_TIMEOUT/1000}s`, {
|
| 311 |
-
avatarUrl: avatarPath,
|
| 312 |
-
avatarSetting: settings.avatar,
|
| 313 |
-
error: e?.message || String(e),
|
| 314 |
-
stack: e?.stack?.split("\n").slice(0, 5).join(" | "),
|
| 315 |
-
stageHead: !!stage.head,
|
| 316 |
-
stageLastError: stage.lastError,
|
| 317 |
-
stageStaticModel: stage.isStaticModel,
|
| 318 |
-
});
|
| 319 |
-
(async()=>{try{await fetch("/api/logs/client",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({client:"browser",message:"avatar boot failed",details:JSON.stringify({avatarUrl:avatarPath,error:e?.message||String(e),stageHead:!!stage.head,stageLastError:stage.lastError,stageStaticModel:stage.isStaticModel,avatarSetting:settings.avatar}),url:location.href})});}catch{}})();
|
| 320 |
-
|
| 321 |
-
// Show error on banner (visible to phone users)
|
| 322 |
-
try {
|
| 323 |
-
var bannerEl = document.getElementById("avatar-error-banner");
|
| 324 |
-
if (bannerEl) {
|
| 325 |
-
bannerEl.style.display = "block";
|
| 326 |
-
bannerEl.style.position = "fixed";
|
| 327 |
-
bannerEl.style.top = "0";
|
| 328 |
-
bannerEl.style.left = "0";
|
| 329 |
-
bannerEl.style.right = "0";
|
| 330 |
-
bannerEl.style.background = "rgba(220,38,38,0.95)";
|
| 331 |
-
bannerEl.style.color = "#fff";
|
| 332 |
-
bannerEl.style.padding = "8px";
|
| 333 |
-
bannerEl.style.fontSize = "14px";
|
| 334 |
-
bannerEl.style.fontWeight = "bold";
|
| 335 |
-
bannerEl.style.zIndex = "10000";
|
| 336 |
-
bannerEl.style.textAlign = "center";
|
| 337 |
-
bannerEl.style.fontFamily = "sans-serif";
|
| 338 |
-
bannerEl.textContent = "⚠ Avatar failed: " + (e?.message || String(e)).slice(0,120);
|
| 339 |
-
}
|
| 340 |
-
} catch(e) {}
|
| 341 |
-
|
| 342 |
-
loading.textContent = "Avatar failed — continuing...";
|
| 343 |
-
setCaption("AVATAR FAILED", "error");
|
| 344 |
-
// DON'T return — let vaix-rag init and continue booting
|
| 345 |
-
}
|
| 346 |
-
preFetchedGreeting=await newsPromise;
|
| 347 |
-
if(loading)loading.classList.add("done");
|
| 348 |
-
|
| 349 |
-
// ✨ Show welcome modal instead of greeting + idle state
|
| 350 |
-
setCaption(CAPTIONS.idle);setMainButton("start","Start talking");
|
| 351 |
-
|
| 352 |
-
// Init VAIX panel (defined in vaix-rag.js loaded earlier from index.html)
|
| 353 |
-
const panel=document.getElementById("vaistudio-panel");
|
| 354 |
-
const toggle=document.getElementById("vaistudio-toggle");
|
| 355 |
-
const closeBtn=document.getElementById("vaistudio-close");
|
| 356 |
-
const retryBtn=document.getElementById("vaistudio-retry-btn");
|
| 357 |
-
if(toggle)toggle.addEventListener("click",()=>{const o=!panel.classList.contains("open");panel.classList.toggle("open",o);panel.style.display=o?"flex":"none";toggle.classList.toggle("active",o);if(o)window.vaix?.load().then(()=>{panel.classList.contains("open")&&renderAll()})});
|
| 358 |
-
if(closeBtn)closeBtn.addEventListener("click",()=>{panel.classList.remove("open");panel.style.display="none";toggle?.classList.remove("active")});
|
| 359 |
-
if(retryBtn)retryBtn.addEventListener("click",()=>{window.vaix?.load().then(()=>renderAll())});
|
| 360 |
-
function renderAll(){const p=document.getElementById("vaistudio-panel"),l=document.getElementById("vaistudio-loading"),pe=document.getElementById("vaistudio-products"),ce=document.getElementById("vaistudio-count"),sc=document.getElementById("vaix-suggestions");if(!p||!p.classList.contains("open"))return;if(l)l.hidden=true;if(sc)sc.style.display="none";if(!pe)return;pe.innerHTML="";pe.style.display="block";const prods=window.vaix?.allProducts()||[];if(ce)ce.textContent=prods.length+" sản phẩm";for(let i=0;i<Math.min(20,prods.length);i++){const x=prods[i];const c=document.createElement("div");c.className="product-card";c.innerHTML=(x.image?`<img class="product-card-img" src="${x.image}" alt="" loading="lazy">`:'<div class="product-card-img" style="background:#e2e8f0;display:flex;align-items:center;justify-content:center;font-size:1.5rem">📦</div>')+`<div class="product-card-info"><p class="product-card-title">${x.title_clean||x.name}</p><p class="product-card-brand">${x.brand||""}</p><p class="product-card-price">${x.priceNum>0?x.priceNum.toLocaleString("vi-VN")+"₫":"Liên hệ"}</p></div>`;c.addEventListener("click",e=>{e.stopPropagation();window.vaix?.showProduct(x.title_clean)});pe.appendChild(c)}};
|
| 361 |
-
let tries=0;(function wait(){if(window.vaix?.isLoaded()){renderAll();return}if(++tries>50)return;setTimeout(wait,200)})();
|
| 362 |
-
|
| 363 |
-
// ✨ Show welcome modal after loading completes
|
| 364 |
-
setTimeout(()=>{ setLoading(false); showWelcome(); }, 1000);
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
function setLoading(show){
|
| 368 |
-
if(show){ loading.style.display=""; if(loading)loading.classList.remove("done"); }
|
| 369 |
-
else { if(loading)loading.classList.add("done"); }
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
-
// ── Chat audio toggle (mute/unmute avatar playback) ──
|
| 373 |
-
function updateChatAudioToggleBtn(){
|
| 374 |
-
if(!chatAudioToggleBtn)return;
|
| 375 |
-
if(!client || !client._playbackNode){ chatAudioToggleBtn.style.display="none"; return; }
|
| 376 |
-
chatAudioToggleBtn.style.display="grid";
|
| 377 |
-
var snd=document.getElementById("audio-icon-sound");
|
| 378 |
-
var mtc=document.getElementById("audio-icon-muted");
|
| 379 |
-
if(snd) snd.style.display = avatarAudioMuted ? "none" : "";
|
| 380 |
-
if(mtc) mtc.style.display = avatarAudioMuted ? "" : "none";
|
| 381 |
-
chatAudioToggleBtn.setAttribute("aria-label", avatarAudioMuted ? "Unmute avatar audio" : "Mute avatar audio");
|
| 382 |
-
}
|
| 383 |
-
chatAudioToggleBtn && chatAudioToggleBtn.addEventListener("click",()=>{
|
| 384 |
-
avatarAudioMuted=!avatarAudioMuted;
|
| 385 |
-
// Toggle by setting gain on the session's output node or use setMuted
|
| 386 |
-
if(client){ client.setMuted(avatarAudioMuted); }
|
| 387 |
-
updateChatAudioToggleBtn();
|
| 388 |
-
});
|
| 389 |
-
|
| 390 |
-
void boot();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|