Files
pgz-sport/static/_ai_widget.js
T

237 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* _ai_widget.js — global floating DABI AI assistant
* Inject with: <script src="/static/_ai_widget.js" defer></script>
*
* Self-contained, no dependencies. Idempotent (refuses to mount twice).
* Reads JWT from localStorage.pgz_access (falls back to sessionStorage /
* localStorage.access_token). POSTs to /sport/api/v2/ai/ask with page
* context (path + hash) so the AI can ground answers in where the user is.
*/
(function () {
'use strict';
if (window.__ai_widget_mounted) return;
window.__ai_widget_mounted = true;
// ─────────── config ───────────
var ENDPOINT = '/sport/api/v2/ai/ask';
var STORAGE_HISTORY_KEY = '_ai_widget_history';
var MAX_HISTORY = 30;
function getToken() {
try {
return localStorage.getItem('pgz_access')
|| sessionStorage.getItem('pgz_access')
|| localStorage.getItem('access_token')
|| '';
} catch (e) { return ''; }
}
function pageContext() {
return {
path: location.pathname || '',
hash: (location.hash || '').replace(/^#/, ''),
title: document.title || ''
};
}
function loadHistory() {
try {
var raw = sessionStorage.getItem(STORAGE_HISTORY_KEY);
var arr = raw ? JSON.parse(raw) : [];
return Array.isArray(arr) ? arr.slice(-MAX_HISTORY) : [];
} catch (e) { return []; }
}
function saveHistory(arr) {
try {
sessionStorage.setItem(STORAGE_HISTORY_KEY, JSON.stringify(arr.slice(-MAX_HISTORY)));
} catch (e) {}
}
// ─────────── DOM ───────────
var btn = document.createElement('button');
btn.id = '_ai_widget_btn';
btn.type = 'button';
btn.title = 'DABI AI Copilot';
btn.setAttribute('aria-label', 'Otvori DABI AI Copilot');
btn.textContent = '🤖';
btn.style.cssText = [
'position:fixed', 'bottom:20px', 'right:20px', 'z-index:99998',
'width:54px', 'height:54px', 'border-radius:50%',
'background:#2563eb', 'color:#fff', 'border:0', 'font-size:24px',
'cursor:pointer', 'box-shadow:0 6px 18px rgba(0,0,0,0.25)',
'transition:transform 0.15s ease', 'line-height:1'
].join(';');
btn.onmouseenter = function () { btn.style.transform = 'scale(1.06)'; };
btn.onmouseleave = function () { btn.style.transform = 'scale(1.0)'; };
var panel = document.createElement('div');
panel.id = '_ai_widget_panel';
panel.style.cssText = [
'position:fixed', 'bottom:88px', 'right:20px', 'z-index:99999',
'width:380px', 'max-width:calc(100vw - 32px)',
'height:520px', 'max-height:calc(100vh - 120px)',
'display:none', 'flex-direction:column',
'background:#0f172a', 'color:#e2e8f0',
'border:1px solid #334155', 'border-radius:12px',
'box-shadow:0 16px 48px rgba(0,0,0,0.5)',
'font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif',
'font-size:13px', 'overflow:hidden'
].join(';');
panel.innerHTML = [
'<div id="_ai_widget_header" style="display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid #334155;background:#1e293b">',
' <span style="font-size:16px">🤖</span>',
' <span style="font-weight:600">DABI AI Copilot</span>',
' <span id="_ai_widget_status" style="margin-left:auto;font-size:11px;color:#94a3b8"></span>',
' <button id="_ai_widget_close" type="button" aria-label="Zatvori" style="background:transparent;border:0;color:#94a3b8;cursor:pointer;font-size:18px;padding:0 4px;line-height:1">×</button>',
'</div>',
'<div id="_ai_widget_msgs" style="flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px"></div>',
'<div style="border-top:1px solid #334155;padding:8px;display:flex;gap:6px;align-items:flex-end;background:#1e293b">',
' <textarea id="_ai_widget_q" rows="2" placeholder="Pitaj DABI…"',
' style="flex:1;padding:8px 10px;border:1px solid #334155;border-radius:6px;background:#0f172a;color:#e2e8f0;font-size:13px;resize:none;outline:none;font-family:inherit"></textarea>',
' <button id="_ai_widget_send" type="button"',
' style="padding:8px 12px;border:0;border-radius:6px;background:#2563eb;color:#fff;font-weight:600;cursor:pointer;font-size:13px">Pošalji</button>',
'</div>'
].join('');
function mount() {
if (!document.body) {
document.addEventListener('DOMContentLoaded', mount);
return;
}
document.body.appendChild(btn);
document.body.appendChild(panel);
wire();
rerender();
}
// ─────────── behaviour ───────────
var messages = loadHistory();
function setStatus(s) {
var el = document.getElementById('_ai_widget_status');
if (el) el.textContent = s || '';
}
function escHTML(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function rerender() {
var box = document.getElementById('_ai_widget_msgs');
if (!box) return;
if (!messages.length) {
box.innerHTML = '<div style="color:#64748b;font-size:12px;text-align:center;padding:20px">Postavi pitanje DABI-ju.<br>Kontekst trenutne stranice se šalje automatski.</div>';
return;
}
box.innerHTML = messages.map(function (m) {
var bubbleStyle = m.role === 'user'
? 'align-self:flex-end;background:#2563eb;color:#fff'
: (m.role === 'error'
? 'align-self:flex-start;background:#7f1d1d;color:#fecaca'
: 'align-self:flex-start;background:#1e293b;color:#e2e8f0;border:1px solid #334155');
return '<div style="max-width:85%;padding:8px 10px;border-radius:8px;white-space:pre-wrap;line-height:1.45;' + bubbleStyle + '">'
+ escHTML(m.text) + '</div>';
}).join('');
box.scrollTop = box.scrollHeight;
}
function open() {
panel.style.display = 'flex';
btn.style.display = 'none';
setTimeout(function () {
var q = document.getElementById('_ai_widget_q');
if (q) q.focus();
}, 50);
}
function close() {
panel.style.display = 'none';
btn.style.display = 'block';
}
function pushMsg(role, text) {
messages.push({ role: role, text: text, t: Date.now() });
if (messages.length > MAX_HISTORY) messages = messages.slice(-MAX_HISTORY);
saveHistory(messages);
rerender();
}
async function ask() {
var inp = document.getElementById('_ai_widget_q');
var sendBtn = document.getElementById('_ai_widget_send');
if (!inp || !sendBtn) return;
var q = (inp.value || '').trim();
if (!q) return;
var tok = getToken();
if (!tok) {
pushMsg('error', '⚠ Prijava potrebna. Otvori /login pa se vrati.');
setStatus('traži prijavu');
return;
}
pushMsg('user', q);
inp.value = '';
sendBtn.disabled = true;
sendBtn.textContent = '…';
setStatus('razmišljam…');
try {
var ctx = pageContext();
var body = {
question: q, query: q, q: q,
context: ctx,
page_path: ctx.path, page_hash: ctx.hash, page_title: ctx.title
};
var r = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok },
body: JSON.stringify(body)
});
if (r.status === 401) {
pushMsg('error', '⚠ Sesija je istekla. Otvori /login.');
setStatus('401');
return;
}
if (!r.ok) {
var t = '';
try { t = await r.text(); } catch (e) {}
pushMsg('error', '❌ HTTP ' + r.status + (t ? ' — ' + t.slice(0, 200) : ''));
setStatus('greška');
return;
}
var data = await r.json();
var answer = data.answer || data.response || data.text
|| (typeof data === 'string' ? data : JSON.stringify(data, null, 2).slice(0, 1500));
pushMsg('assistant', answer);
setStatus('odgovor');
} catch (e) {
pushMsg('error', '❌ ' + (e && e.message ? e.message : String(e)));
setStatus('greška');
} finally {
sendBtn.disabled = false;
sendBtn.textContent = 'Pošalji';
}
}
function wire() {
btn.addEventListener('click', open);
var closeBtn = document.getElementById('_ai_widget_close');
if (closeBtn) closeBtn.addEventListener('click', close);
var sendBtn = document.getElementById('_ai_widget_send');
if (sendBtn) sendBtn.addEventListener('click', ask);
var q = document.getElementById('_ai_widget_q');
if (q) {
q.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); ask(); }
if (ev.key === 'Escape') { close(); }
});
}
document.addEventListener('keydown', function (ev) {
// Cmd/Ctrl + K opens the widget — convenience shortcut
if ((ev.metaKey || ev.ctrlKey) && ev.key.toLowerCase() === 'k') {
ev.preventDefault();
if (panel.style.display === 'none') open(); else close();
}
});
}
mount();
})();