Spaces:
Sleeping
Sleeping
| 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"); | |
| } | |
| }); | |