TestX / chatbox.js
school44s's picture
Upload 13 files
cee43ad verified
Raw
History Blame Contribute Delete
7.79 kB
(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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function build() {
injectCss();
var fab = el('button', ID_PREFIX + '-fab', '&#128172;');
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">&#10005;</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();
}
})();