/** * AGC Document Search - Main Application * Handles application state management and UI interactions */ // Application State const appState = { isInitializing: true, isOnline: navigator.onLine, apiStatus: "connecting", // Search State searchQuery: "", searchResults: null, searchHistory: loadFromStorage("searchHistory") || [], // Documents State documents: [], documentTypes: ["All Types"], selectedDocument: null, bookmarks: loadFromStorage("bookmarks") || [], // UI State activeTab: "all-docs", viewMode: loadFromStorage("viewMode") || "grid", // Loading States loading: { search: false, documents: false, }, // AI Chat aiChat: { messages: loadFromStorage("chatMessages") || [], isTyping: false, }, }; // DOM Elements const elements = { // Safely get elements with a fallback getElement: function (id) { const element = document.getElementById(id); if (!element) { console.warn(`Element with ID '${id}' not found in the DOM`); } return element; }, get searchInput() { return this.getElement("mainSearchInput"); }, get searchButton() { return this.getElement("mainSearchButton"); }, get searchResultsSection() { return this.getElement("resultsSection"); }, get searchResultsContainer() { return this.getElement("searchResultsContainer"); }, get recentDocumentsContainer() { return this.getElement("recentDocumentsContainer"); }, get chatMessages() { return this.getElement("chatMessages"); }, get chatInput() { return this.getElement("chatInput"); }, get sendMessageButton() { return this.getElement("chatSendBtn"); }, get mainContent() { return this.getElement("mainContent"); }, // For elements that use querySelectorAll get tabButtons() { return document.querySelectorAll(".tab") || []; }, // Other elements get mobileMenuButton() { return this.getElement("mobileMenuToggle"); }, get mobileMenu() { return this.getElement("mobileMenu"); }, // Will be created dynamically documentViewerModal: null, }; // Utility Functions function formatDate(dateString) { if (!dateString) return "N/A"; return new Date(dateString).toLocaleDateString("en-MY", { year: "numeric", month: "short", day: "numeric", }); } function truncateText(text, maxLength = 100) { if (!text || text.length <= maxLength) return text; return text.substring(0, maxLength) + "..."; } function debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function loadFromStorage(key) { try { const data = localStorage.getItem(`agc_${key}`); return data ? JSON.parse(data) : null; } catch (error) { console.error(`Error loading ${key} from storage:`, error); return null; } } function saveToStorage(key, data) { try { localStorage.setItem(`agc_${key}`, JSON.stringify(data)); return true; } catch (error) { console.error(`Error saving ${key} to storage:`, error); return false; } } // Search Functions async function performSearch(query, docType = "All Types") { if (!query.trim()) return; // Update UI to loading state appState.loading.search = true; updateLoadingState(); try { // Perform search via API const results = await apiService.searchDocuments(query); // Update application state appState.searchResults = results; // Add to search history if (!appState.searchHistory.some((item) => item.query === query)) { appState.searchHistory.unshift({ query, timestamp: new Date().toISOString(), count: results.documents.length, }); // Keep last 20 searches if (appState.searchHistory.length > 20) { appState.searchHistory = appState.searchHistory.slice(0, 20); } saveToStorage("searchHistory", appState.searchHistory); } // Update UI with results displaySearchResults(results); } catch (error) { console.error("Search failed:", error); elements.searchResultsContainer.innerHTML = `

Sorry, the search failed. Please try again.

${error.message}

`; } finally { appState.loading.search = false; updateLoadingState(); // Show results section if hidden elements.searchResultsSection.classList.remove("hidden"); } } function displaySearchResults(results) { const { documents, query, enhanced_query, answer } = results; if (!documents || documents.length === 0) { elements.searchResultsContainer.innerHTML = `

No results found for "${query}"

Try using different keywords or browse all documents.

`; return; } let resultsHTML = ""; // Add AI answer if available if (answer) { resultsHTML += `

AI Analysis

${answer}

`; } // Enhanced query display if (enhanced_query && enhanced_query !== query) { resultsHTML += `

