1821 lines
52 KiB
JavaScript
1821 lines
52 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 "";
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Helper function for relevancy scoring
|
|
function getRelevancyInfo(similarity) {
|
|
const score = similarity || Math.random() * 0.6 + 0.4; // Default random score if not provided
|
|
const percent = Math.round(score * 100);
|
|
|
|
let className = "low-relevance";
|
|
if (score >= 0.8) {
|
|
className = "high-relevance";
|
|
} else if (score >= 0.6) {
|
|
className = "medium-relevance";
|
|
}
|
|
|
|
return {
|
|
score,
|
|
percent,
|
|
className,
|
|
display: score.toFixed(3),
|
|
};
|
|
}
|
|
|
|
// Search Functions
|
|
async function performSearch(query, docType = "All Types") {
|
|
if (!query.trim()) return;
|
|
|
|
// Switch to search tab to show results
|
|
switchTab("search");
|
|
|
|
// Update UI to loading state
|
|
appState.loading.search = true;
|
|
updateLoadingState();
|
|
|
|
// Show query enhancement
|
|
displayQueryEnhancement(query);
|
|
|
|
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 (only when on search tab)
|
|
if (appState.activeTab === "search") {
|
|
elements.searchResultsSection.classList.add("visible");
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// Get relevancy information
|
|
const relevancy = getRelevancyInfo(doc.similarity);
|
|
|
|
resultsHTML += `
|
|
<div class="result-card" data-id="${doc.id}">
|
|
<div class="result-header-with-score">
|
|
<div class="result-title-section">
|
|
<div class="doc-type-badge ${docType.toLowerCase()}">${docType}</div>
|
|
<h3 class="result-title">${docTitle}</h3>
|
|
</div>
|
|
<div class="relevance-score">
|
|
<span class="score-label">Relevance:</span>
|
|
<span class="score-value">${relevancy.display}</span>
|
|
<div class="score-bar">
|
|
<div class="score-fill" style="width: ${
|
|
relevancy.percent
|
|
}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="result-content">
|
|
<p>${truncateText(docContent, 200)}</p>
|
|
</div>
|
|
<div class="result-footer">
|
|
<div class="result-meta">
|
|
${docDate ? `<span class="result-date">${docDate}</span>` : ""}
|
|
<span class="relevance-badge ${relevancy.className}">${
|
|
relevancy.percent
|
|
}% match</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;
|
|
}
|
|
|
|
// Query Enhancement Display
|
|
function displayQueryEnhancement(originalQuery) {
|
|
const enhancementDiv = document.getElementById("queryEnhancement");
|
|
const originalQuerySpan = document.getElementById("originalQuery");
|
|
const enhancedQuerySpan = document.getElementById("enhancedQuery");
|
|
const keywordsDiv = document.getElementById("extractedKeywords");
|
|
|
|
if (!enhancementDiv) return;
|
|
|
|
// Simulate AI query enhancement (in real implementation, this would come from API)
|
|
const enhancedQuery = generateEnhancedQuery(originalQuery);
|
|
const keywords = extractKeywords(originalQuery);
|
|
|
|
originalQuerySpan.textContent = originalQuery;
|
|
enhancedQuerySpan.textContent = enhancedQuery;
|
|
|
|
// Display keywords
|
|
keywordsDiv.innerHTML = "";
|
|
keywords.forEach((keyword) => {
|
|
const keywordSpan = document.createElement("span");
|
|
keywordSpan.className = "query-keyword";
|
|
keywordSpan.textContent = keyword;
|
|
keywordsDiv.appendChild(keywordSpan);
|
|
});
|
|
|
|
// Show the enhancement panel
|
|
enhancementDiv.classList.add("visible");
|
|
|
|
// Auto-hide after 5 seconds
|
|
setTimeout(() => {
|
|
enhancementDiv.classList.remove("visible");
|
|
}, 5000);
|
|
}
|
|
|
|
// Generate enhanced query (simulated AI enhancement)
|
|
function generateEnhancedQuery(query) {
|
|
const legalTerms = {
|
|
fraud:
|
|
"financial fraud OR criminal misrepresentation OR deceptive practices",
|
|
investigation:
|
|
"criminal investigation OR forensic examination OR evidence gathering",
|
|
"cross-border":
|
|
"international jurisdiction OR transnational crime OR mutual legal assistance",
|
|
"money laundering":
|
|
"anti-money laundering OR financial crime OR proceeds of crime",
|
|
corruption: "public corruption OR bribery OR abuse of office",
|
|
contract:
|
|
"contractual dispute OR breach of contract OR commercial agreement",
|
|
};
|
|
|
|
let enhanced = query.toLowerCase();
|
|
|
|
Object.keys(legalTerms).forEach((term) => {
|
|
if (enhanced.includes(term)) {
|
|
enhanced = enhanced.replace(new RegExp(term, "gi"), legalTerms[term]);
|
|
}
|
|
});
|
|
|
|
return enhanced;
|
|
}
|
|
|
|
// Extract keywords from query
|
|
function extractKeywords(query) {
|
|
const stopWords = [
|
|
"the",
|
|
"a",
|
|
"an",
|
|
"and",
|
|
"or",
|
|
"but",
|
|
"in",
|
|
"on",
|
|
"at",
|
|
"to",
|
|
"for",
|
|
"of",
|
|
"with",
|
|
"by",
|
|
"is",
|
|
"are",
|
|
"was",
|
|
"were",
|
|
"be",
|
|
"been",
|
|
"being",
|
|
"have",
|
|
"has",
|
|
"had",
|
|
"do",
|
|
"does",
|
|
"did",
|
|
"will",
|
|
"would",
|
|
"could",
|
|
"should",
|
|
];
|
|
|
|
return query
|
|
.toLowerCase()
|
|
.replace(/[^\w\s]/g, "")
|
|
.split(/\s+/)
|
|
.filter((word) => word.length > 2 && !stopWords.includes(word))
|
|
.slice(0, 8); // Limit to 8 keywords
|
|
}
|
|
|
|
// Advanced Filtering Functions
|
|
function setupAdvancedFilters() {
|
|
const dateFilter = document.getElementById("dateFilter");
|
|
const customDateRange = document.getElementById("customDateRange");
|
|
const customDateRangeTo = document.getElementById("customDateRangeTo");
|
|
const applyFiltersBtn = document.getElementById("applyFiltersBtn");
|
|
const resetFiltersBtn = document.getElementById("resetFiltersBtn");
|
|
const saveFiltersBtn = document.getElementById("saveFiltersBtn");
|
|
|
|
// Handle custom date range visibility
|
|
if (dateFilter) {
|
|
dateFilter.addEventListener("change", (e) => {
|
|
if (e.target.value === "custom") {
|
|
customDateRange.style.display = "block";
|
|
customDateRangeTo.style.display = "block";
|
|
} else {
|
|
customDateRange.style.display = "none";
|
|
customDateRangeTo.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
|
|
// Apply filters
|
|
if (applyFiltersBtn) {
|
|
applyFiltersBtn.addEventListener("click", applyAdvancedFilters);
|
|
}
|
|
|
|
// Reset filters
|
|
if (resetFiltersBtn) {
|
|
resetFiltersBtn.addEventListener("click", resetAllFilters);
|
|
}
|
|
|
|
// Save filter preset
|
|
if (saveFiltersBtn) {
|
|
saveFiltersBtn.addEventListener("click", saveFilterPreset);
|
|
}
|
|
}
|
|
|
|
function applyAdvancedFilters() {
|
|
const filters = {
|
|
docType: document.getElementById("docTypeFilter")?.value || "All Types",
|
|
dateRange: document.getElementById("dateFilter")?.value || "all",
|
|
title: document.getElementById("titleFilter")?.value || "",
|
|
jurisdiction: document.getElementById("jurisdictionFilter")?.value || "all",
|
|
legalArea: document.getElementById("legalAreaFilter")?.value || "all",
|
|
startDate: document.getElementById("startDate")?.value || "",
|
|
endDate: document.getElementById("endDate")?.value || "",
|
|
};
|
|
|
|
console.log("Applying advanced filters:", filters);
|
|
|
|
// Apply filters to current document view
|
|
loadDocuments(filters.docType, filters);
|
|
}
|
|
|
|
function resetAllFilters() {
|
|
document.getElementById("docTypeFilter").value = "All Types";
|
|
document.getElementById("dateFilter").value = "all";
|
|
document.getElementById("titleFilter").value = "";
|
|
document.getElementById("jurisdictionFilter").value = "all";
|
|
document.getElementById("legalAreaFilter").value = "all";
|
|
document.getElementById("startDate").value = "";
|
|
document.getElementById("endDate").value = "";
|
|
|
|
// Hide custom date range
|
|
document.getElementById("customDateRange").style.display = "none";
|
|
document.getElementById("customDateRangeTo").style.display = "none";
|
|
|
|
// Reload documents without filters
|
|
loadDocuments("All Types");
|
|
}
|
|
|
|
function saveFilterPreset() {
|
|
const filters = {
|
|
docType: document.getElementById("docTypeFilter")?.value || "All Types",
|
|
dateRange: document.getElementById("dateFilter")?.value || "all",
|
|
title: document.getElementById("titleFilter")?.value || "",
|
|
jurisdiction: document.getElementById("jurisdictionFilter")?.value || "all",
|
|
legalArea: document.getElementById("legalAreaFilter")?.value || "all",
|
|
};
|
|
|
|
const presetName = prompt("Enter a name for this filter preset:");
|
|
if (presetName) {
|
|
const savedPresets = loadFromStorage("filterPresets") || {};
|
|
savedPresets[presetName] = filters;
|
|
saveToStorage("filterPresets", savedPresets);
|
|
showNotification(
|
|
"success",
|
|
`Filter preset "${presetName}" saved successfully!`
|
|
);
|
|
}
|
|
}
|
|
|
|
// AI Assistant Enhanced Functions
|
|
function setupAIAssistantTools() {
|
|
const analyzeDocBtn = document.getElementById("analyzeDocBtn");
|
|
const summarizeBtn = document.getElementById("summarizeBtn");
|
|
const legalConceptsBtn = document.getElementById("legalConceptsBtn");
|
|
const crossReferenceBtn = document.getElementById("crossReferenceBtn");
|
|
const precedentSearchBtn = document.getElementById("precedentSearchBtn");
|
|
const caseAnalysisBtn = document.getElementById("caseAnalysisBtn");
|
|
const closeAnalysisPanel = document.getElementById("closeAnalysisPanel");
|
|
|
|
if (analyzeDocBtn) {
|
|
analyzeDocBtn.addEventListener("click", () => analyzeDocument());
|
|
}
|
|
if (summarizeBtn) {
|
|
summarizeBtn.addEventListener("click", () => generateSummary());
|
|
}
|
|
if (legalConceptsBtn) {
|
|
legalConceptsBtn.addEventListener("click", () => explainLegalConcepts());
|
|
}
|
|
if (crossReferenceBtn) {
|
|
crossReferenceBtn.addEventListener("click", () => findCrossReferences());
|
|
}
|
|
if (precedentSearchBtn) {
|
|
precedentSearchBtn.addEventListener("click", () => searchPrecedents());
|
|
}
|
|
if (caseAnalysisBtn) {
|
|
caseAnalysisBtn.addEventListener("click", () => performCaseAnalysis());
|
|
}
|
|
if (closeAnalysisPanel) {
|
|
closeAnalysisPanel.addEventListener("click", () => hideAnalysisPanel());
|
|
}
|
|
}
|
|
|
|
function analyzeDocument() {
|
|
const panel = document.getElementById("documentAnalysisPanel");
|
|
const content = document.getElementById("analysisContent");
|
|
|
|
if (!appState.selectedDocument) {
|
|
showNotification("warning", "Please select a document to analyze first.");
|
|
return;
|
|
}
|
|
|
|
// Simulate document analysis
|
|
content.innerHTML = `
|
|
<h4>Document Analysis: ${appState.selectedDocument.title}</h4>
|
|
<div style="margin: 1rem 0;">
|
|
<strong>Document Type:</strong> ${appState.selectedDocument.doc_type}<br>
|
|
<strong>Complexity Level:</strong> High<br>
|
|
<strong>Legal Areas:</strong> Criminal Law, International Law<br>
|
|
<strong>Key Entities:</strong>
|
|
<span class="legal-concept-highlight">Prosecution<span class="concept-tooltip">The legal party pursuing the case</span></span>,
|
|
<span class="legal-concept-highlight">Defense<span class="concept-tooltip">The legal party defending against charges</span></span>,
|
|
<span class="legal-concept-highlight">Court of Appeal<span class="concept-tooltip">Higher court reviewing lower court decisions</span></span>
|
|
</div>
|
|
<div style="margin: 1rem 0;">
|
|
<strong>Summary:</strong> This document contains detailed proceedings of a complex financial fraud case involving international jurisdictions...
|
|
</div>
|
|
`;
|
|
|
|
panel.classList.add("visible");
|
|
}
|
|
|
|
function generateSummary() {
|
|
addChatMessage(
|
|
"Generate a comprehensive summary of the most recent legal document.",
|
|
"user"
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const summaryMessage = `Based on the document analysis, here's a comprehensive summary:
|
|
|
|
**Key Points:**
|
|
• Primary legal issues involve cross-border financial fraud
|
|
• Multiple jurisdictions are involved (Malaysia, Singapore, UK)
|
|
• Evidence collection spans 18 months
|
|
• 7 defendants across 3 countries
|
|
|
|
**Legal Precedents:**
|
|
• Similar cases: Wong v. State (2019), International Fraud Case No. 245/2020
|
|
• Applicable laws: AMLA 2001, Financial Services Act 2013
|
|
|
|
**Outcome:**
|
|
• 5 convictions, 2 acquittals
|
|
• Total restitution: RM 45.2 million
|
|
• Appeal pending in Federal Court`;
|
|
|
|
addChatMessage(summaryMessage, "assistant");
|
|
}, 1500);
|
|
}
|
|
|
|
function explainLegalConcepts() {
|
|
addChatMessage(
|
|
"Explain the key legal concepts mentioned in recent conversations.",
|
|
"user"
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const conceptsMessage = `Here are explanations for key legal concepts:
|
|
|
|
**MLATs (Mutual Legal Assistance Treaties):**
|
|
International agreements between countries to facilitate cooperation in criminal investigations and prosecutions.
|
|
|
|
**Jurisdictional Issues:**
|
|
Questions about which court or legal system has the authority to hear a particular case, especially important in cross-border crimes.
|
|
|
|
**Admissibility of Evidence:**
|
|
The legal standard determining whether evidence can be presented in court proceedings, considering factors like how it was obtained and its relevance.
|
|
|
|
**Cross-Border Financial Fraud:**
|
|
Financial crimes that span multiple countries, requiring international cooperation for investigation and prosecution.`;
|
|
|
|
addChatMessage(conceptsMessage, "assistant");
|
|
}, 2000);
|
|
}
|
|
|
|
function findCrossReferences() {
|
|
addChatMessage(
|
|
"Find documents and cases that cross-reference with the current topic.",
|
|
"user"
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const crossRefMessage = `Found several cross-references:
|
|
|
|
**Related Cases:**
|
|
• PP v. Lim Financial Fraud (2021) - Similar MLAT procedures
|
|
• International Banking Fraud Case 156/2020 - Evidence admissibility precedent
|
|
• Cross-Border Money Laundering v. State (2019) - Jurisdictional determination
|
|
|
|
**Referenced Legislation:**
|
|
• Anti-Money Laundering Act 2001
|
|
• Mutual Assistance in Criminal Matters Act 2002
|
|
• Financial Services Act 2013
|
|
|
|
**International Treaties:**
|
|
• UNCAC (UN Convention Against Corruption)
|
|
• ASEAN MLAT Framework
|
|
• Commonwealth Scheme for Mutual Legal Assistance`;
|
|
|
|
addChatMessage(crossRefMessage, "assistant");
|
|
}, 1800);
|
|
}
|
|
|
|
function searchPrecedents() {
|
|
addChatMessage(
|
|
"Search for legal precedents related to the current case or topic.",
|
|
"user"
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const precedentsMessage = `Relevant legal precedents found:
|
|
|
|
**Binding Precedents:**
|
|
1. **Federal Court - Wong Ah Kow v. PP (2020)**
|
|
- Established framework for international evidence collection
|
|
- Relevance: 95% match
|
|
|
|
2. **Court of Appeal - State v. International Banking Corp (2019)**
|
|
- Guidelines for cross-border financial investigations
|
|
- Relevance: 87% match
|
|
|
|
**Persuasive Precedents:**
|
|
1. **High Court - Lee Financial Services v. State (2021)**
|
|
- MLAT application procedures
|
|
- Relevance: 78% match
|
|
|
|
**International Precedents:**
|
|
1. **Singapore High Court - Trans-Asian Fraud Case (2020)**
|
|
- Similar factual matrix
|
|
- Persuasive value in Malaysian courts`;
|
|
|
|
addChatMessage(precedentsMessage, "assistant");
|
|
}, 2200);
|
|
}
|
|
|
|
function performCaseAnalysis() {
|
|
addChatMessage(
|
|
"Perform a detailed legal case analysis of the current matter.",
|
|
"user"
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const analysisMessage = `Comprehensive Case Analysis:
|
|
|
|
**Facts Summary:**
|
|
Complex financial fraud involving multiple jurisdictions, defendants across 3 countries, total loss RM 45.2M.
|
|
|
|
**Legal Issues:**
|
|
1. Jurisdictional competence of Malaysian courts
|
|
2. Admissibility of foreign evidence
|
|
3. Application of MLAT procedures
|
|
4. Cross-border asset recovery
|
|
|
|
**Legal Analysis:**
|
|
- **Jurisdiction:** Established under s. 3 AMLA 2001
|
|
- **Evidence:** Foreign evidence admissible per MACMA 2002
|
|
- **Procedure:** MLAT requests properly executed
|
|
- **Recovery:** Asset forfeiture orders enforceable
|
|
|
|
**Likely Outcome:**
|
|
Strong prosecution case with 85% conviction probability based on similar precedents.
|
|
|
|
**Recommendations:**
|
|
- Ensure all MLAT procedures properly documented
|
|
- Consider plea negotiations for minor defendants
|
|
- Prepare for appellate challenges on jurisdictional grounds`;
|
|
|
|
addChatMessage(analysisMessage, "assistant");
|
|
}, 2500);
|
|
}
|
|
|
|
function hideAnalysisPanel() {
|
|
const panel = document.getElementById("documentAnalysisPanel");
|
|
panel.classList.remove("visible");
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Choose container class based on view mode
|
|
const containerClass =
|
|
appState.viewMode === "list" ? "documents-list" : "documents-grid";
|
|
const cardClass =
|
|
appState.viewMode === "list" ? "document-card list-view" : "document-card";
|
|
|
|
let documentsHTML = `<div class="${containerClass}">`;
|
|
|
|
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="${cardClass}" 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,
|
|
appState.viewMode === "list" ? 150 : 100
|
|
)}</p>
|
|
</div>
|
|
<div class="document-footer">
|
|
<div class="document-meta">
|
|
${docDate ? `<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>
|
|
${
|
|
formatDate(document.created_at)
|
|
? `
|
|
<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");
|
|
}
|
|
|
|
// Handle search results visibility based on active tab
|
|
if (elements.searchResultsSection) {
|
|
if (tabId === "search") {
|
|
// Show search results if they exist and we're on search tab
|
|
if (appState.searchResults) {
|
|
elements.searchResultsSection.classList.add("visible");
|
|
}
|
|
} else {
|
|
// Hide search results when not on search tab
|
|
elements.searchResultsSection.classList.remove("visible");
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
console.log("🚀 Initializing AGC Document Search Application...");
|
|
|
|
// Set initial state
|
|
appState.isInitializing = true;
|
|
|
|
try {
|
|
// Check API connection
|
|
console.log("📡 Checking API connection...");
|
|
const isConnected = await apiService.checkConnection();
|
|
|
|
if (isConnected) {
|
|
appState.apiStatus = "connected";
|
|
console.log("✅ API connection successful");
|
|
|
|
// Load initial data
|
|
await Promise.all([loadDocumentTypes(), loadDocuments()]);
|
|
|
|
// Load chat history
|
|
loadChatHistory();
|
|
|
|
console.log("📚 Initial data loaded successfully");
|
|
} else {
|
|
appState.apiStatus = "disconnected";
|
|
console.warn("⚠️ API connection failed - running in offline mode");
|
|
|
|
// Show error message to user
|
|
showNotification(
|
|
"error",
|
|
"Unable to connect to the server. Some features may be limited."
|
|
);
|
|
|
|
// Load cached data if available
|
|
const cachedDocuments = loadFromStorage("cachedDocuments");
|
|
if (cachedDocuments) {
|
|
appState.documents = cachedDocuments;
|
|
displayRecentDocuments(cachedDocuments);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ Initialization failed:", error);
|
|
appState.apiStatus = "error";
|
|
|
|
showNotification(
|
|
"error",
|
|
"Failed to initialize the application. Please refresh the page."
|
|
);
|
|
} finally {
|
|
appState.isInitializing = false;
|
|
updateLoadingState();
|
|
}
|
|
|
|
// Setup event listeners and UI components
|
|
setupEventListeners();
|
|
setupAdvancedFilters();
|
|
setupAIAssistantTools();
|
|
setupViewToggle();
|
|
|
|
console.log("🎉 Application initialized successfully!");
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// View Toggle Functions
|
|
function setupViewToggle() {
|
|
const gridViewBtn = document.getElementById("gridViewBtn");
|
|
const listViewBtn = document.getElementById("listViewBtn");
|
|
|
|
if (gridViewBtn) {
|
|
gridViewBtn.addEventListener("click", () => switchToGridView());
|
|
}
|
|
if (listViewBtn) {
|
|
listViewBtn.addEventListener("click", () => switchToListView());
|
|
}
|
|
|
|
// Set initial state based on saved preference
|
|
if (appState.viewMode === "list") {
|
|
listViewBtn?.classList.add("active");
|
|
gridViewBtn?.classList.remove("active");
|
|
} else {
|
|
gridViewBtn?.classList.add("active");
|
|
listViewBtn?.classList.remove("active");
|
|
}
|
|
}
|
|
|
|
function switchToGridView() {
|
|
// Update button states
|
|
document.getElementById("gridViewBtn")?.classList.add("active");
|
|
document.getElementById("listViewBtn")?.classList.remove("active");
|
|
|
|
// Update view mode in state
|
|
appState.viewMode = "grid";
|
|
saveToStorage("viewMode", "grid");
|
|
|
|
// Update document container
|
|
updateDocumentView();
|
|
}
|
|
|
|
function switchToListView() {
|
|
// Update button states
|
|
document.getElementById("listViewBtn")?.classList.add("active");
|
|
document.getElementById("gridViewBtn")?.classList.remove("active");
|
|
|
|
// Update view mode in state
|
|
appState.viewMode = "list";
|
|
saveToStorage("viewMode", "list");
|
|
|
|
// Update document container
|
|
updateDocumentView();
|
|
}
|
|
|
|
function updateDocumentView() {
|
|
const container = document.getElementById("recentDocumentsContainer");
|
|
if (!container) return;
|
|
|
|
// Re-render documents with current view mode
|
|
displayRecentDocuments(appState.documents.slice(0, 6));
|
|
}
|