| (function () { |
| 'use strict'; |
|
|
| var API_DEFAULT = 'https://chatbox.npgia-himlam.workers.dev/'; |
| var ID_PREFIX = 'avchat'; |
| var MODELS = { nivip: 'nivip', zerta: 'zerta' }; |
| var MODEL_LABELS = { nivip: 'Nivip', zerta: 'Zerta' }; |
|
|
| function baseUrl() { |
| try { |
| var v = localStorage.getItem('anivietdb-chat-api'); |
| if (v) return v; |
| } catch (e) {} |
| return API_DEFAULT; |
| } |
|
|
| function injectCss() { |
| if (document.getElementById(ID_PREFIX + '-style')) return; |
| var st = document.createElement('style'); |
| st.id = ID_PREFIX + '-style'; |
| st.textContent = [ |
| '.' + ID_PREFIX + '-fab {', |
| ' position: fixed; right: 16px; bottom: 16px; width: 56px; height: 56px;', |
| ' border-radius: 50%; border: none; cursor: pointer; font-size: 24px; z-index: 9998;', |
| ' background: linear-gradient(135deg, #e84393, #a29bfe); color: #fff;', |
| ' box-shadow: 0 8px 24px rgba(232,67,147,.5); display: flex; align-items: center; justify-content: center;', |
| ' transition: transform .2s;', |
| '}', |
| '.' + ID_PREFIX + '-fab:hover { transform: scale(1.08); }', |
| '.' + ID_PREFIX + '-box {', |
| ' position: fixed; right: 16px; bottom: 16px; width: 340px; max-width: calc(100vw - 32px);', |
| ' background: #14141f; color: #fff; border-radius: 12px; overflow: hidden;', |
| ' box-shadow: 0 8px 30px rgba(0,0,0,.5); z-index: 9999;', |
| ' font-family: Roboto, Arial, sans-serif; display: none;', |
| '}', |
| '.' + ID_PREFIX + '-head { background: #1d1d2e; padding: 12px 16px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; }', |
| '.' + ID_PREFIX + '-head h3 { margin: 0; font-size: 15px; color: #fff; }', |
| '.' + ID_PREFIX + '-model { background: #14141f; color: #fff; border: 1px solid #444; border-radius: 6px; font-size: 12.5px; padding: 4px 6px; outline: none; cursor: pointer; }', |
| '.' + ID_PREFIX + '-close { background: none; border: none; color: #aaa; font-size: 15px; cursor: pointer; padding: 0 4px; }', |
| '.' + ID_PREFIX + '-close:hover { color: #fff; }', |
| '.' + ID_PREFIX + '-msgs { padding: 14px; max-height: 340px; overflow-y: auto; font-size: 14px; line-height: 1.5; background: #fff; color: #222; }', |
| '.' + ID_PREFIX + '-msgs .' + ID_PREFIX + '-user { text-align: right; margin: 6px 0; }', |
| '.' + ID_PREFIX + '-msgs .' + ID_PREFIX + '-user::before { content: "Bạn: "; color: #007aff; font-weight: 700; }', |
| '.' + ID_PREFIX + '-msgs .' + ID_PREFIX + '-ai { margin: 6px 0; white-space: pre-wrap; word-break: break-word; }', |
| '.' + ID_PREFIX + '-msgs .' + ID_PREFIX + '-ai::before { content: "AI: "; color: #e84393; font-weight: 700; }', |
| '.' + ID_PREFIX + '-msgs .' + ID_PREFIX + '-err { margin: 6px 0; color: #c0392b; white-space: pre-wrap; word-break: break-word; }', |
| '.' + ID_PREFIX + '-input { display: flex; border-top: 1px solid #333; background: #1d1d2e; }', |
| '.' + ID_PREFIX + '-input input { flex: 1; padding: 10px 12px; border: none; outline: none; background: #1d1d2e; color: #fff; font-size: 14px; }', |
| '.' + ID_PREFIX + '-input button { background: #e84393; color: #fff; border: none; padding: 0 16px; cursor: pointer; font-size: 14px; }', |
| '.' + ID_PREFIX + '-input button:disabled { opacity: .5; cursor: wait; }', |
| '.' + ID_PREFIX + '-input button:hover:not(:disabled) { background: #d63384; }' |
| ].join('\n'); |
| document.head.appendChild(st); |
| } |
|
|
| function el(tag, className, html) { |
| var n = document.createElement(tag); |
| if (className) n.className = className; |
| if (html !== undefined) n.innerHTML = html; |
| return n; |
| } |
|
|
| function escapeHtml(s) { |
| return String(s).replace(/[&<>"']/g, function (c) { |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; |
| }); |
| } |
|
|
| function build() { |
| injectCss(); |
|
|
| var fab = el('button', ID_PREFIX + '-fab', '💬'); |
| fab.title = 'Chat với trợ lý gợi ý anime/manga'; |
|
|
| var box = el('div', ID_PREFIX + '-box'); |
| box.innerHTML = |
| '<div class="' + ID_PREFIX + '-head">' + |
| '<h3>Chatbox ANIVIETDB</h3>' + |
| '<select class="' + ID_PREFIX + '-model" id="' + ID_PREFIX + '-model" aria-label="Chọn mô hình">' + |
| Object.keys(MODELS).map(function (k) { |
| return '<option value="' + k + '">' + MODEL_LABELS[k] + '</option>'; |
| }).join('') + |
| '</select>' + |
| '<button class="' + ID_PREFIX + '-close" id="' + ID_PREFIX + '-close" aria-label="Đóng">✕</button>' + |
| '</div>'; |
| var msgs = el('div', ID_PREFIX + '-msgs'); |
| msgs.innerHTML = '<div class="' + ID_PREFIX + '-ai">Chào bạn! Mình là trợ lý gợi ý anime, manga và light novel của ANIVIETDB. Bạn muốn xem gì hôm nay?</div>'; |
| box.appendChild(msgs); |
| var inputRow = el('div', ID_PREFIX + '-input'); |
| var inp = el('input'); |
| inp.type = 'text'; |
| inp.placeholder = 'Nhập tin nhắn...'; |
| inp.autocomplete = 'off'; |
| var sendBtn = el('button', null, 'Gửi'); |
| inputRow.appendChild(inp); |
| inputRow.appendChild(sendBtn); |
| box.appendChild(inputRow); |
| document.body.appendChild(box); |
| document.body.appendChild(fab); |
|
|
| var closeBtn = box.querySelector('#' + ID_PREFIX + '-close'); |
| var modelSel = box.querySelector('#' + ID_PREFIX + '-model'); |
|
|
| function currentModel() { |
| var saved = null; |
| try { saved = localStorage.getItem('anivietdb-chat-model'); } catch (e) {} |
| return MODELS[saved] ? saved : 'nivip'; |
| } |
|
|
| modelSel.value = currentModel(); |
| modelSel.addEventListener('change', function () { |
| try { localStorage.setItem('anivietdb-chat-model', modelSel.value); } catch (e) {} |
| }); |
|
|
| function toggle(show) { |
| box.style.display = show ? 'block' : 'none'; |
| fab.style.display = show ? 'none' : 'block'; |
| if (show) inp.focus(); |
| } |
|
|
| function send() { |
| var msg = inp.value.trim(); |
| if (!msg) return; |
| inp.value = ''; |
| msgs.innerHTML += '<div class="' + ID_PREFIX + '-user">' + escapeHtml(msg) + '</div>'; |
| msgs.scrollTop = msgs.scrollHeight; |
| var p = el('div', ID_PREFIX + '-ai', '...'); |
| msgs.appendChild(p); |
| msgs.scrollTop = msgs.scrollHeight; |
| sendBtn.disabled = true; |
| inp.disabled = true; |
| fetch(baseUrl().replace(/\/+$/, '') + '/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: msg, model: modelSel.value }) |
| }).then(function (r) { |
| return r.json().catch(function () { return {}; }).then(function (d) { |
| if (!r.ok) { |
| var e = new Error((d && d.error) || ('HTTP ' + r.status)); |
| e.status = r.status; |
| throw e; |
| } |
| return d; |
| }); |
| }).then(function (d) { |
| p.className = ID_PREFIX + '-ai'; |
| p.textContent = d.reply || '...'; |
| msgs.scrollTop = msgs.scrollHeight; |
| }).catch(function (e) { |
| p.className = ID_PREFIX + '-err'; |
| p.textContent = 'Lỗi: ' + (e.message || 'không rõ'); |
| msgs.scrollTop = msgs.scrollHeight; |
| }).finally(function () { |
| sendBtn.disabled = false; |
| inp.disabled = false; |
| inp.focus(); |
| }); |
| } |
|
|
| fab.addEventListener('click', function () { toggle(true); }); |
| closeBtn.addEventListener('click', function () { toggle(false); }); |
| sendBtn.addEventListener('click', send); |
| inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') send(); }); |
| } |
|
|
| function init() { |
| if (document.getElementById(ID_PREFIX + '-fab')) return; |
| build(); |
| } |
|
|
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', init); |
| } else { |
| init(); |
| } |
| })(); |