Showing results for: ${enhanced_query}

Original search: ${query}

`; } // Results count resultsHTML += `

Found ${documents.length} result${ documents.length !== 1 ? "s" : "" }

`; // Document results resultsHTML += '
'; documents.forEach((doc) => { const docTitle = doc.title || "Untitled Document"; const docContent = doc.content || "No content available"; const docType = doc.doc_type || "Unknown"; const docDate = formatDate(doc.created_at); const isBookmarked = appState.bookmarks.includes(doc.id); resultsHTML += `
${docType}

${docTitle}

${truncateText(docContent, 200)}

`; }); resultsHTML += "
"; elements.searchResultsContainer.innerHTML = resultsHTML; } // Document Functions async function loadDocuments(docType = "All Types") { appState.loading.documents = true; updateLoadingState(); try { // Load documents based on document type const filters = { doc_type: docType !== "All Types" ? docType : undefined, }; const documents = await apiService.getDocuments(filters); appState.documents = documents; // Display documents in recent documents section displayRecentDocuments(documents.slice(0, 6)); // Show first 6 documents } catch (error) { console.error("Failed to load documents:", error); // Check if the container exists before attempting to update it const container = document.getElementById("recentDocumentsContainer"); if (container) { container.innerHTML = `

Sorry, we couldn't load the documents. Please try again.

${error.message}

`; } // Show error notification showNotification( "error", "Failed to load documents. Please check the console for details." ); } finally { appState.loading.documents = false; updateLoadingState(); } } function displayRecentDocuments(documents) { // Check if the container element exists const recentDocumentsContainer = document.getElementById( "recentDocumentsContainer" ); if (!recentDocumentsContainer) { console.warn("Recent documents container not found in the DOM"); return; } if (!documents || documents.length === 0) { recentDocumentsContainer.innerHTML = `

No recent documents found.

`; return; } let documentsHTML = '
'; documents.forEach((doc) => { const docTitle = doc.title || "Untitled Document"; const docContent = doc.content || "No content available"; const docType = doc.doc_type || "Unknown"; const docDate = formatDate(doc.created_at); const isBookmarked = appState.bookmarks.includes(doc.id); documentsHTML += `
${docType}

${docTitle}

${truncateText(docContent, 100)}

`; }); documentsHTML += "
"; recentDocumentsContainer.innerHTML = documentsHTML; } async function loadDocumentTypes() { try { const response = await apiService.getDocumentTypes(); if (response && response.document_types) { // Add "All Types" as the first option appState.documentTypes = ["All Types", ...response.document_types]; // Update UI elements that use document types (e.g., filter dropdowns) updateDocumentTypeSelectors(); } } catch (error) { console.error("Failed to load document types:", error); } } function updateDocumentTypeSelectors() { // For future implementation - update any dropdowns or filters with document types } // View document details window.viewDocument = async function (documentId) { try { console.log(`Viewing document with ID: ${documentId}`); showLoader(); // Convert any numeric IDs to our document format if (typeof documentId === "number" || !isNaN(parseInt(documentId, 10))) { console.log(`Converting numeric ID ${documentId} to string format`); // If it's a number between 1-6, convert to our doc format const numId = parseInt(documentId, 10); if (numId >= 1 && numId <= 6) { documentId = `doc${numId}`; console.log(`Converted to: ${documentId}`); } } // Fetch document from API const document = await apiService.getDocument(documentId); // Extract legal information from content const extractedData = extractLegalInfo(document.content); // Create modal if it doesn't exist let modal = window.document.querySelector(".document-modal"); if (!modal) { modal = window.document.createElement("div"); modal.className = "document-modal"; modal.innerHTML = `

