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, '
◆ $1
'); 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 = `
KIA Duke procesuar...
`; 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(`

${loc.name}

${loc.desc}

`) .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}
REPUBLIKA E SHQIPËRISË
MINISTRIA E MBROJTJES • SHTABI I PËRGJITHSHËM

RAPORT I INTELIGJENCËS (KIA)

SISTEMI C4ISR • KOMANDA E INTELIGJENCËS ARTIFICIALE
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 += `