* Works great as a WordPress "Custom HTML" / Elementor HTML widget too — * just paste the entire contents inside tags. * * Want it powered by a real LLM later (OpenAI/Anthropic/etc.)? * See the ANSWER ENGINE section near the bottom — swap `localAnswer()` * for a fetch() to your own backend (never call AI APIs with a secret * key directly from the browser). */ (function () { "use strict"; /* ============================================================ 1. STYLES — injected into a Shadow DOM so the host site's CSS (WordPress/Elementor theme) can never clash with ours. ============================================================ */ const CSS = ` :host, .mop-root { all: initial; } * { box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .mop-root { position: fixed; bottom: 22px; right: 22px; z-index: 999999; display: block; } @font-face { font-family: 'Oswald-fallback'; src: local('Arial Narrow'), local('Arial'); } /* ---------- Launcher button ---------- */ .mop-launcher { width: 62px; height: 62px; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #1a1a1a, #0a0a0a 70%); border: 2px solid #C1121F; box-shadow: 0 6px 20px rgba(193,18,31,0.45), 0 0 0 0 rgba(193,18,31,0.6); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform .2s ease, box-shadow .3s ease; animation: mop-pulse 2.6s infinite; } .mop-launcher:hover { transform: scale(1.06); } .mop-launcher svg { width: 28px; height: 28px; } .mop-launcher .mop-dot { position: absolute; top: -2px; right: -2px; width: 14px; height: 14px; background: #E5383B; border: 2px solid #0a0a0a; border-radius: 50%; display: none; } .mop-launcher.mop-has-badge .mop-dot { display: block; } @keyframes mop-pulse { 0% { box-shadow: 0 6px 20px rgba(193,18,31,0.45), 0 0 0 0 rgba(193,18,31,0.5); } 70% { box-shadow: 0 6px 20px rgba(193,18,31,0.45), 0 0 0 12px rgba(193,18,31,0); } 100% { box-shadow: 0 6px 20px rgba(193,18,31,0.45), 0 0 0 0 rgba(193,18,31,0); } } /* ---------- Chat panel ---------- */ .mop-panel { position: absolute; bottom: 78px; right: 0; width: 360px; max-width: calc(100vw - 40px); height: 520px; max-height: 75vh; background: #101010; border: 1px solid #2a2a2a; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.6); display: none; flex-direction: column; overflow: hidden; transform-origin: bottom right; animation: mop-open .22s cubic-bezier(.2,.8,.3,1); } .mop-panel.mop-visible { display: flex; } @keyframes mop-open { from { opacity: 0; transform: scale(.9) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } } .mop-header { background: linear-gradient(135deg, #0a0a0a 0%, #1c0304 60%, #C1121F 220%); padding: 16px 16px 14px; position: relative; border-bottom: 3px solid #C1121F; } .mop-header-top { display: flex; align-items: center; justify-content: space-between; } .mop-brand { display: flex; align-items: center; gap: 10px; } .mop-logo { width: 34px; height: 34px; border-radius: 8px; background: #C1121F; display: flex; align-items: center; justify-content: center; font-weight: 800; color: #0a0a0a; font-size: 15px; letter-spacing: -0.5px; flex-shrink: 0; } .mop-brand-text h3 { margin: 0; color: #fff; font-size: 15px; font-weight: 800; letter-spacing: 0.4px; text-transform: uppercase; } .mop-brand-text p { margin: 1px 0 0; color: #ff9a9a; font-size: 11px; font-weight: 500; display: flex; align-items: center; gap: 5px; } .mop-brand-text p::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: #3ddc73; display: inline-block; } .mop-close { background: rgba(255,255,255,0.08); border: none; color: #fff; width: 28px; height: 28px; border-radius: 50%; cursor: pointer; font-size: 15px; display: flex; align-items: center; justify-content: center; transition: background .15s; } .mop-close:hover { background: rgba(255,255,255,0.18); } .mop-stitch { height: 6px; background-image: repeating-linear-gradient(90deg, #C1121F 0 10px, transparent 10px 20px); opacity: .8; } .mop-messages { flex: 1; overflow-y: auto; padding: 16px 14px; display: flex; flex-direction: column; gap: 10px; background: #101010; background-image: radial-gradient(circle at 100% 0%, rgba(193,18,31,0.06), transparent 40%); } .mop-messages::-webkit-scrollbar { width: 6px; } .mop-messages::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; } .mop-msg { max-width: 84%; display: flex; flex-direction: column; } .mop-msg.bot { align-self: flex-start; } .mop-msg.user { align-self: flex-end; align-items: flex-end; } .mop-bubble { padding: 10px 13px; border-radius: 14px; font-size: 13.5px; line-height: 1.5; white-space: pre-wrap; } .mop-msg.bot .mop-bubble { background: #1c1c1c; color: #f0f0f0; border: 1px solid #2a2a2a; border-bottom-left-radius: 4px; } .mop-msg.user .mop-bubble { background: linear-gradient(135deg, #C1121F, #8f0f18); color: #fff; border-bottom-right-radius: 4px; } .mop-msg a { color: #ff6b6b; } .mop-typing { display: flex; gap: 4px; padding: 12px 13px; } .mop-typing span { width: 6px; height: 6px; border-radius: 50%; background: #C1121F; animation: mop-bounce 1.2s infinite ease-in-out; } .mop-typing span:nth-child(2) { animation-delay: .15s; } .mop-typing span:nth-child(3) { animation-delay: .3s; } @keyframes mop-bounce { 0%,80%,100% { transform: translateY(0); opacity:.5 } 40% { transform: translateY(-4px); opacity:1 } } .mop-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 14px 12px; background: #101010; } .mop-chip { background: #1c1c1c; border: 1px solid #C1121F; color: #ff9a9a; font-size: 12px; font-weight: 600; padding: 6px 11px; border-radius: 999px; cursor: pointer; transition: background .15s, color .15s; white-space: nowrap; } .mop-chip:hover { background: #C1121F; color: #fff; } .mop-inputbar { display: flex; align-items: center; gap: 8px; padding: 12px; background: #0a0a0a; border-top: 1px solid #2a2a2a; } .mop-input { flex: 1; background: #1c1c1c; border: 1px solid #2a2a2a; border-radius: 10px; padding: 10px 12px; color: #fff; font-size: 13.5px; outline: none; } .mop-input:focus { border-color: #C1121F; } .mop-input::placeholder { color: #6b6b6b; } .mop-send { width: 38px; height: 38px; border-radius: 10px; background: #C1121F; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: background .15s, transform .1s; } .mop-send:hover { background: #E5383B; } .mop-send:active { transform: scale(.94); } .mop-send svg { width: 17px; height: 17px; } .mop-footer { text-align: center; font-size: 10px; color: #4a4a4a; padding: 6px 0 10px; background: #0a0a0a; letter-spacing: .3px; } @media (max-width: 420px) { .mop-panel { width: calc(100vw - 24px); right: -10px; height: 70vh; } } `; /* ============================================================ 2. KNOWLEDGE BASE — edit/expand these freely. Each topic has keywords (Vietnamese, no-accent variants included) and a reply. Add new topics any time — no build step needed. ============================================================ */ const KB = [ { id: "greeting", keywords: ["chao", "hi", "hello", "xin chao", "alo"], reply: "Chào bạn 👋 Mình là trợ lý ảo của MOP SHOP. Mình có thể giúp bạn chọn giày, tư vấn bảo quản, thương hiệu, hoặc hướng dẫn đặt hàng. Bạn cần hỗ trợ gì nào?", }, { id: "what-is", keywords: ["giay the thao la gi", "dinh nghia", "khai niem"], reply: "Giày thể thao (sports/athletic shoes) là loại giày được thiết kế chuyên biệt để hỗ trợ vận động — đế êm, ôm chân, thoáng khí — khác với sneaker vốn thiên về thời trang/đường phố hơn là hiệu năng vận động.", }, { id: "vs-sneaker", keywords: ["phan biet", "sneaker", "khac nhau"], reply: "Nhiều người dùng lẫn hai từ này, nhưng thực ra:\n• Giày thể thao (athletic shoes): tối ưu cho vận động — chạy, tập gym, bóng rổ.\n• Sneaker: thiên về phong cách, thời trang đường phố, không nhất thiết để tập luyện.\nMOP SHOP có cả hai dòng, bạn cho mình biết nhu cầu để tư vấn kỹ hơn nhé!", }, { id: "types", keywords: ["loai giay", "phan loai", "running", "training", "chay bo", "bong ro", "lifestyle"], reply: "Các loại phổ biến bên mình:\n• Giày chạy bộ (Running) — đệm êm, nhẹ\n• Giày tập luyện (Training) — bám sàn, hỗ trợ đa hướng\n• Giày bóng rổ — cổ cao, chống lật cổ chân\n• Giày lifestyle/sneaker — phối đồ hàng ngày\nBạn dùng giày cho mục đích gì để mình gợi ý đúng dòng nhé?", }, { id: "brands", keywords: ["thuong hieu", "hang nao", "nike", "adidas", "puma", "converse", "vans", "new balance"], reply: "MOP SHOP tuyển chọn các thương hiệu biểu tượng: Nike, Adidas, Puma, New Balance, Converse, Vans... Bạn thích phong cách hãng nào, mình lọc sản phẩm phù hợp cho bạn xem thử nhé?", }, { id: "how-to-choose", keywords: ["chon giay", "size", "nen mua", "tu van chon", "mua giay nao"], reply: "Để chọn đúng giày, hãy tự hỏi: \"Mình dùng giày để làm gì nhiều nhất?\"\n• Chạy bộ → ưu tiên đệm êm, nhẹ\n• Tập gym → đế bám, ổn định\n• Mặc hàng ngày → ưu tiên form dáng, phối đồ\nVề size, nên đo chân vào cuối ngày (chân hơi giãn nở) và tham khảo bảng size từng hãng vì mỗi hãng ra form khác nhau.", }, { id: "care", keywords: ["bao quan", "ve sinh", "cham soc giay", "giu giay ben"], reply: "Mẹo bảo quản giày bền đẹp:\n1. Hạn chế giặt máy — dễ hỏng form, bong keo\n2. Vệ sinh bằng bàn chải mềm + dung dịch chuyên dụng\n3. Để giày ở nơi khô thoáng, tránh nắng trực tiếp\n4. Dùng giấy/form giữ dáng khi không mang\n5. Thay dây giày định kỳ để giữ form tổng thể", }, { id: "wash-machine", keywords: ["giat may", "giat may duoc khong", "co giat duoc khong"], reply: "Giày thể thao thì nên hạn chế tối đa việc giặt máy — lực quay và nước nóng có thể làm hỏng form, bong keo và phai màu. Cách an toàn hơn: giặt tay nhẹ nhàng bằng bàn chải mềm và để khô tự nhiên ở nơi thoáng gió, tránh phơi nắng trực tiếp.", }, { id: "shipping", keywords: ["giao hang", "van chuyen", "ship", "bao lau nhan hang", "phi ship"], reply: "Về giao hàng và phí vận chuyển, bạn vui lòng xem chi tiết mới nhất tại trang Thanh Toán hoặc liên hệ trực tiếp để được báo chính xác cho khu vực của bạn: https://ps48577.id.vn/lien-he/", }, { id: "return", keywords: ["doi tra", "hoan tien", "tra hang", "bao hanh"], reply: "Về chính sách đổi trả/bảo hành, đội ngũ MOP SHOP sẽ hỗ trợ bạn chi tiết nhất qua trang Liên Hệ: https://ps48577.id.vn/lien-he/ — bạn cứ để lại thông tin đơn hàng để được xử lý nhanh nhé.", }, { id: "cart", keywords: ["gio hang", "thanh toan", "dat hang", "mua hang", "checkout"], reply: "Bạn có thể xem giỏ hàng tại đây 🛒 https://ps48577.id.vn/mop-shop-gio-hang/ và tiến hành thanh toán tại https://ps48577.id.vn/thanh-toan/. Cần mình gợi ý sản phẩm trước không?", }, { id: "shop", keywords: ["cua hang", "san pham", "xem giay", "danh muc"], reply: "Toàn bộ sản phẩm đang có tại Cửa Hàng: https://ps48577.id.vn/cua-hang/ — bạn đang tìm loại giày nào để mình lọc giúp bạn nhanh hơn?", }, { id: "contact", keywords: ["lien he", "hotline", "so dien thoai", "gap nhan vien", "noi chuyen voi nguoi"], reply: "Bạn có thể liên hệ đội ngũ MOP SHOP trực tiếp tại đây: https://ps48577.id.vn/lien-he/ — mình sẽ chuyển tiếp mọi câu hỏi cần người thật hỗ trợ nhé.", }, ]; const FALLBACK = "Mình chưa chắc về câu hỏi này 🤔 Bạn thử hỏi về: chọn giày, bảo quản, thương hiệu, giao hàng, đổi trả — hoặc liên hệ đội ngũ MOP SHOP tại https://ps48577.id.vn/lien-he/ để được hỗ trợ trực tiếp."; const QUICK_CHIPS = [ "Cách chọn giày", "Cách bảo quản", "Thương hiệu nào có?", "Liên hệ hỗ trợ", ]; /* ============================================================ 3. ANSWER ENGINE ------------------------------------------------------------ `localAnswer()` = free, instant, runs in-browser (default, used below). To upgrade to a real generative AI model later: 1. Stand up a small backend endpoint (e.g. /api/chat) that holds your API key server-side. 2. Replace the body of `getAnswer()` with a fetch() to that endpoint, passing the user's message, and return the response text. Never put a real API key in this file — it is publicly visible to anyone who views page source. ============================================================ */ function normalize(str) { return str .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") // strip Vietnamese diacritics for matching .replace(/đ/g, "d") .trim(); } function localAnswer(userText) { const text = normalize(userText); let best = null; let bestScore = 0; for (const topic of KB) { let score = 0; for (const kw of topic.keywords) { if (text.includes(normalize(kw))) score += kw.split(" ").length; } if (score > bestScore) { bestScore = score; best = topic; } } return best ? best.reply : FALLBACK; } async function getAnswer(userText) { // --- Swap point for a real AI backend, see comment block above --- return localAnswer(userText); } /* ============================================================ 4. UI ASSEMBLY ============================================================ */ function iconChat() { return ` `; } function iconClose() { return "✕"; } function iconSend() { return ` `; } function buildWidget() { const host = document.createElement("div"); host.id = "mop-chat-host"; document.body.appendChild(host); const shadow = host.attachShadow({ mode: "open" }); const style = document.createElement("style"); style.textContent = CSS; shadow.appendChild(style); const root = document.createElement("div"); root.className = "mop-root"; root.innerHTML = `

MOP SHOP

Trợ lý đang trực tuyến

`; shadow.appendChild(root); const panel = shadow.getElementById("mop-panel"); const launcher = shadow.getElementById("mop-launcher"); const closeBtn = shadow.getElementById("mop-close"); const messagesEl = shadow.getElementById("mop-messages"); const chipsEl = shadow.getElementById("mop-chips"); const input = shadow.getElementById("mop-input"); const sendBtn = shadow.getElementById("mop-send"); let opened = false; function addMessage(text, who) { const wrap = document.createElement("div"); wrap.className = `mop-msg ${who}`; const bubble = document.createElement("div"); bubble.className = "mop-bubble"; // basic auto-link for URLs in bot replies const linked = text.replace( /(https?:\/\/[^\s]+)/g, (m) => `${m}` ); bubble.innerHTML = linked; wrap.appendChild(bubble); messagesEl.appendChild(wrap); messagesEl.scrollTop = messagesEl.scrollHeight; } function showTyping() { const wrap = document.createElement("div"); wrap.className = "mop-msg bot"; wrap.id = "mop-typing-indicator"; wrap.innerHTML = `
`; messagesEl.appendChild(wrap); messagesEl.scrollTop = messagesEl.scrollHeight; } function hideTyping() { const el = shadow.getElementById("mop-typing-indicator"); if (el) el.remove(); } async function handleUserMessage(text) { if (!text.trim()) return; addMessage(text, "user"); input.value = ""; showTyping(); const delay = 450 + Math.random() * 500; const answer = await getAnswer(text); setTimeout(() => { hideTyping(); addMessage(answer, "bot"); }, delay); } function renderChips() { chipsEl.innerHTML = ""; QUICK_CHIPS.forEach((label) => { const chip = document.createElement("button"); chip.className = "mop-chip"; chip.textContent = label; chip.addEventListener("click", () => handleUserMessage(label)); chipsEl.appendChild(chip); }); } function openPanel() { panel.classList.add("mop-visible"); launcher.classList.remove("mop-has-badge"); opened = true; if (!messagesEl.childElementCount) { showTyping(); setTimeout(() => { hideTyping(); addMessage( "Chào mừng bạn đến với MOP SHOP 🔥 Mình là trợ lý ảo, sẵn sàng tư vấn giày, bảo quản, thương hiệu và đơn hàng. Bạn cần gì nào?", "bot" ); }, 500); } setTimeout(() => input.focus(), 250); } function closePanel() { panel.classList.remove("mop-visible"); opened = false; } launcher.addEventListener("click", () => (opened ? closePanel() : openPanel())); closeBtn.addEventListener("click", closePanel); sendBtn.addEventListener("click", () => handleUserMessage(input.value)); input.addEventListener("keydown", (e) => { if (e.key === "Enter") handleUserMessage(input.value); }); renderChips(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", buildWidget); } else { buildWidget(); } })();

Great things are on the horizon

Something big is brewing! Our store is in the works and will be launching soon!

Nike P-6000 - Metallic Silver/Black (Nam)Nike P-6000 – Metallic Silver/Black (Nam)
1.290.000 

Availability: 50 in stock

- +
Scroll to Top