`; window.document.body.appendChild(modal); // Add event listeners const closeButtons = modal.querySelectorAll(".document-modal-close"); closeButtons.forEach((button) => { button.addEventListener("click", () => { modal.classList.add("hidden"); }); }); modal .querySelector(".document-modal-overlay") .addEventListener("click", () => { modal.classList.add("hidden"); }); } // Update modal title modal.querySelector(".document-modal-title").textContent = document.title || `Document ${documentId}`; // Build the modal content with structured information let modalContent = `
Type: ${ document.doc_type || "Unknown" }
Date: ${formatDate( document.created_at )}
${ document.source ? `
Source: ${document.source}
` : "" }
`; // Add extracted information if available modalContent += generateDocumentSummary(extractedData); // Add full content section modalContent += `

Full Document Content

${
            document.content || "No content available"
          }
`; // Update modal body modal.querySelector(".document-modal-body").innerHTML = modalContent; // Add styles for new elements if not already present if (!window.document.querySelector("style#document-details-styles")) { const detailsStyles = window.document.createElement("style"); detailsStyles.id = "document-details-styles"; detailsStyles.textContent = ` .section-title { margin: 0 0 0.75rem 0; font-size: 1.1rem; color: var(--primary-dark); display: flex; align-items: center; gap: 0.5rem; } .full-content-section { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--gray-light); } .full-content-container { background-color: #f8f9fa; border-radius: var(--radius); border: 1px solid var(--gray-light); padding: 1rem; max-height: 400px; overflow-y: auto; } .full-content-text { font-family: monospace; font-size: 0.9rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; margin: 0; } .document-summary { margin: 1rem 0; } .info-section { margin-bottom: 1rem; padding: 1rem; background-color: #f8f9fa; border-radius: var(--radius); border-left: 3px solid var(--primary); } .info-section h4 { margin: 0 0 0.75rem 0; color: var(--primary-dark); font-size: 1rem; display: flex; align-items: center; gap: 0.5rem; } .info-section p { margin: 0.5rem 0; font-size: 0.9rem; } .acts-list, .persons-list { margin: 0.5rem 0; padding-left: 0; list-style: none; } .act-item, .person-item { background-color: rgba(59, 130, 246, 0.1); margin: 0.25rem 0; padding: 0.5rem 0.75rem; border-radius: 0.25rem; font-size: 0.9rem; border-left: 3px solid var(--primary); } .person-item { border-left-color: var(--warning); background-color: rgba(245, 158, 11, 0.1); } .person-name { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.35rem; } .person-role, .person-category { font-size: 0.85rem; margin-top: 0.25rem; } .role-label, .category-label { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 500; } .role-label { background-color: var(--primary); color: white; } .category-label { background-color: var(--gray-light); color: var(--gray-dark); } .allegation-item { margin: 0.75rem 0; padding: 0.75rem; background-color: rgba(239, 68, 68, 0.05); border-radius: 0.25rem; border-left: 3px solid var(--danger); } .allegation-preview { margin: 0.5rem 0 0 0; font-size: 0.9rem; color: var(--gray); font-style: italic; } .status-ongoing { color: var(--primary); font-weight: 600; } .status-selesai, .status-completed { color: var(--success); font-weight: 600; } .status-pending { color: var(--warning); font-weight: 600; } `; window.document.head.appendChild(detailsStyles); } // Show modal modal.classList.remove("hidden"); } catch (error) { console.error("Error loading document:", error); showNotification( "error", `Failed to load document details: ${error.message}` ); } finally { hideLoader(); } }; function hideDocumentViewer() { if (elements.documentViewerModal) { elements.documentViewerModal.classList.add("hidden"); } } function toggleBookmark(documentId) { if (!documentId) return; const bookmarks = [...appState.bookmarks]; const index = bookmarks.indexOf(documentId); if (index === -1) { // Add bookmark bookmarks.push(documentId); } else { // Remove bookmark bookmarks.splice(index, 1); } // Update state and storage appState.bookmarks = bookmarks; saveToStorage("bookmarks", bookmarks); // Update UI document .querySelectorAll(`.bookmark-button[onclick*="${documentId}"]`) .forEach((button) => { if (index === -1) { button.classList.add("bookmarked"); } else { button.classList.remove("bookmarked"); } }); } // Tab Navigation function switchTab(tabId) { // Update active tab state appState.activeTab = tabId; // Get all tab links and remove active class const mainTabLinks = document.querySelectorAll(".main-tab-link"); mainTabLinks.forEach((t) => t.classList.remove("active")); // Add active class to the corresponding tab const activeTab = document.querySelector(`.main-tab-link[href="#${tabId}"]`); if (activeTab) { activeTab.classList.add("active"); } // Hide all tab content document.querySelectorAll(".tab-content").forEach((content) => { content.classList.remove("active"); }); // Show the selected tab content const tabContent = document.getElementById(tabId); if (tabContent) { tabContent.classList.add("active"); } // Load tab-specific content if (tabId === "browse") { loadDocuments("All Types"); } else if (tabId === "search") { // Any search-specific initialization } else if (tabId === "chat") { // Any chat-specific initialization } } // AI Chat Functions function sendChatMessage() { const message = elements.chatInput.value.trim(); if (!message) return; // Add user message to UI addChatMessage(message, "user"); // Clear input elements.chatInput.value = ""; // Set typing indicator appState.aiChat.isTyping = true; updateChatTypingIndicator(); // Show API connection error message setTimeout(() => { const aiResponse = "The AI assistant feature is currently unavailable. Please ensure the AI service is properly configured and connected."; // Add error response to UI addChatMessage(aiResponse, "system"); // Clear typing indicator appState.aiChat.isTyping = false; updateChatTypingIndicator(); }, 1000); } function addChatMessage(message, role) { // Add to state appState.aiChat.messages.push({ role, content: message, timestamp: new Date().toISOString(), }); // Save to storage (limited to last 50 messages) if (appState.aiChat.messages.length > 50) { appState.aiChat.messages = appState.aiChat.messages.slice(-50); } saveToStorage("chatMessages", appState.aiChat.messages); // Add to UI const chatMessageElement = document.createElement("div"); if (role === "user") { chatMessageElement.innerHTML = `

${message}

`; } else if (role === "system") { chatMessageElement.innerHTML = `

${message}

`; } else { chatMessageElement.innerHTML = `

${message}

`; } elements.chatMessages.appendChild(chatMessageElement); // Scroll to bottom elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; } function updateChatTypingIndicator() { // Remove existing typing indicator if any const existingIndicator = elements.chatMessages.querySelector(".typing-indicator"); if (existingIndicator) { existingIndicator.remove(); } if (appState.aiChat.isTyping) { // Add typing indicator const indicatorElement = document.createElement("div"); indicatorElement.className = "typing-indicator"; indicatorElement.innerHTML = `
`; elements.chatMessages.appendChild(indicatorElement); elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; } } // UI Helper Functions function updateLoadingState() { // Update loading indicators based on appState.loading const searchButton = document.getElementById("mainSearchButton"); if (searchButton) { if (appState.loading.search) { searchButton.innerHTML = ''; searchButton.disabled = true; } else { searchButton.innerHTML = ''; searchButton.disabled = false; } } // Other loading indicators can be updated here } function showLoadingOverlay() { let overlay = document.querySelector(".loading-overlay"); if (!overlay) { overlay = document.createElement("div"); overlay.className = "loading-overlay"; overlay.innerHTML = `
`; document.body.appendChild(overlay); } overlay.classList.remove("hidden"); } function hideLoadingOverlay() { const overlay = document.querySelector(".loading-overlay"); if (overlay) { overlay.classList.add("hidden"); } } // Initialize Application async function initializeApp() { try { showLoadingOverlay(); // Check API connection (blocking) const isConnected = await apiService.checkConnection(); appState.apiStatus = isConnected ? "connected" : "disconnected"; console.log(`API Status: ${appState.apiStatus}`); if (!isConnected) { hideLoadingOverlay(); showNotification( "error", "Cannot connect to API server. Please ensure the backend service is running." ); // Check if mainContent element exists const mainContent = document.getElementById("mainContent"); if (mainContent) { mainContent.innerHTML = `

API Connection Error

The application could not connect to the backend API server.

Please make sure:

  • The backend server is running (uvicorn api:app --reload)
  • The API is accessible at ${apiService.baseUrl}
`; } return; } try { // Load document types await loadDocumentTypes(); } catch (error) { console.error("Failed to load document types:", error); showNotification( "warning", "Could not load document types. Some filtering options may be unavailable." ); } try { // Load initial documents await loadDocuments(); } catch (error) { console.error("Failed to load documents:", error); showNotification( "error", "Failed to load documents. Please check the console for details." ); } // Setup event listeners setupEventListeners(); // Load chat history loadChatHistory(); } catch (error) { console.error("Failed to initialize application:", error); hideLoadingOverlay(); showNotification( "error", "Application initialization failed. Please check the console for details." ); // Show error on main content const mainContent = document.getElementById("mainContent"); if (mainContent) { mainContent.innerHTML = `

Application Error

Something went wrong while initializing the application.

Error details:

${error.message || "Unknown error"}
`; } } finally { // Mark initialization as complete appState.isInitializing = false; hideLoadingOverlay(); } } function setupEventListeners() { // Search if (elements.searchButton) { elements.searchButton.addEventListener("click", () => { if (elements.searchInput) { performSearch(elements.searchInput.value); } }); } if (elements.searchInput) { elements.searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { performSearch(elements.searchInput.value); } }); } // Tab navigation - fix for the main tabs const mainTabLinks = document.querySelectorAll(".main-tab-link"); mainTabLinks.forEach((tab) => { tab.addEventListener("click", (e) => { e.preventDefault(); // Prevent default anchor behavior // Remove active class from all tabs mainTabLinks.forEach((t) => t.classList.remove("active")); // Add active class to clicked tab tab.classList.add("active"); // Get the tab ID from the href attribute const tabId = tab.getAttribute("href").substring(1); // Remove # from href // Hide all tab content document.querySelectorAll(".tab-content").forEach((content) => { content.classList.remove("active"); }); // Show the selected tab content const tabContent = document.getElementById(tabId); if (tabContent) { tabContent.classList.add("active"); } }); }); // Mobile menu tab navigation const mobileMenuLinks = document.querySelectorAll(".mobile-menu a"); mobileMenuLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const tabId = link.getAttribute("href").substring(1); // Hide mobile menu after selection const mobileMenu = document.getElementById("mobileMenu"); if (mobileMenu) mobileMenu.classList.remove("active"); // Remove active class from all main tabs mainTabLinks.forEach((t) => t.classList.remove("active")); // Add active to the corresponding main tab const correspondingTab = document.querySelector( `.main-tab-link[href="#${tabId}"]` ); if (correspondingTab) correspondingTab.classList.add("active"); // Hide all tab content document.querySelectorAll(".tab-content").forEach((content) => { content.classList.remove("active"); }); // Show the selected tab content const tabContent = document.getElementById(tabId); if (tabContent) { tabContent.classList.add("active"); } }); }); // Mobile menu toggle if (elements.mobileMenuButton && elements.mobileMenu) { elements.mobileMenuButton.addEventListener("click", () => { elements.mobileMenu.classList.toggle("active"); }); } // Chat if (elements.sendMessageButton) { elements.sendMessageButton.addEventListener("click", sendChatMessage); } if (elements.chatInput) { elements.chatInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { sendChatMessage(); } }); } } function loadChatHistory() { // Display existing chat messages appState.aiChat.messages.forEach((message) => { addChatMessage(message.content, message.role); }); } // Export functions for global access (needed for onclick handlers) window.toggleBookmark = toggleBookmark; // Initialize the application when DOM is loaded document.addEventListener("DOMContentLoaded", initializeApp); // Utility functions for document extraction function extractLegalInfo(content) { if (!content) return {}; const info = { fileNumber: extractField(content, "File Number"), status: extractField(content, "Status"), caseNumber: extractField(content, "Case Number"), dppSuggestion: extractField(content, "DPP Suggestion"), hodDecision: extractField(content, "HOD Decision"), allegations: extractAllegations(content), persons: extractPersons(content), acts: extractActs(content), }; return info; } // Helper function to extract field values function extractField(content, fieldName) { const regex = new RegExp(`${fieldName}:\\s*([^\\n]+)`, "i"); const match = content.match(regex); return match ? match[1].trim() : null; } // Extract allegations information function extractAllegations(content) { const allegations = []; const allegationPattern = /ALLEGATION #(\d+):([\s\S]*?)(?=ALLEGATION #|\n--- |$)/g; let match; while ((match = allegationPattern.exec(content)) !== null) { allegations.push({ number: match[1], details: match[2].trim().substring(0, 200) + "...", }); } return allegations; } // Extract person information function extractPersons(content) { const persons = []; // Try to find JSON-formatted person data const jsonPersonPattern = /\{"peranan":\s*"([^"]+)",\s*"category":\s*"([^"]+)",\s*"namaPihak":\s*"([^"]+)"\}/g; let jsonMatch; while ((jsonMatch = jsonPersonPattern.exec(content)) !== null) { try { persons.push({ name: jsonMatch[3], role: jsonMatch[1], category: jsonMatch[2], }); } catch (e) { console.error("Error processing person data:", e); } } // If no JSON data found, try the old format as fallback if (persons.length === 0) { const personPattern = /Person ID: (\d+)[\s\S]*?namaPerayuResponden['":]?\s*['":]([^'"]+)['"]/g; let match; while ((match = personPattern.exec(content)) !== null) { persons.push({ id: match[1], name: match[2].trim(), role: "Unknown", category: "Unknown", }); } } return persons; } // Extract legal acts and sections function extractActs(content) { const acts = []; const actPattern = /(Akta [^\\n]+)|(Section [^\\n]+)|(Seksyen [^\\n]+)/gi; let match; while ((match = actPattern.exec(content)) !== null) { const act = match[0].trim(); if (act && !acts.includes(act)) { acts.push(act); } } return acts.slice(0, 3); // Limit to 3 acts } // Generate document summary based on extracted data function generateDocumentSummary(data) { let summary = '
'; // Case information if (data.caseNumber || data.fileNumber) { summary += '
'; summary += '

Case Information:

'; if (data.caseNumber) summary += `

Case: ${data.caseNumber}

`; if (data.fileNumber) summary += `

File: ${data.fileNumber}

`; if (data.status) summary += `

Status: ${ data.status }

`; summary += "
"; } // Legal acts if (data.acts && data.acts.length > 0) { summary += '
'; summary += '

Legal References:

'; summary += '
"; } // Persons involved if (data.persons && data.persons.length > 0) { summary += '
'; summary += '

Persons Involved:

'; summary += '
"; } // Allegations count if (data.allegations && data.allegations.length > 0) { summary += '
'; summary += `

Allegations: (${data.allegations.length})

`; data.allegations.forEach((allegation) => { summary += `
Allegation ${allegation.number}:

${allegation.details}

`; }); summary += "
"; } summary += "
"; return summary; } // Show notification function showNotification(type, message) { // Create notification element if it doesn't exist let notification = document.querySelector(".notification"); if (notification) { // Remove existing notification first notification.remove(); } // Create new notification notification = document.createElement("div"); notification.className = `notification ${type}`; let icon; switch (type) { case "success": icon = "check-circle"; break; case "error": icon = "exclamation-circle"; break; case "warning": icon = "exclamation-triangle"; break; default: icon = "info-circle"; } notification.innerHTML = `
${message}
`; document.body.appendChild(notification); // Auto-remove after 5 seconds setTimeout(() => { notification.classList.add("notification-hide"); setTimeout(() => { if (notification && notification.parentNode) { notification.remove(); } }, 300); }, 5000); } // Show loading indicator function showLoader() { showLoadingOverlay(); } // Hide loading indicator function hideLoader() { hideLoadingOverlay(); }