feat: /api/v2/analiza/* endpoints - sport analytics backend
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
/* _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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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();
|
||||
})();
|
||||
Reference in New Issue
Block a user