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 = `
${icons[type] || icons.info}
${message}
`;
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, '$1');
text = text.replace(/__(.*?)__/g, '$1');
text = text.replace(/(?$1');
text = text.replace(/\[(VLERËSIMI I SITUATËS|ANALIZA INTELIGJENTE|ANALIZA KRAHASUESE|REKOMANDIMI)\]/g,
'
');
text = text.replace(/^[•\-]\s+(.+)$/gm, '• $1
');
text = text.replace(/^(\d+)\.\s+(.+)$/gm, '$1. $2
');
text = text.replace(/\n\n/g, '
');
text = text.replace(/\n/g, '
');
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 = `
`;
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 = '';
const roleConfig = ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor;
const classTag = `${roleConfig.classification}`;
div.innerHTML = `
${avatarSvg}
KIA
${getTimeStr()}
${classTag}
`;
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 = ` ${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 = `${c.icon} ${c.text}`;
}
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 = '';
div.innerHTML = `
${avatarSvg}
${(ROLE_CONFIG[currentRole] || ROLE_CONFIG.visitor).name.toUpperCase()}
${getTimeStr()}
`;
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 = 'Pyetje ngjashme:';
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 = ` ${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 = `${c.icon} ${c.text}`;
}
} 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 = 'Pyetni për dokumentin:';
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 = `
${conv.title || 'Sesion i ri'}
${timeStr}
`;
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'
? ''
: '';
const t = new Date(msg.time);
const ts = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
div.innerHTML = `
${avatarSvg}
${msg.role === 'user' ? 'OFICER' : 'KIA'}
${ts}
${msg.role === 'user' ? escapeHtml(msg.content) : renderMarkdown(msg.content)}
`;
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 = `
KIA
SYSTEM READY
${roleConfig.classification}
Sesion i ri u krijua. Pres urdhrat tuaja, ${roleConfig.greeting}.
`;
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 = `
${doc.icon}
${doc.name}
${dateStr} • ${doc.chars} karaktere
${doc.text.substring(0, 150)}...
`;
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: ``,
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(``)
.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 = `
KIA — Raport i Inteligjencës
${roleConfig.classification}
DATA: ${new Date().toLocaleDateString('sq-AL')}
REFERENCA: KIA-REF-${Math.floor(Math.random() * 90000) + 10000}
ORA: ${reportTime}
NIVELI: ${roleConfig.classification}
SESIONI: ${sessionId ? sessionId.substring(0, 12).toUpperCase() : 'N/A'}
GJENERUAR NGA: ${currentRole.toUpperCase() || 'OFICER'}
`;
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 += `
${sender} ${time}
${content}
`;
});
reportHtml += `