1316 lines
37 KiB
JavaScript
1316 lines
37 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<div class="error-message">
|
|
<p>Sorry, the search failed. Please try again.</p>
|
|
<p class="error-details">${error.message}</p>
|
|
</div>
|
|
`;
|
|
} 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 = `
|
|
<div class="no-results">
|
|
<p>No results found for "${query}"</p>
|
|
<p>Try using different keywords or browse all documents.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let resultsHTML = "";
|
|
|
|
// Add AI answer if available
|
|
if (answer) {
|
|
resultsHTML += `
|
|
<div class="ai-answer">
|
|
<h3>AI Analysis</h3>
|
|
<p>${answer}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Enhanced query display
|
|
if (enhanced_query && enhanced_query !== query) {
|
|
resultsHTML += `
|
|
<div class="enhanced-query">
|
|
<p>Showing results for: <strong>${enhanced_query}</strong></p>
|
|
<p class="original-query">Original search: ${query}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Results count
|
|
resultsHTML += `
|
|
<div class="results-count">
|
|
<p>Found ${documents.length} result${
|
|
documents.length !== 1 ? "s" : ""
|
|
}</p>
|
|
</div>
|
|
`;
|
|
|
|
// Document results
|
|
resultsHTML += '<div class="results-list">';
|
|
|
|
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 += `
|
|
<div class="result-card" data-id="${doc.id}">
|
|
<div class="result-header">
|
|
<div class="doc-type-badge ${docType.toLowerCase()}">${docType}</div>
|
|
<h3 class="result-title">${docTitle}</h3>
|
|
</div>
|
|
<div class="result-content">
|
|
<p>${truncateText(docContent, 200)}</p>
|
|
</div>
|
|
<div class="result-footer">
|
|
<div class="result-meta">
|
|
<span class="result-date">${docDate}</span>
|
|
</div>
|
|
<div class="result-actions">
|
|
<button class="bookmark-button ${isBookmarked ? "bookmarked" : ""}"
|
|
onclick="toggleBookmark('${doc.id}')">
|
|
<i class="fas fa-bookmark"></i>
|
|
</button>
|
|
<button class="view-button" onclick="viewDocument('${doc.id}')">
|
|
View Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
resultsHTML += "</div>";
|
|
|
|
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 = `
|
|
<div class="error-message">
|
|
<p>Sorry, we couldn't load the documents. Please try again.</p>
|
|
<p class="error-details">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="no-documents">
|
|
<p>No recent documents found.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let documentsHTML = '<div class="documents-grid">';
|
|
|
|
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 += `
|
|
<div class="document-card" data-id="${doc.id}">
|
|
<div class="document-header">
|
|
<div class="doc-type-badge ${docType.toLowerCase()}">${docType}</div>
|
|
<h3 class="document-title">${docTitle}</h3>
|
|
</div>
|
|
<div class="document-content">
|
|
<p>${truncateText(docContent, 100)}</p>
|
|
</div>
|
|
<div class="document-footer">
|
|
<div class="document-meta">
|
|
<span class="document-date">${docDate}</span>
|
|
</div>
|
|
<div class="document-actions">
|
|
<button class="bookmark-button ${isBookmarked ? "bookmarked" : ""}"
|
|
onclick="toggleBookmark('${doc.id}')">
|
|
<i class="fas fa-bookmark"></i>
|
|
</button>
|
|
<button class="view-button" onclick="viewDocument('${doc.id}')">
|
|
View
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
documentsHTML += "</div>";
|
|
|
|
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 = `
|
|
<div class="document-modal-overlay"></div>
|
|
<div class="document-modal-container">
|
|
<div class="document-modal-header">
|
|
<h3 class="document-modal-title"></h3>
|
|
<button class="document-modal-close">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="document-modal-body"></div>
|
|
<div class="document-modal-footer">
|
|
<button class="view-button document-modal-close">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="document-meta">
|
|
<div class="document-meta-item">
|
|
<span class="document-meta-label">Type:</span>
|
|
<span class="document-meta-value">${
|
|
document.doc_type || "Unknown"
|
|
}</span>
|
|
</div>
|
|
<div class="document-meta-item">
|
|
<span class="document-meta-label">Date:</span>
|
|
<span class="document-meta-value">${formatDate(
|
|
document.created_at
|
|
)}</span>
|
|
</div>
|
|
${
|
|
document.source
|
|
? `
|
|
<div class="document-meta-item">
|
|
<span class="document-meta-label">Source:</span>
|
|
<span class="document-meta-value">${document.source}</span>
|
|
</div>
|
|
`
|
|
: ""
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
// Add extracted information if available
|
|
modalContent += generateDocumentSummary(extractedData);
|
|
|
|
// Add full content section
|
|
modalContent += `
|
|
<div class="full-content-section">
|
|
<h4 class="section-title"><i class="fas fa-file-alt"></i> Full Document Content</h4>
|
|
<div class="full-content-container">
|
|
<pre class="full-content-text">${
|
|
document.content || "No content available"
|
|
}</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div style="background-color: #1e3a8a; color: white; padding: 0.5rem 1rem; border-radius: 0.5rem; max-width: 80%; margin-left: auto; margin-bottom: 0.5rem;">
|
|
<p style="margin: 0;">${message}</p>
|
|
</div>
|
|
`;
|
|
} else if (role === "system") {
|
|
chatMessageElement.innerHTML = `
|
|
<div style="background-color: #ef4444; color: white; padding: 0.5rem 1rem; border-radius: 0.5rem; max-width: 80%; margin-bottom: 0.5rem;">
|
|
<p style="margin: 0;">${message}</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
chatMessageElement.innerHTML = `
|
|
<div style="background-color: #e5e7eb; padding: 0.5rem 1rem; border-radius: 0.5rem; max-width: 80%; margin-bottom: 0.5rem;">
|
|
<p style="margin: 0;">${message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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 = `
|
|
<div style="background-color: #e5e7eb; padding: 0.5rem 1rem; border-radius: 0.5rem; max-width: 80%; margin-bottom: 0.5rem; display: flex; align-items: center;">
|
|
<div style="width: 4px; height: 4px; background-color: #6b7280; border-radius: 50%; margin-right: 3px; animation: pulse 1s infinite;"></div>
|
|
<div style="width: 4px; height: 4px; background-color: #6b7280; border-radius: 50%; margin-right: 3px; animation: pulse 1s infinite 0.2s;"></div>
|
|
<div style="width: 4px; height: 4px; background-color: #6b7280; border-radius: 50%; animation: pulse 1s infinite 0.4s;"></div>
|
|
</div>
|
|
`;
|
|
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 = '<i class="fas fa-spinner fa-spin"></i>';
|
|
searchButton.disabled = true;
|
|
} else {
|
|
searchButton.innerHTML = '<i class="fas fa-search"></i>';
|
|
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 = `
|
|
<div class="loading-spinner">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
</div>
|
|
`;
|
|
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 = `
|
|
<div class="container">
|
|
<div class="api-error">
|
|
<h2>API Connection Error</h2>
|
|
<p>The application could not connect to the backend API server.</p>
|
|
<div class="error-details">
|
|
<p>Please make sure:</p>
|
|
<ul>
|
|
<li>The backend server is running (uvicorn api:app --reload)</li>
|
|
<li>The API is accessible at ${apiService.baseUrl}</li>
|
|
</ul>
|
|
<button class="view-button" onclick="window.location.reload()">Try Again</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
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 = `
|
|
<div class="container">
|
|
<div class="api-error">
|
|
<h2>Application Error</h2>
|
|
<p>Something went wrong while initializing the application.</p>
|
|
<div class="error-details">
|
|
<p>Error details:</p>
|
|
<pre>${error.message || "Unknown error"}</pre>
|
|
<button class="view-button" onclick="window.location.reload()">Try Again</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
} 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 = '<div class="document-summary">';
|
|
|
|
// Case information
|
|
if (data.caseNumber || data.fileNumber) {
|
|
summary += '<div class="info-section">';
|
|
summary += '<h4><i class="fas fa-info-circle"></i> Case Information:</h4>';
|
|
if (data.caseNumber)
|
|
summary += `<p><strong>Case:</strong> ${data.caseNumber}</p>`;
|
|
if (data.fileNumber)
|
|
summary += `<p><strong>File:</strong> ${data.fileNumber}</p>`;
|
|
if (data.status)
|
|
summary += `<p><strong>Status:</strong> <span class="status-${data.status.toLowerCase()}">${
|
|
data.status
|
|
}</span></p>`;
|
|
summary += "</div>";
|
|
}
|
|
|
|
// Legal acts
|
|
if (data.acts && data.acts.length > 0) {
|
|
summary += '<div class="info-section">';
|
|
summary += '<h4><i class="fas fa-gavel"></i> Legal References:</h4>';
|
|
summary += '<ul class="acts-list">';
|
|
data.acts.forEach((act) => {
|
|
summary += `<li class="act-item">${act}</li>`;
|
|
});
|
|
summary += "</ul></div>";
|
|
}
|
|
|
|
// Persons involved
|
|
if (data.persons && data.persons.length > 0) {
|
|
summary += '<div class="info-section">';
|
|
summary += '<h4><i class="fas fa-users"></i> Persons Involved:</h4>';
|
|
summary += '<ul class="persons-list">';
|
|
data.persons.forEach((person) => {
|
|
summary += `<li class="person-item">
|
|
<div class="person-name">${person.name}</div>
|
|
${
|
|
person.role
|
|
? `<div class="person-role"><span class="role-label">${person.role}</span></div>`
|
|
: ""
|
|
}
|
|
${
|
|
person.category
|
|
? `<div class="person-category"><span class="category-label">${person.category}</span></div>`
|
|
: ""
|
|
}
|
|
</li>`;
|
|
});
|
|
summary += "</ul></div>";
|
|
}
|
|
|
|
// Allegations count
|
|
if (data.allegations && data.allegations.length > 0) {
|
|
summary += '<div class="info-section">';
|
|
summary += `<h4><i class="fas fa-exclamation-triangle"></i> Allegations: (${data.allegations.length})</h4>`;
|
|
data.allegations.forEach((allegation) => {
|
|
summary += `<div class="allegation-item">
|
|
<strong>Allegation ${allegation.number}:</strong>
|
|
<p class="allegation-preview">${allegation.details}</p>
|
|
</div>`;
|
|
});
|
|
summary += "</div>";
|
|
}
|
|
|
|
summary += "</div>";
|
|
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 = `
|
|
<div class="notification-content">
|
|
<i class="fas fa-${icon}"></i>
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
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();
|
|
}
|