kiafa's picture
Premium UI/UX Overhaul & Optimization Update
633633c verified
import './style.css';
// =====================================================================
// KIA COMMAND CENTER — v3.0 Multi-Role Edition
// Features: Multi-Role Auth, Streaming, Toast Notifications,
// Dashboard Animations, Classification Badges, Confidence,
// Processing Status, Post-Response Suggestions, Shortcuts
// =====================================================================
// --- DOM REFERENCES ---
const chatBox = document.getElementById('chat-box');
const chatInput = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const ttsAudio = document.getElementById('tts-audio');
const fileInput = document.getElementById('file-upload');
const recordBtn = document.getElementById('record-btn');
const clock = document.getElementById('military-clock');
// API URL
const API_BASE = window.location.origin === "http://localhost:5173"
? 'http://localhost:8001/api'
: '/api';
// --- STATE ---
let scannedTextCache = "";
let isThinking = false;
let sessionId = localStorage.getItem('shp_session_id') || "";
let currentView = 'dashboard';
let mapInstance = null;
let ttsEnabled = false;
let typewriterEnabled = true;
let currentRole = localStorage.getItem('shp_role') || "";
let currentClassification = "I PAKLASIFIKUAR";
let messageIndex = 0; // Global counter for feedback tracking
// Role display config
const ROLE_CONFIG = {
visitor: { name: "Vizitor", classification: "I PAKLASIFIKUAR", color: "green", greeting: "Vizitor i nderuar" },
officer: { name: "Oficer", classification: "I KUFIZUAR", color: "yellow", greeting: "I nderuar Oficer" },
commander: { name: "Komandant", classification: "KONFIDENCIAL", color: "orange", greeting: "Komandant i nderuar" },
general: { name: "Gjeneral", classification: "SEKRET", color: "red", greeting: "Shkëlqesia juaj, Gjeneral" },
};
// =====================================================================
// ROLE SELECTION SCREEN
// =====================================================================
function initRoleScreen() {
const roleScreen = document.getElementById('role-screen');
const loginOverlay = document.getElementById('login-overlay');
const accessInput = document.getElementById('access-code-input');
const loginSubmitBtn = document.getElementById('login-submit-btn');
const loginCancelBtn = document.getElementById('login-cancel-btn');
const loginRoleTxt = document.getElementById('login-role-txt');
if (!roleScreen) return;
// If already has a role and token, skip (simplification for prototype)
if (localStorage.getItem('shp_token') && currentRole && ROLE_CONFIG[currentRole]) {
roleScreen.style.display = 'none';
applyRole(currentRole);
showLoadingScreen();
return;
}
roleScreen.style.display = 'flex';
let pendingRole = null;
let pendingCard = null;
document.querySelectorAll('.role-card').forEach(card => {
card.addEventListener('click', () => {
pendingRole = card.dataset.role;
pendingCard = card;
const config = ROLE_CONFIG[pendingRole];
if (pendingRole === 'visitor') {
// No password needed for visitor
attemptLogin(pendingRole, "", card, roleScreen);
} else {
loginRoleTxt.textContent = `Akses: ${config.classification}`;
loginOverlay.classList.remove('hidden');
accessInput.value = '';
accessInput.focus();
}
});
});
loginCancelBtn?.addEventListener('click', () => {
loginOverlay.classList.add('hidden');
pendingRole = null;
pendingCard = null;
});
loginSubmitBtn?.addEventListener('click', () => {
if (pendingRole) {
attemptLogin(pendingRole, accessInput.value, pendingCard, roleScreen);
}
});
accessInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && pendingRole) {
attemptLogin(pendingRole, accessInput.value, pendingCard, roleScreen);
}
});
}
async function attemptLogin(role, accessCode, card, roleScreen) {
const loginOverlay = document.getElementById('login-overlay');
try {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: role, access_code: accessCode })
});
if (res.ok) {
const data = await res.json();
currentRole = role;
localStorage.setItem('shp_role', role);
localStorage.setItem('shp_token', data.access_token);
if (loginOverlay) loginOverlay.classList.add('hidden');
card.classList.add('selected');
setTimeout(() => {
roleScreen.classList.add('fade-out');
setTimeout(() => {
roleScreen.style.display = 'none';
applyRole(role);
showLoadingScreen();
}, 500);
}, 300);
} else {
const err = await res.json();
alert(err.detail || "Verifikimi dështoi");
}
} catch(e) {
alert("Gabim lidhjeje me serverin e verifikimit");
}
}
function applyRole(role) {
const config = ROLE_CONFIG[role] || ROLE_CONFIG.visitor;
currentClassification = config.classification;
// Update classification banner
const banner = document.getElementById('classification-banner');
if (banner) {
banner.textContent = config.classification;
banner.className = `classification-banner class-${config.color}`;
}
// Update user badge
const badge = document.getElementById('user-badge');
const label = document.getElementById('user-role-label');
if (badge) badge.className = `user-badge role-${config.color}`;
if (label) label.textContent = config.name.toUpperCase();
// Update welcome title
const welcome = document.getElementById('welcome-title');
if (welcome) welcome.textContent = `Mirë se vini, ${config.greeting}`;
// Update initial chat message classification tag
const tag = document.getElementById('msg-class-tag');
if (tag) {
tag.textContent = config.classification;
tag.className = `msg-classification class-${config.color}`;
}
}
// =====================================================================
// LOADING SCREEN WITH REAL STEPS
// =====================================================================
function showLoadingScreen() {
const loadingScreen = document.getElementById('loading-screen');
if (!loadingScreen) return;
loadingScreen.classList.remove('hidden');
loadingScreen.style.display = 'flex';
const progressBar = document.getElementById('progress-bar');
const steps = document.querySelectorAll('.loader-step');
// Animate steps sequentially
let stepIndex = 0;
const stepInterval = setInterval(() => {
if (stepIndex < steps.length) {
steps[stepIndex].classList.add('active');
if (progressBar) progressBar.style.width = `${((stepIndex + 1) / steps.length) * 100}%`;
stepIndex++;
} else {
clearInterval(stepInterval);
}
}, 600);
// Start health check
bootSystem();
}
let healthRetries = 0;
const MAX_HEALTH_RETRIES = 5;
async function bootSystem() {
try {
const res = await fetch(`${API_BASE}/health`);
if (res.ok) {
const data = await res.json();
updateDashboardData(data);
finishLoading();
return;
}
} catch (e) {
console.warn("Health check failed, retry:", healthRetries);
}
healthRetries++;
if (healthRetries < MAX_HEALTH_RETRIES) {
setTimeout(bootSystem, 2000);
} else {
// Fallback: show app anyway
finishLoading();
showToast("⚠️ Sistemi u nis pa lidhje me serverin", "warning");
}
}
function finishLoading() {
const loaderText = document.getElementById('loader-text');
if (loaderText) loaderText.textContent = "SISTEMI OPERACIONAL. NISJA...";
const progressBar = document.getElementById('progress-bar');
if (progressBar) {
progressBar.style.width = '100%';
progressBar.style.animation = 'none';
}
// Mark all steps complete
document.querySelectorAll('.loader-step').forEach(s => s.classList.add('active', 'done'));
setTimeout(() => {
document.getElementById('loading-screen')?.classList.add('hidden');
document.getElementById('app')?.classList.add('ready');
}, 800);
}
// =====================================================================
// MILITARY CLOCK
// =====================================================================
function updateClock() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
if (clock) clock.textContent = `${h}:${m}:${s}`;
}
setInterval(updateClock, 1000);
updateClock();
function getTimeStr() {
const now = new Date();
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
}
// =====================================================================
// TOAST NOTIFICATION SYSTEM
// =====================================================================
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const icons = { info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌' };
toast.innerHTML = `
<span class="toast-icon">${icons[type] || icons.info}</span>
<span class="toast-msg">${message}</span>
`;
container.appendChild(toast);
// Trigger animation
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
toast.classList.add('hide');
setTimeout(() => toast.remove(), 400);
}, duration);
}
// =====================================================================
// VIEW ROUTER
// =====================================================================
function switchView(viewName) {
currentView = viewName;
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
const target = document.getElementById(`view-${viewName}`);
if (target) target.classList.add('active');
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const navBtn = document.querySelector(`[data-view="${viewName}"]`);
if (navBtn) navBtn.classList.add('active');
if (viewName === 'map' && !mapInstance) {
setTimeout(initMap, 100);
}
if (viewName === 'dashboard') {
checkHealth();
}
document.getElementById('sidebar')?.classList.remove('open');
}
// Nav item clicks
document.querySelectorAll('.nav-item').forEach(btn => {
btn.addEventListener('click', () => switchView(btn.dataset.view));
});
// Dashboard "Start Chat" button
document.getElementById('start-chat-btn')?.addEventListener('click', () => switchView('chat'));
// Quick action buttons
document.querySelectorAll('.quick-action[data-query]').forEach(btn => {
btn.addEventListener('click', () => {
switchView('chat');
setTimeout(() => sendMessage(btn.dataset.query), 200);
});
});
// Quick upload button on dashboard
document.getElementById('quick-upload')?.addEventListener('click', () => {
switchView('documents');
});
// SITREP generator button on dashboard
document.getElementById('quick-sitrep')?.addEventListener('click', () => {
generateSitrep();
});
// Mobile menu toggle
document.getElementById('mobile-menu-btn')?.addEventListener('click', () => {
document.getElementById('sidebar')?.classList.toggle('open');
});
// =====================================================================
// MARKDOWN RENDERING
// =====================================================================
function renderMarkdown(text) {
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/__(.*?)__/g, '<strong>$1</strong>');
text = text.replace(/(?<!\*)\*(?!\*)(.*?)\*(?!\*)/g, '<em>$1</em>');
text = text.replace(/\[(VLERËSIMI I SITUATËS|ANALIZA INTELIGJENTE|ANALIZA KRAHASUESE|REKOMANDIMI)\]/g,
'<div class="section-header">◆ $1</div>');
text = text.replace(/^[•\-]\s+(.+)$/gm, '<div class="bullet-item">• $1</div>');
text = text.replace(/^(\d+)\.\s+(.+)$/gm, '<div class="numbered-item"><span class="num">$1.</span> $2</div>');
text = text.replace(/\n\n/g, '<br><br>');
text = text.replace(/\n/g, '<br>');
return text;
}
// =====================================================================
// SUGGESTION CHIPS
// =====================================================================
async function loadSuggestions() {
const container = document.getElementById('suggestions');
if (!container) return;
container.innerHTML = '';
const fallback = [
"Cili është zinxhiri i komandimit?",
"Buxheti i mbrojtjes 2026",
"Misionet KFOR",
"Pajisjet e reja ushtarake",
"Departamentet J",
"Bashkëpunimi NATO",
];
let questions = fallback;
try {
const res = await fetch(`${API_BASE}/suggestions?role=${currentRole || 'visitor'}`);
const data = await res.json();
if (data.suggestions?.length) questions = data.suggestions;
} catch (e) { /* use fallback */ }
questions.forEach(q => {
const chip = document.createElement('button');
chip.className = 'suggestion-chip';
chip.innerText = q;
chip.addEventListener('click', () => {
chatInput.value = q;
sendMessage(q);
container.style.display = 'none';
});
container.appendChild(chip);
});
}
// =====================================================================
// CHAT — MESSAGE HANDLING
// =====================================================================
function showProcessingStatus() {
const ps = document.getElementById('processing-status');
if (ps) {
ps.style.display = 'flex';
const ragStep = document.getElementById('ps-rag');
const modelStep = document.getElementById('ps-model');
if (ragStep) ragStep.classList.add('active');
if (modelStep) modelStep.classList.remove('active');
// After 1s, show model step
setTimeout(() => {
if (ragStep) ragStep.classList.add('done');
if (modelStep) modelStep.classList.add('active');
}, 800);
}
}
function hideProcessingStatus() {
const ps = document.getElementById('processing-status');
if (ps) {
ps.style.display = 'none';
document.getElementById('ps-rag')?.classList.remove('active', 'done');
document.getElementById('ps-model')?.classList.remove('active', 'done');
}
}
function showThinking() {
const div = document.createElement('div');
div.className = 'message bot';
div.id = 'thinking-indicator';
div.innerHTML = `
<div class="msg-avatar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</div>
<div class="msg-body">
<div class="msg-meta">
<span class="msg-sender">KIA</span>
<span class="msg-time">Duke procesuar...</span>
</div>
<div class="msg-content">
<div class="thinking-dots"><span></span><span></span><span></span></div>
</div>
</div>
`;
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
function removeThinking() {
const el = document.getElementById('thinking-indicator');
if (el) el.remove();
}
// Manual TTS Helper
async function speakText(text) {
try {
const dummyForm = new FormData();
dummyForm.append("text", text.substring(0, 500));
const res = await fetch(`${API_BASE}/tts`, { method: "POST", body: dummyForm });
if (res.ok) {
const audioBlob = await res.blob();
ttsAudio.src = URL.createObjectURL(audioBlob);
ttsAudio.play().catch(() => {});
}
} catch (err) {
console.warn("TTS playback failed:", err);
}
}
function createBotMessageContainer() {
const div = document.createElement('div');
div.className = 'message bot';
const avatarSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>';
const roleConfig = ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor;
const classTag = `<span class="msg-classification class-${roleConfig.color}">${roleConfig.classification}</span>`;
div.innerHTML = `
<div class="msg-avatar">${avatarSvg}</div>
<div class="msg-body">
<div class="msg-meta">
<span class="msg-sender">KIA</span>
<span class="msg-time">${getTimeStr()}</span>
${classTag}
<span class="conf-placeholder"></span>
</div>
<div class="msg-content"><span class="msg-text typing"></span></div>
</div>
`;
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
return {
container: div,
textSpan: div.querySelector('.msg-text'),
contentDiv: div.querySelector('.msg-content'),
metaDiv: div.querySelector('.msg-meta'),
confPlaceholder: div.querySelector('.conf-placeholder')
};
}
function finalizeBotMessageActions(contentDiv, text, meta, sources, userQuery = '') {
const textSpan = contentDiv.querySelector('.msg-text');
if (textSpan) textSpan.classList.remove('typing');
const actionsDiv = document.createElement('div');
actionsDiv.className = 'msg-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'msg-action-btn';
copyBtn.innerHTML = '📋 Kopjo';
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(text);
copyBtn.innerHTML = '✅ Kopjuar';
showToast('Teksti u kopjua', 'success', 2000);
setTimeout(() => copyBtn.innerHTML = '📋 Kopjo', 2000);
});
actionsDiv.appendChild(copyBtn);
const speakerBtn = document.createElement('button');
speakerBtn.className = 'msg-action-btn';
speakerBtn.innerHTML = '🔊 Dëgjo';
speakerBtn.addEventListener('click', () => speakText(text));
actionsDiv.appendChild(speakerBtn);
// Feedback buttons (👍/👎)
const currentMsgIdx = messageIndex++;
const feedbackGroup = document.createElement('span');
feedbackGroup.className = 'feedback-group';
const thumbUp = document.createElement('button');
thumbUp.className = 'msg-action-btn feedback-btn';
thumbUp.innerHTML = '👍';
thumbUp.title = 'Përgjigje e mirë';
const thumbDown = document.createElement('button');
thumbDown.className = 'msg-action-btn feedback-btn';
thumbDown.innerHTML = '👎';
thumbDown.title = 'Përgjigje e dobët';
const handleFeedback = async (rating, btn, otherBtn) => {
btn.classList.add('feedback-active');
otherBtn.classList.remove('feedback-active');
btn.disabled = true;
otherBtn.disabled = true;
try {
const shpToken = localStorage.getItem('shp_token') || '';
await fetch(`${API_BASE}/feedback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${shpToken}` },
body: JSON.stringify({
session_id: sessionId,
message_index: currentMsgIdx,
rating: rating,
query: userQuery,
response_preview: text.substring(0, 300)
})
});
showToast(rating === 'up' ? '👍 Faleminderit!' : '📝 Vlerësimi u regjistrua', 'success', 2000);
} catch(e) {
console.warn('Feedback failed:', e);
}
};
thumbUp.addEventListener('click', () => handleFeedback('up', thumbUp, thumbDown));
thumbDown.addEventListener('click', () => handleFeedback('down', thumbDown, thumbUp));
feedbackGroup.appendChild(thumbUp);
feedbackGroup.appendChild(thumbDown);
actionsDiv.appendChild(feedbackGroup);
if (meta?.latency_ms) {
const latencySpan = document.createElement('span');
latencySpan.className = 'msg-latency';
latencySpan.textContent = `⏱️ ${(meta.latency_ms / 1000).toFixed(1)}s`;
actionsDiv.appendChild(latencySpan);
const intelLatency = document.getElementById('intel-latency');
if (intelLatency) intelLatency.textContent = `${meta.latency_ms}ms`;
}
contentDiv.appendChild(actionsDiv);
if (sources && sources.length > 0) {
const sourcesDiv = document.createElement('div');
sourcesDiv.className = 'msg-sources';
sources.forEach(s => {
const badge = document.createElement('span');
badge.className = 'source-badge';
badge.innerHTML = `<span class="source-dot"></span> ${s}`;
sourcesDiv.appendChild(badge);
});
contentDiv.appendChild(sourcesDiv);
updateIntelSources(sources);
}
if (text.length > 100) {
showPostSuggestions(text);
}
}
async function appendMessage(text, isUser, sources = [], meta = null) {
if (!isUser) {
const bot = createBotMessageContainer();
if (meta?.confidence) {
const confMap = {
high: { icon: '🟢', text: 'Burime zyrtare', cls: 'conf-high' },
medium: { icon: '🟡', text: 'Informacion i përgjithshëm', cls: 'conf-medium' },
low: { icon: '🔴', text: 'Pa burime direkte', cls: 'conf-low' },
};
const c = confMap[meta.confidence] || confMap.low;
bot.confPlaceholder.innerHTML = `<span class="confidence-badge ${c.cls}">${c.icon} ${c.text}</span>`;
}
bot.textSpan.innerHTML = renderMarkdown(text);
finalizeBotMessageActions(bot.contentDiv, text, meta, sources);
return;
}
// User message
const div = document.createElement('div');
div.className = 'message user';
const avatarSvg = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
div.innerHTML = `
<div class="msg-avatar">${avatarSvg}</div>
<div class="msg-body">
<div class="msg-meta">
<span class="msg-sender">${(ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor).name.toUpperCase()}</span>
<span class="msg-time">${getTimeStr()}</span>
</div>
<div class="msg-content"><span class="msg-text"></span></div>
</div>
`;
chatBox.appendChild(div);
div.querySelector('.msg-text').innerText = text;
chatBox.scrollTop = chatBox.scrollHeight;
}
// =====================================================================
// POST-RESPONSE SUGGESTIONS
// =====================================================================
function showPostSuggestions(responseText) {
// Remove existing
document.querySelectorAll('.post-suggestions').forEach(el => el.remove());
const suggestions = generateRelatedQuestions(responseText);
if (suggestions.length === 0) return;
const container = document.createElement('div');
container.className = 'post-suggestions';
container.innerHTML = '<span class="ps-label">Pyetje ngjashme:</span>';
suggestions.forEach(q => {
const chip = document.createElement('button');
chip.className = 'post-suggestion-chip';
chip.textContent = q;
chip.addEventListener('click', () => {
container.remove();
sendMessage(q);
});
container.appendChild(chip);
});
chatBox.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight;
}
function generateRelatedQuestions(text) {
const questions = [];
const lowerText = text.toLowerCase();
if (lowerText.includes('buxhet') || lowerText.includes('financ')) {
questions.push('Si krahasohet buxheti me vendet fqinje?');
}
if (lowerText.includes('nato') || lowerText.includes('aleancë')) {
questions.push('Cilat janë detyrimet e Shqipërisë ndaj NATO?');
}
if (lowerText.includes('forc') || lowerText.includes('ushtri')) {
questions.push('Sa persona shërbejnë aktualisht në FA?');
}
if (lowerText.includes('misione') || lowerText.includes('kfor')) {
questions.push('Cilat janë misionet aktive tani?');
}
if (lowerText.includes('modern') || lowerText.includes('pajisje')) {
questions.push('Cilat janë projektet e ardhshme të modernizimit?');
}
if (lowerText.includes('shtab') || lowerText.includes('komandim')) {
questions.push('Si funksionon departamenti J-3?');
}
// Always add a general follow-up
if (questions.length === 0) {
questions.push('Më jep më shumë detaje');
}
return questions.slice(0, 3);
}
// Update Intel Panel with sources
function updateIntelSources(sources) {
const section = document.getElementById('sources-section');
const list = document.getElementById('sources-list');
if (!section || !list) return;
section.style.display = 'block';
list.innerHTML = '';
sources.forEach(s => {
const item = document.createElement('div');
item.className = 'source-item';
item.innerHTML = `<span class="si-icon"></span> ${s}`;
list.appendChild(item);
});
}
// =====================================================================
// SLASH COMMANDS
// =====================================================================
const SLASH_COMMANDS = {
'/mot': { description: 'Moti taktik', transform: (args) => `Si është moti në ${args || 'Kuçovë'}?` },
'/detar': { description: 'Kushtet detare', transform: (args) => `Si janë kushtet detare në ${args || 'Pashaliman'}?` },
'/lajme': { description: 'Lajmet e fundit', transform: (args) => `Cilat janë lajmet e fundit ${args ? 'për ' + args : 'të mbrojtjes'}?` },
'/nato': { description: 'Zhvillimet NATO', transform: () => 'Cilat janë zhvillimet e fundit në NATO?' },
'/termet': { description: 'Aktiviteti sizmik', transform: () => 'Ka pasur tërmete afër Shqipërisë kohët e fundit?' },
'/kurs': { description: 'Kursi i këmbimit', transform: () => 'Sa është kursi i këmbimit EUR/LEK sot?' },
'/sitrep': { description: 'Gjeneroj SITREP', action: 'sitrep' },
'/pastro': { description: 'Pastro bisedën', action: 'clear' },
'/help': { description: 'Ndihmë komandat', action: 'help' },
};
function handleSlashCommand(input) {
const parts = input.trim().split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1).join(' ');
const command = SLASH_COMMANDS[cmd];
if (!command) return null;
if (command.action === 'help') {
let helpText = '**⌨️ Komandat e Disponueshme:**\n\n';
Object.entries(SLASH_COMMANDS).forEach(([key, val]) => {
helpText += `• \`${key}\` — ${val.description}\n`;
});
helpText += '\n_Shkruani komandën dhe shtypni Enter_';
appendMessage(helpText, false);
return 'handled';
}
if (command.action === 'clear') {
document.getElementById('new-session-btn')?.click();
return 'handled';
}
if (command.action === 'sitrep') {
generateSitrep();
return 'handled';
}
if (command.transform) {
return command.transform(args);
}
return null;
}
// =====================================================================
// CHAT — SEND MESSAGE
// =====================================================================
async function sendMessage(message) {
if (!message || isThinking) return;
// Check for slash commands
if (message.startsWith('/')) {
const result = handleSlashCommand(message);
if (result === 'handled') {
chatInput.value = '';
return;
}
if (result) {
message = result; // Transform command to natural query
}
}
isThinking = true;
chatInput.disabled = true;
sendBtn.disabled = true;
// Hide suggestions
const suggestions = document.getElementById('suggestions');
if (suggestions) suggestions.style.display = 'none';
// Remove post-suggestions
document.querySelectorAll('.post-suggestions').forEach(el => el.remove());
appendMessage(message, true);
chatInput.value = '';
showThinking();
showProcessingStatus();
try {
const shpToken = localStorage.getItem('shp_token') || "";
const res = await fetch(`${API_BASE}/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${shpToken}`
},
body: JSON.stringify({
message: message,
scanned_text: scannedTextCache,
session_id: sessionId
})
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
removeThinking();
hideProcessingStatus();
const botElement = createBotMessageContainer();
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
let fullResponse = "";
let finalSources = [];
let finalMeta = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop(); // keep partial chunks
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
if (data.type === 'meta') {
finalSources = data.sources || [];
finalMeta = data.meta || null;
if (data.session_id) {
sessionId = data.session_id;
localStorage.setItem('shp_session_id', sessionId);
}
if (finalMeta?.confidence) {
const confMap = {
high: { icon: '🟢', text: 'Burime zyrtare', cls: 'conf-high' },
medium: { icon: '🟡', text: 'Informacion i përgjithshëm', cls: 'conf-medium' },
low: { icon: '🔴', text: 'Pa burime direkte', cls: 'conf-low' },
};
const c = confMap[finalMeta.confidence] || confMap.low;
botElement.confPlaceholder.innerHTML = `<span class="confidence-badge ${c.cls}">${c.icon} ${c.text}</span>`;
}
} else if (data.type === 'widget') {
renderDynamicWidget(data.widget_type, data.data, botElement.contentDiv);
} else if (data.type === 'clear') {
fullResponse = "";
botElement.textSpan.innerHTML = "";
} else if (data.type === 'chunk') {
fullResponse += data.content;
botElement.textSpan.innerHTML = renderMarkdown(fullResponse);
chatBox.scrollTop = chatBox.scrollHeight;
} else if (data.type === 'done') {
if (data.latency_ms && finalMeta) finalMeta.latency_ms = data.latency_ms;
} else if (data.type === 'error') {
fullResponse += "\n\n**[GABIM]** " + data.content;
botElement.textSpan.innerHTML = renderMarkdown(fullResponse);
}
} catch(e) { console.error("Parse error", e); }
}
}
}
finalizeBotMessageActions(botElement.contentDiv, fullResponse, finalMeta, finalSources, message);
saveToHistory(message, fullResponse);
if (ttsEnabled) {
try {
const dummyForm = new FormData();
dummyForm.append("text", fullResponse.substring(0, 500));
const ttsRes = await fetch(`${API_BASE}/tts`, { method: "POST", body: dummyForm });
if (ttsRes.ok) {
const audioBlob = await ttsRes.blob();
ttsAudio.src = URL.createObjectURL(audioBlob);
ttsAudio.play().catch(() => {});
}
} catch (ttsErr) {
console.warn("TTS unavailable:", ttsErr);
}
}
} catch (err) {
removeThinking();
hideProcessingStatus();
console.error(err);
appendMessage("GABIM: Lidhja me Qendrën e Inteligjencës dështoi. Kontrolloni lidhjen.", false);
showToast("Lidhja me serverin dështoi", "error");
} finally {
isThinking = false;
chatInput.disabled = false;
sendBtn.disabled = false;
chatInput.focus();
}
}
// =====================================================================
// EVENT LISTENERS
// =====================================================================
sendBtn?.addEventListener('click', () => sendMessage(chatInput.value.trim()));
chatInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage(chatInput.value.trim());
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// F1 for help
if (e.key === 'F1') {
e.preventDefault();
toggleShortcutsModal();
}
// Ctrl+/ to focus input
if (e.ctrlKey && e.key === '/') {
e.preventDefault();
switchView('chat');
setTimeout(() => chatInput?.focus(), 100);
}
// Ctrl+N for new session
if (e.ctrlKey && e.key === 'n') {
e.preventDefault();
document.getElementById('new-session-btn')?.click();
}
// Ctrl+P for PDF export
if (e.ctrlKey && e.key === 'p') {
e.preventDefault();
document.getElementById('export-pdf-btn')?.click();
}
// Escape to clear input
if (e.key === 'Escape') {
if (document.activeElement === chatInput) {
chatInput.value = '';
chatInput.blur();
}
// Close modals
document.getElementById('shortcuts-modal').style.display = 'none';
}
// Number keys for navigation (when not in input)
if (document.activeElement !== chatInput && !e.ctrlKey && !e.altKey) {
const viewMap = { '1': 'dashboard', '2': 'chat', '3': 'map', '4': 'documents', '5': 'orgchart' };
if (viewMap[e.key]) {
switchView(viewMap[e.key]);
}
}
});
// Config toggles
document.getElementById('toggle-tts')?.addEventListener('change', (e) => {
ttsEnabled = e.target.checked;
showToast(ttsEnabled ? "Zëri automatik: AKTIV" : "Zëri automatik: JOAKTIV", "info", 2000);
});
document.getElementById('toggle-typewriter')?.addEventListener('change', (e) => {
typewriterEnabled = e.target.checked;
});
// Help button
document.getElementById('help-btn')?.addEventListener('click', toggleShortcutsModal);
document.getElementById('close-shortcuts')?.addEventListener('click', () => {
document.getElementById('shortcuts-modal').style.display = 'none';
});
function toggleShortcutsModal() {
const modal = document.getElementById('shortcuts-modal');
if (modal) modal.style.display = modal.style.display === 'none' ? 'flex' : 'none';
}
// =====================================================================
// FILE UPLOAD / OCR
// =====================================================================
fileInput?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
await processFile(file);
});
document.getElementById('doc-upload-input')?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
await processFile(file);
});
async function processFile(file) {
const formData = new FormData();
formData.append("document", file);
showToast(`📄 Duke skanuar "${file.name}"...`, "info", 3000);
try {
const res = await fetch(`${API_BASE}/ocr`, { method: "POST", body: formData });
const data = await res.json();
scannedTextCache = data.text;
addDocumentToLibrary(file.name, data.text);
showToast(`✅ Dokumenti "${file.name}" u skanua me sukses`, "success");
if (currentView === 'chat') {
appendMessage(`📄 Dokumenti "${file.name}" u skanua me sukses. (${data.text.length} karaktere)`, false);
// Suggest questions about the document
showDocumentSuggestions(file.name);
} else {
switchView('chat');
setTimeout(() => {
appendMessage(`📄 Dokumenti "${file.name}" u skanua me sukses. Mund të bëni pyetje mbi përmbajtjen.`, false);
showDocumentSuggestions(file.name);
}, 300);
}
} catch (err) {
console.error("OCR Error:", err);
appendMessage("❌ Gabim gjatë skanimit të dokumentit.", false);
showToast("Skanimi i dokumentit dështoi", "error");
}
}
function showDocumentSuggestions(filename) {
const container = document.createElement('div');
container.className = 'post-suggestions';
container.innerHTML = '<span class="ps-label">Pyetni për dokumentin:</span>';
const questions = [
`Përmbledh dokumentin "${filename}"`,
"Cilat janë pikat kryesore?",
"Çfarë rekomandimesh jep ky dokument?",
];
questions.forEach(q => {
const chip = document.createElement('button');
chip.className = 'post-suggestion-chip';
chip.textContent = q;
chip.addEventListener('click', () => {
container.remove();
sendMessage(q);
});
container.appendChild(chip);
});
chatBox.appendChild(container);
chatBox.scrollTop = chatBox.scrollHeight;
}
// =====================================================================
// VOICE RECORDING (STT)
// =====================================================================
let isRecording = false;
let mediaRecorder;
let audioChunks = [];
recordBtn?.addEventListener('click', async () => {
if (!isRecording) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
isRecording = true;
recordBtn.classList.add('recording');
audioChunks = [];
showToast("🎤 Regjistrimi filloi...", "info", 2000);
mediaRecorder.addEventListener("dataavailable", event => {
audioChunks.push(event.data);
});
mediaRecorder.addEventListener("stop", async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
const formData = new FormData();
formData.append("audio", audioBlob, "recording.wav");
try {
const res = await fetch(`${API_BASE}/stt`, { method: "POST", body: formData });
const data = await res.json();
if (data.text) {
showToast("🎤 Zëri u njoh me sukses", "success", 2000);
sendMessage(data.text);
}
} catch (e) {
console.error("STT Error", e);
showToast("Njohja e zërit dështoi", "error");
}
});
} catch (e) {
showToast("Ju lutem jepni leje për mikrofonin!", "warning");
}
} else {
mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(t => t.stop());
isRecording = false;
recordBtn.classList.remove('recording');
}
});
// =====================================================================
// CONVERSATION HISTORY (localStorage)
// =====================================================================
function getConversations() {
try {
return JSON.parse(localStorage.getItem('shp_conversations') || '[]');
} catch { return []; }
}
function saveToHistory(userMsg, botReply) {
const conversations = getConversations();
let session = conversations.find(c => c.id === sessionId);
if (!session) {
session = {
id: sessionId,
title: userMsg.substring(0, 40) + (userMsg.length > 40 ? '...' : ''),
created: new Date().toISOString(),
messages: []
};
conversations.unshift(session);
}
session.messages.push(
{ role: 'user', content: userMsg, time: new Date().toISOString() },
{ role: 'bot', content: botReply, time: new Date().toISOString() }
);
session.updated = new Date().toISOString();
if (conversations.length > 20) conversations.length = 20;
localStorage.setItem('shp_conversations', JSON.stringify(conversations));
renderHistoryList();
}
function renderHistoryList() {
const list = document.getElementById('history-list');
if (!list) return;
const conversations = getConversations();
list.innerHTML = '';
conversations.slice(0, 10).forEach(conv => {
const item = document.createElement('div');
item.className = `history-item ${conv.id === sessionId ? 'active' : ''}`;
const time = new Date(conv.updated || conv.created);
const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}${time.toLocaleDateString('sq-AL')}`;
item.innerHTML = `
<div class="hi-title">${conv.title || 'Sesion i ri'}</div>
<div class="hi-time">${timeStr}</div>
`;
item.addEventListener('click', () => loadConversation(conv));
list.appendChild(item);
});
}
function loadConversation(conv) {
sessionId = conv.id;
localStorage.setItem('shp_session_id', sessionId);
chatBox.innerHTML = '';
conv.messages.forEach(msg => {
const div = document.createElement('div');
div.className = `message ${msg.role === 'user' ? 'user' : 'bot'}`;
const avatarSvg = msg.role === 'user'
? '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'
: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>';
const t = new Date(msg.time);
const ts = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
div.innerHTML = `
<div class="msg-avatar">${avatarSvg}</div>
<div class="msg-body">
<div class="msg-meta">
<span class="msg-sender">${msg.role === 'user' ? 'OFICER' : 'KIA'}</span>
<span class="msg-time">${ts}</span>
</div>
<div class="msg-content"><span class="msg-text">${msg.role === 'user' ? escapeHtml(msg.content) : renderMarkdown(msg.content)}</span></div>
</div>
`;
chatBox.appendChild(div);
});
chatBox.scrollTop = chatBox.scrollHeight;
switchView('chat');
renderHistoryList();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// New session
document.getElementById('new-session-btn')?.addEventListener('click', () => {
sessionId = '';
localStorage.removeItem('shp_session_id');
const roleConfig = ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor;
chatBox.innerHTML = `
<div class="message bot">
<div class="msg-avatar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</div>
<div class="msg-body">
<div class="msg-meta">
<span class="msg-sender">KIA</span>
<span class="msg-time">SYSTEM READY</span>
<span class="msg-classification class-${roleConfig.color}">${roleConfig.classification}</span>
</div>
<div class="msg-content"><span class="msg-text">Sesion i ri u krijua. Pres urdhrat tuaja, ${roleConfig.greeting}.</span></div>
</div>
</div>
`;
const suggestions = document.getElementById('suggestions');
if (suggestions) suggestions.style.display = 'flex';
loadSuggestions();
switchView('chat');
renderHistoryList();
showToast("Sesion i ri u krijua", "info", 2000);
});
// =====================================================================
// DOCUMENT LIBRARY
// =====================================================================
function getDocuments() {
try {
return JSON.parse(localStorage.getItem('shp_documents') || '[]');
} catch { return []; }
}
function addDocumentToLibrary(filename, text) {
const docs = getDocuments();
const ext = filename.split('.').pop().toLowerCase();
const iconMap = {
'pdf': '📕', 'docx': '📘', 'xlsx': '📗',
'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️'
};
docs.unshift({
id: Date.now().toString(),
name: filename,
icon: iconMap[ext] || '📄',
text: text,
date: new Date().toISOString(),
chars: text.length,
});
if (docs.length > 50) docs.length = 50;
localStorage.setItem('shp_documents', JSON.stringify(docs));
renderDocuments();
}
function renderDocuments() {
const grid = document.getElementById('documents-grid');
const empty = document.getElementById('docs-empty');
if (!grid) return;
const docs = getDocuments();
grid.querySelectorAll('.doc-card').forEach(c => c.remove());
if (docs.length === 0) {
if (empty) empty.style.display = 'flex';
return;
}
if (empty) empty.style.display = 'none';
docs.forEach(doc => {
const card = document.createElement('div');
card.className = 'doc-card';
const date = new Date(doc.date);
const dateStr = date.toLocaleDateString('sq-AL') + ' ' +
String(date.getHours()).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0');
card.innerHTML = `
<div class="doc-icon">${doc.icon}</div>
<div class="doc-name">${doc.name}</div>
<div class="doc-meta">${dateStr}${doc.chars} karaktere</div>
<div class="doc-preview">${doc.text.substring(0, 150)}...</div>
`;
card.addEventListener('click', () => {
scannedTextCache = doc.text;
switchView('chat');
setTimeout(() => {
appendMessage(`📄 Dokumenti "${doc.name}" u ngarkua nga arkiva. Mund të bëni pyetje mbi përmbajtjen.`, false);
}, 200);
});
grid.insertBefore(card, empty);
});
}
// =====================================================================
// INTERACTIVE MAP (LEAFLET)
// =====================================================================
function initMap() {
if (mapInstance || !document.getElementById('leaflet-map')) return;
try {
mapInstance = L.map('leaflet-map', {
zoomControl: true,
attributionControl: false,
}).setView([41.3275, 19.8187], 7);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 18,
}).addTo(mapInstance);
const createIcon = (color) => L.divIcon({
html: `<div style="width:12px;height:12px;border-radius:50%;background:${color};border:2px solid rgba(255,255,255,0.4);box-shadow:0 0 8px ${color}"></div>`,
className: '',
iconSize: [12, 12],
iconAnchor: [6, 6],
});
const hqIcon = createIcon('#d4a017');
const baseIcon = createIcon('#3b82f6');
const missionIcon = createIcon('#22c55e');
const trainingIcon = createIcon('#a855f7');
const industryIcon = createIcon('#f97316');
// Layer Groups
const hqLayer = L.layerGroup();
const baseLayer = L.layerGroup();
const missionLayer = L.layerGroup();
const trainingLayer = L.layerGroup();
const industryLayer = L.layerGroup();
const locations = [
// Headquarters
{ name: "Shtabi i Përgjithshëm", desc: "Qendra komanduese, Tiranë", lat: 41.3275, lng: 19.8187, icon: hqIcon, layer: hqLayer },
{ name: "Ministria e Mbrojtjes", desc: "Bulevardi Dëshmorët e Kombit, Tiranë", lat: 41.3246, lng: 19.8163, icon: hqIcon, layer: hqLayer },
// Military Bases
{ name: "Baza Ajrore Kuçovë", desc: "Baza e NATO-s dhe Dronëve TB2", lat: 40.8014, lng: 19.9060, icon: baseIcon, layer: baseLayer },
{ name: "Baza Detare Vlorë", desc: "Baza e Forcës Detare Pashaliman", lat: 40.4607, lng: 19.4833, icon: baseIcon, layer: baseLayer },
{ name: "Porto Romano", desc: "Baza e re Detare (në ndërtim)", lat: 41.3653, lng: 19.4447, icon: baseIcon, layer: baseLayer },
{ name: "Garnizoni Shkodër", desc: "Garnizoni verior", lat: 42.0682, lng: 19.5126, icon: baseIcon, layer: baseLayer },
{ name: "Garnizoni Korçë", desc: "Garnizoni juglindor", lat: 40.6186, lng: 20.7808, icon: baseIcon, layer: baseLayer },
// International Missions
{ name: "KFOR - Kosovë", desc: "KFOR Komanda e Batalionit Rezervë", lat: 42.6629, lng: 21.1655, icon: missionIcon, layer: missionLayer },
{ name: "EUFOR Althea", desc: "Trupat Paqeruajtëse", lat: 43.8563, lng: 18.4131, icon: missionIcon, layer: missionLayer },
{ name: "NATO eFP - Letoni", desc: "Grupi Luftarak Adazi", lat: 57.0768, lng: 24.3315, icon: missionIcon, layer: missionLayer },
{ name: "NATO eVP - Bullgari", desc: "Prezenca ushtarake Novo Selo", lat: 42.7483, lng: 26.3117, icon: missionIcon, layer: missionLayer },
{ name: "Misioni EUTM Mali", desc: "EU Training Mission", lat: 12.6392, lng: -8.0029, icon: missionIcon, layer: missionLayer },
// Training & Education
{ name: "AFA", desc: "Akademia e Forcave të Armatosura", lat: 41.3375, lng: 19.7887, icon: trainingIcon, layer: trainingLayer },
{ name: "Qendra e Stërvitjes Bizë", desc: "Poligoni Ndërkombëtar NATO", lat: 41.3364, lng: 20.1506, icon: trainingIcon, layer: trainingLayer },
{ name: "Qendra Trajnimit Zall-Herr", desc: "Regjimenti i Operacioneve Speciale (ROS)", lat: 41.4119, lng: 19.8622, icon: trainingIcon, layer: trainingLayer },
// Industrial Centers (KAYO)
{ name: "KAYO Rubik", desc: "Prodhim armësh të lehta", lat: 41.7686, lng: 19.7850, icon: industryIcon, layer: industryLayer },
{ name: "KAYO Poliçan", desc: "Prodhim municioni", lat: 40.6150, lng: 20.0981, icon: industryIcon, layer: industryLayer },
{ name: "KAYO Gramsh", desc: "Komponentë ushtarakë", lat: 40.8661, lng: 20.1833, icon: industryIcon, layer: industryLayer },
{ name: "KAYO Shkozet", desc: "Hub i ri industrial (Bashkëpunime)", lat: 41.3289, lng: 19.4883, icon: industryIcon, layer: industryLayer },
];
locations.forEach(loc => {
L.marker([loc.lat, loc.lng], { icon: loc.icon })
.bindPopup(`<div class="map-popup-custom"><h4>${loc.name}</h4><p>${loc.desc}</p></div>`)
.addTo(loc.layer);
});
// Seismic Threat Heatmap (Simulated fault lines & recent activity areas)
const heatPoints = [
[41.48, 19.47, 0.8], [41.32, 19.45, 0.9], [41.35, 19.55, 0.6], // Durres area
[41.60, 19.65, 0.5], [41.52, 19.48, 0.7],
[40.71, 19.98, 0.6], [40.65, 20.05, 0.4], [40.75, 20.10, 0.8] // Berat-Gramsh area
];
// Ensure L.heatLayer exists (from CDN) before adding
let heatLayer;
if (typeof L.heatLayer !== 'undefined') {
heatLayer = L.heatLayer(heatPoints, {radius: 45, blur: 25, maxZoom: 10, gradient: {0.4: 'yellow', 0.65: 'orange', 1: 'red'}});
} else {
heatLayer = L.layerGroup();
}
// Add all groups to map by default
hqLayer.addTo(mapInstance);
baseLayer.addTo(mapInstance);
missionLayer.addTo(mapInstance);
trainingLayer.addTo(mapInstance);
industryLayer.addTo(mapInstance);
// Layer Control Configuration
const overlays = {
"Shtabet Komanduese": hqLayer,
"Bazat Ushtarake": baseLayer,
"Misionet NATO": missionLayer,
"Qendrat Stërvitore": trainingLayer,
"Zonat Industriale": industryLayer,
"🗺️ Hartëzim i Kërcënimeve (Sizmik)": heatLayer
};
L.control.layers(null, overlays, {collapsed: true, position: 'topright'}).addTo(mapInstance);
setTimeout(() => mapInstance.invalidateSize(), 200);
} catch (e) {
console.error("Map init error:", e);
}
}
// =====================================================================
// ORG CHART INTERACTIVITY
// =====================================================================
document.querySelectorAll('.org-node[data-query]').forEach(node => {
node.addEventListener('click', () => {
const query = node.dataset.query;
switchView('chat');
setTimeout(() => sendMessage(query), 200);
});
});
// =====================================================================
// PDF EXPORT
// =====================================================================
document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
const messages = chatBox.querySelectorAll('.message');
if (messages.length <= 1) {
showToast('Nuk ka bisedë për eksportim', 'warning');
return;
}
const reportTime = getTimeStr();
const logoUrl = `${window.location.origin}/afa_logo.png`;
const roleConfig = ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor;
let reportHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>KIA — Raport i Inteligjencës</title>
<style>
@page { margin: 20mm; size: A4; }
body {
font-family: 'Times New Roman', Times, serif;
margin: 0; padding: 0; color: #111; line-height: 1.6;
background: #fff;
}
.classification {
text-align: center; font-family: 'Arial', sans-serif;
font-size: 1rem; font-weight: 900; color: #d32f2f;
letter-spacing: 6px; text-transform: uppercase;
border-top: 3px solid #d32f2f; border-bottom: 3px solid #d32f2f;
padding: 8px; margin-bottom: 35px;
}
.header-wrapper {
display: flex; align-items: center; justify-content: center;
border-bottom: 4px double #000; padding-bottom: 25px; margin-bottom: 25px;
}
.logo { width: 100px; margin-right: 25px; filter: grayscale(100%); }
.header-text { text-align: center; }
.header-text h1 {
font-size: 1.8rem; color: #000; margin: 5px 0;
text-transform: uppercase; letter-spacing: 1.5px;
}
.header-text .subtitle { font-size: 1rem; color: #333; font-weight: bold; }
.doc-info {
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
border: 2px solid #000; padding: 15px; margin-bottom: 40px;
font-family: 'Arial', sans-serif; font-size: 0.85rem; background: #fdfdfd;
}
.doc-info-item strong { display: inline-block; width: 120px; color: #444; }
.report-body { font-size: 1.05rem; }
.exchange { margin: 25px 0; padding: 15px 20px; border-left: 4px solid #ccc; page-break-inside: auto; }
.exchange.bot { border-left-color: #0f172a; background-color: #f8fafc; border-right: 1px solid #e2e8f0; border-top: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0; }
.exchange.user { background: #fff; border-left-color: #64748b; }
.exchange .sender {
font-family: 'Arial', sans-serif; font-weight: 800; font-size: 0.85rem;
color: #0f172a; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1.5px;
display: flex; justify-content: space-between; border-bottom: 1px solid #e2e8f0; padding-bottom: 5px;
}
.exchange.user .sender { color: #475569; }
.exchange .time { font-weight: normal; font-size: 0.75rem; color: #64748b; }
/* Markdown Elements Styling */
.exchange .content h1, .exchange .content h2, .exchange .content h3 { font-family: 'Arial', sans-serif; margin-top: 20px; text-transform: uppercase; font-size: 1.1rem; }
.exchange .content p { margin: 10px 0; text-align: justify; }
.exchange .content ul { margin: 10px 0; padding-left: 25px; }
.exchange .content li { margin-bottom: 5px; }
.exchange .content blockquote { border-left: 3px solid #d32f2f; margin: 10px 0; padding-left: 15px; font-style: italic; color: #444; }
.footer {
margin-top: 60px; padding-top: 20px; border-top: 2px solid #000;
font-size: 0.8rem; font-family: 'Arial', sans-serif; color: #333;
text-align: center; page-break-inside: avoid;
}
.footer .warning { font-weight: bold; color: #d32f2f; margin-bottom: 5px; text-transform: uppercase; }
@media print {
.classification { color: #000; border-color: #000; }
.footer .warning { color: #000; }
.exchange.bot { background-color: #f1f1f1 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>
</head>
<body>
<div class="classification">${roleConfig.classification}</div>
<div class="header-wrapper">
<img src="${logoUrl}" class="logo" alt="Stema" />
<div class="header-text">
<div class="subtitle">REPUBLIKA E SHQIPËRISË</div>
<div class="subtitle">MINISTRIA E MBROJTJES • SHTABI I PËRGJITHSHËM</div>
<h1 style="margin-top: 15px;">RAPORT I INTELIGJENCËS (KIA)</h1>
<div class="subtitle" style="font-weight: normal; font-size: 0.85rem; margin-top: 5px;">SISTEMI C4ISR • KOMANDA E INTELIGJENCËS ARTIFICIALE</div>
</div>
</div>
<div class="doc-info">
<div class="doc-info-item"><strong>DATA:</strong> ${new Date().toLocaleDateString('sq-AL')}</div>
<div class="doc-info-item"><strong>REFERENCA:</strong> KIA-REF-${Math.floor(Math.random() * 90000) + 10000}</div>
<div class="doc-info-item"><strong>ORA:</strong> ${reportTime}</div>
<div class="doc-info-item"><strong>NIVELI:</strong> ${roleConfig.classification}</div>
<div class="doc-info-item"><strong>SESIONI:</strong> ${sessionId ? sessionId.substring(0, 12).toUpperCase() : 'N/A'}</div>
<div class="doc-info-item"><strong>GJENERUAR NGA:</strong> ${currentRole.toUpperCase() || 'OFICER'}</div>
</div>
<div class="report-body">
`;
messages.forEach(msg => {
const isUser = msg.classList.contains('user');
const sender = isUser ? 'OFICER' : 'KIA';
const time = msg.querySelector('.msg-time')?.textContent || '';
const content = msg.querySelector('.msg-text')?.innerHTML || msg.querySelector('.msg-content')?.innerHTML || '';
reportHtml += `
<div class="exchange ${isUser ? 'user' : 'bot'}">
<div class="sender">${sender} <span class="time">${time}</span></div>
<div class="content">${content}</div>
</div>
`;
});
reportHtml += `
</div> <!-- End report-body -->
<div class="footer">
<div class="warning">KUJDES — DOKUMENT I KLASIFIKUAR</div>
<p>Riprodhimi, shpërndarja ose mbajtja e paautorizuar e këtij dokumenti përbën vepër penale sipas Kodit Ushtarak të RSH.<br>
Gjeneruar automatikisht nga KIA Command Center • ${new Date().toISOString()}</p>
<strong>${roleConfig.classification}</strong>
</div>
<script>window.onload = () => { setTimeout(() => window.print(), 500); }<\/script>
</body>
</html>
`;
const blob = new Blob([reportHtml], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.onbeforeunload = () => URL.revokeObjectURL(url);
}
showToast("📄 Raporti u gjenerua", "success", 2000);
});
// =====================================================================
// DASHBOARD ANIMATIONS & SPARKLINES
// =====================================================================
function animateValue(el, end, duration = 1200) {
if (!el) return;
const start = 0;
let startTime = null;
const step = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = Math.min((timestamp - startTime) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic
el.textContent = Math.floor(eased * (end - start) + start);
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
function drawSparkline(canvasId, data, color = '#3b82f6') {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
if (data.length < 2) return;
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
data.forEach((val, i) => {
const x = (i / (data.length - 1)) * w;
const y = h - ((val - min) / range) * (h - 4) - 2;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
// Fill area under
ctx.lineTo(w, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
ctx.fill();
}
function updateDashboardData(data) {
const uptimeEl = document.getElementById('dash-uptime');
if (uptimeEl) uptimeEl.textContent = data.uptime_human || '--';
// Tool status
const toolsEl = document.getElementById('dash-tools');
if (toolsEl && data.tools) {
toolsEl.textContent = `${data.tools.real_api_tools} / ${data.tools.total_tools}`;
}
// Intel panel
const intelRag = document.getElementById('intel-rag');
const intelVector = document.getElementById('intel-vector');
const intelUptime = document.getElementById('intel-uptime');
if (intelRag) intelRag.textContent = `${data.rag?.total_items || '?'} dokumente`;
if (intelVector) intelVector.textContent = data.rag?.vector_search ? 'AKTIV' : 'JO';
if (intelUptime) intelUptime.textContent = data.uptime_human || '--';
// Capability indicators
const caps = {
'cap-rag': true,
'cap-stt': data.modules?.stt,
'cap-tts': data.modules?.tts,
'cap-ocr': data.modules?.ocr,
'cap-vec': data.rag?.vector_search,
'cap-multi': true,
};
Object.entries(caps).forEach(([id, active]) => {
const el = document.getElementById(id);
if (el) el.classList.toggle('active', !!active);
});
}
// Live Widget Updates
async function updateLiveWidgets() {
// Exchange Rate
try {
const res = await fetch(`${API_BASE}/exchange`);
const data = await res.json();
const excStr = data.data;
const match = excStr.match(/1 EUR = ([\d.]+) LEK/);
const excEl = document.getElementById('dash-exchange');
if (match && excEl) {
excEl.textContent = `${match[1]} LEK`;
excEl.style.color = '#eab308'; // Gold
}
} catch(e) {}
// Weather
try {
const res = await fetch(`${API_BASE}/weather?location=kuçovë`);
const data = await res.json();
const wStr = data.data;
const tempMatch = wStr.match(/Temperatura: ([\d.-]+)/);
const statMatch = wStr.match(/Statusi Ajror:.*?([A-ZÇË\s]+)(?=—|-)/);
const wEl = document.getElementById('dash-weather');
const stEl = document.getElementById('dash-weather-status');
if (tempMatch && wEl) {
wEl.textContent = `${tempMatch[1]} °C`;
wEl.style.color = '#38bdf8'; // Sky blue
}
if (statMatch && stEl) {
stEl.textContent = statMatch[1].trim();
// Status colors
if (wStr.includes('KUQE')) stEl.style.color = '#ef4444';
else if (wStr.includes('VERDHË')) stEl.style.color = '#f59e0b';
else stEl.style.color = '#22c55e';
}
} catch(e) {}
}
function renderDynamicWidget(type, data, container) {
if (!data) return;
const widgetBox = document.createElement('div');
widgetBox.className = 'agentic-widget';
if (type === 'weather') {
const flightStatClass = data.status.includes('GJELBËR') ? 'green' : (data.status.includes('KUQE') ? 'red' : 'yellow');
widgetBox.innerHTML = `
<div class="widget-header">
<span class="widget-icon">☁️</span>
<span class="widget-title">Meteorologjia Taktike — ${data.location}</span>
</div>
<div class="widget-body grid-2">
<div class="w-metric"><span class="w-label">Temp</span><span class="w-val">${data.temp}°C</span></div>
<div class="w-metric"><span class="w-label">Era</span><span class="w-val">${data.wind} km/h ${data.wind_dir}</span></div>
<div class="w-metric"><span class="w-label">Lagështia</span><span class="w-val">${data.humidity}%</span></div>
<div class="w-metric"><span class="w-label">Reshje</span><span class="w-val">${data.precip} mm</span></div>
</div>
${data.hourly_temps ? `<div style="margin-top: 10px;"><canvas id="chart-${data.location.replace(/\s+/g, '')}" height="60"></canvas></div>` : ''}
<div class="widget-footer status-${flightStatClass}">
Akses Ajror: ${data.status}
</div>
`;
container.appendChild(widgetBox);
// Render Chart.js if data exists
if (data.hourly_temps && typeof Chart !== 'undefined') {
const ctx = document.getElementById(`chart-${data.location.replace(/\s+/g, '')}`).getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: Array.from({length: data.hourly_temps.length}, (_, i) => `${i}h`),
datasets: [{
label: 'Temperatura °C',
data: data.hourly_temps,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
pointRadius: 0,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { display: false },
y: { display: false, min: Math.min(...data.hourly_temps) - 2 }
}
}
});
}
return; // Early return to bypass default append under `type === weather`
} else if (type === 'exchange') {
widgetBox.innerHTML = `
<div class="widget-header">
<span class="widget-icon">💹</span>
<span class="widget-title">Tregjet Financiare (LEK)</span>
</div>
<div class="widget-body grid-4">
<div class="w-metric"><span class="w-label">EUR</span><span class="w-val">${data.eur}</span></div>
<div class="w-metric"><span class="w-label">USD</span><span class="w-val">${data.usd}</span></div>
<div class="w-metric"><span class="w-label">GBP</span><span class="w-val">${data.gbp}</span></div>
<div class="w-metric"><span class="w-label">TRY</span><span class="w-val">${data.try}</span></div>
</div>
`;
}
container.appendChild(widgetBox);
}
// =====================================================================
// HEALTH / DASHBOARD STATUS
// =====================================================================
function setLoadingState(el, isLoading) {
if (!el) return;
if (isLoading) el.classList.add('loading');
else el.classList.remove('loading');
}
async function checkHealth() {
const banner = document.getElementById('system-banner');
setLoadingState(document.getElementById('dash-uptime'), true);
setLoadingState(document.getElementById('dash-docs'), true);
setLoadingState(document.getElementById('dash-model'), true);
setLoadingState(document.getElementById('dash-sessions'), true);
try {
const res = await fetch(`${API_BASE}/health`);
const data = await res.json();
if (banner) banner.style.display = 'none';
updateDashboardData(data);
} catch (e) {
if (banner) {
banner.style.display = 'block';
banner.querySelector('span').textContent = "Lidhja me Qendrën e Inteligjencës dështoi. Po riprovojmë...";
}
} finally {
setLoadingState(document.getElementById('dash-uptime'), false);
}
}
// =====================================================================
// SITREP GENERATOR (Full Intelligence Briefing)
// =====================================================================
async function generateSitrep() {
switchView('chat');
appendMessage('/sitrep — Gjenerimi i Raportit Ditor', true);
showThinking();
showProcessingStatus();
try {
const shpToken = localStorage.getItem('shp_token') || '';
const res = await fetch(`${API_BASE}/sitrep`, {
headers: { 'Authorization': `Bearer ${shpToken}` }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
removeThinking();
hideProcessingStatus();
const sitrep = await res.json();
// Build formatted SITREP message
let sitrepText = `**📋 RAPORTI DITOR I INTELIGJENCËS (SITREP)**\n`;
sitrepText += `**Klasifikimi:** ${sitrep.classification}\n`;
sitrepText += `**Gjeneruar:** ${new Date(sitrep.generated_at).toLocaleString('sq-AL')}\n\n`;
sitrepText += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
for (const [key, section] of Object.entries(sitrep.sections)) {
sitrepText += `**[${section.title}]**\n`;
if (typeof section.data === 'object' && !Array.isArray(section.data)) {
for (const [loc, report] of Object.entries(section.data)) {
if (report) {
sitrepText += `\n• **${loc}:**\n`;
// Extract key lines from the report
const lines = report.split('\n').filter(l => l.trim() && !l.includes('━'));
lines.slice(0, 6).forEach(line => {
sitrepText += ` ${line.trim()}\n`;
});
}
}
} else if (section.data) {
const lines = section.data.split('\n').filter(l => l.trim() && !l.includes('━'));
lines.slice(0, 8).forEach(line => {
sitrepText += `${line.trim()}\n`;
});
}
sitrepText += '\n';
}
sitrepText += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
sitrepText += `_Ky raport u gjenerua automatikisht nga KIA Command Center._`;
appendMessage(sitrepText, false);
showToast('📋 SITREP u gjenerua me sukses', 'success');
} catch (err) {
removeThinking();
hideProcessingStatus();
console.error('SITREP error:', err);
appendMessage('❌ Gjenerimi i SITREP dështoi. Provoni përsëri.', false);
showToast('SITREP gjenerimi dështoi', 'error');
}
}
// =====================================================================
// CHAT SEARCH (Search through conversation history)
// =====================================================================
function initChatSearch() {
const historySection = document.getElementById('history-section');
if (!historySection) return;
// Create search box
const searchBox = document.createElement('div');
searchBox.className = 'history-search';
searchBox.innerHTML = `
<input type="text" id="history-search-input" placeholder="🔍 Kërko në biseda..." autocomplete="off" />
`;
const headerEl = historySection.querySelector('.history-header');
if (headerEl) {
headerEl.after(searchBox);
}
const searchInput = document.getElementById('history-search-input');
searchInput?.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
filterHistoryList(query);
});
}
function filterHistoryList(query) {
const list = document.getElementById('history-list');
if (!list) return;
if (!query) {
renderHistoryList();
return;
}
const conversations = getConversations();
list.innerHTML = '';
const filtered = conversations.filter(conv => {
if (conv.title?.toLowerCase().includes(query)) return true;
return conv.messages?.some(msg =>
msg.content?.toLowerCase().includes(query)
);
});
if (filtered.length === 0) {
list.innerHTML = `<div class="history-empty">Asnjë rezultat për "${query}"</div>`;
return;
}
filtered.forEach(conv => {
const item = document.createElement('div');
item.className = `history-item ${conv.id === sessionId ? 'active' : ''}`;
const time = new Date(conv.updated || conv.created);
const timeStr = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
// Find matching message snippet
let snippet = '';
if (query) {
const matchMsg = conv.messages?.find(m => m.content?.toLowerCase().includes(query));
if (matchMsg) {
const idx = matchMsg.content.toLowerCase().indexOf(query);
const start = Math.max(0, idx - 20);
snippet = '...' + matchMsg.content.substring(start, start + 60) + '...';
}
}
item.innerHTML = `
<div class="hi-title">${conv.title || 'Sesion i ri'}</div>
${snippet ? `<div class="hi-snippet">${snippet}</div>` : ''}
<div class="hi-time">${timeStr}</div>
`;
item.addEventListener('click', () => loadConversation(conv));
list.appendChild(item);
});
}
// =====================================================================
// ANALYTICS DISPLAY (Dashboard metrics)
// =====================================================================
async function loadAnalytics() {
try {
const shpToken = localStorage.getItem('shp_token') || '';
const res = await fetch(`${API_BASE}/analytics`, {
headers: { 'Authorization': `Bearer ${shpToken}` }
});
if (!res.ok) return;
const data = await res.json();
// Update dashboard metrics
const queriesEl = document.getElementById('dash-queries');
if (queriesEl) queriesEl.textContent = data.total_queries || 0;
const queries24El = document.getElementById('dash-queries-24h');
if (queries24El) queries24El.textContent = data.queries_24h || 0;
const latencyEl = document.getElementById('dash-avg-latency');
if (latencyEl) latencyEl.textContent = `${data.avg_latency_ms || 0}ms`;
// Feedback stats
const fbRes = await fetch(`${API_BASE}/feedback/stats`, {
headers: { 'Authorization': `Bearer ${shpToken}` }
});
if (fbRes.ok) {
const fb = await fbRes.json();
const satEl = document.getElementById('dash-satisfaction');
if (satEl) satEl.textContent = `${fb.satisfaction_rate}%`;
}
} catch (e) {
// Silent fail — analytics are non-critical
}
}
// =====================================================================
// INITIALIZATION
// =====================================================================
// Start with role selection
initRoleScreen();
// Load persistent data
loadSuggestions();
renderHistoryList();
renderDocuments();
initChatSearch();
// Periodic checks
setInterval(checkHealth, 60000);
setInterval(updateLiveWidgets, 300000);
setTimeout(updateLiveWidgets, 1500);
setTimeout(loadAnalytics, 3000);
// Default routing
if (!currentRole) {
} else if (sessionId) {
switchView('chat');
} else {
switchView('dashboard');
}
// =====================================================================
// THEME TOGGLE
// =====================================================================
if (themeBtn) {
// Load preference
if (localStorage.getItem('shp_theme') === 'light') {
document.body.classList.add('light-theme');
}
themeBtn.addEventListener('click', () => {
document.body.classList.toggle('light-theme');
if (document.body.classList.contains('light-theme')) {
localStorage.setItem('shp_theme', 'light');
showToast("Modaliteti i Dritës aktivizua", "info", 2000);
} else {
localStorage.setItem('shp_theme', 'dark');
showToast("Modaliteti i Errësirës aktivizua", "info", 2000);
}
});
}
// =====================================================================
// DOCX EXPORT
// =====================================================================
document.getElementById('export-docx-btn')?.addEventListener('click', async () => {
const messages = chatBox.querySelectorAll('.message');
if (messages.length <= 1) {
showToast('Nuk ka bisedë për eksportim', 'warning');
return;
}
let chatText = "";
messages.forEach(msg => {
const isUser = msg.classList.contains('user');
const sender = isUser ? 'OFICER' : 'KIA';
let content = msg.querySelector('.msg-text')?.innerText || msg.querySelector('.msg-content')?.innerText || '';
chatText += `${sender}: ${content}\n\n`;
});
try {
showToast("⏳ Duke gjeneruar dokumentin DOCX...", "info");
const shpToken = localStorage.getItem('shp_token') || '';
const roleConfig = ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor;
const res = await fetch(`${API_BASE}/export/docx`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${shpToken}`
},
body: JSON.stringify({
title: "RAPORT I INTELIGJENCËS",
content: chatText.trim(),
classification: roleConfig.classification
})
});
if (!res.ok) throw new Error("Export failed");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `KIA_Raport_${new Date().getTime()}.docx`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast("✅ Dokumenti u eksportua me sukses", "success");
} catch(err) {
console.error("DOCX Export error:", err);
showToast("Gabim gjatë eksportimit të dokumentit", "error");
}
});