agc-chatbot/templates/old_index.html

1068 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AGC Document Chatbot</title>
<link
rel="icon"
type="image/png"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAA3klEQVRYhe2WQQ6AIAxEX2Jic+xReAJP4BF4DE/gCbyBJ7CJfkQNDJREmoEkpkm3FN60r50WjDFmSTBzznVSyjOABsAGYAOQ/wB0WcDMa2ZeALj/gJTySQhxBrAA6ACsAC4ANsELOOdc57Vundd66j0AzHhfJSJe1kcAhv/UuXvOXgJF4pxz6F/G/rKgUQQzN2VZKgC7EUeQe49SSqW1VkKhDgAAM7VVVb0BgJQSgH5D9hZ6Y9Y0DS3LonrvkVJaO+/Tle31PmBmBnAm5w3LsvjOOWXKILhGAABaa621lpGfWLABWAHsAP49xCwVZJqTdWYAAAAASUVORK5CYII="
/>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.search-container {
margin-bottom: 20px;
}
input[type="text"],
select {
padding: 8px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
button {
padding: 8px 16px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.document-card {
border: 1px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.document-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Document Header */
.document-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.document-title {
margin: 0;
color: #2c3e50;
font-size: 1.1rem;
flex: 1;
}
.document-meta {
display: flex;
gap: 10px;
align-items: center;
}
.doc-type {
background-color: #3498db;
color: white;
padding: 3px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.doc-date {
color: #7f8c8d;
font-size: 0.85rem;
}
/* Document Summary */
.document-summary {
margin-bottom: 15px;
}
.info-section {
margin-bottom: 12px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #4caf50;
}
.info-section h4 {
margin: 0 0 8px 0;
color: #2c3e50;
font-size: 0.95rem;
}
.info-section p {
margin: 4px 0;
font-size: 0.9rem;
}
/* Status styling */
.status-selesai {
color: #27ae60;
font-weight: bold;
}
.status-pending {
color: #f39c12;
font-weight: bold;
}
.status-ongoing {
color: #3498db;
font-weight: bold;
}
/* Lists */
.acts-list,
.persons-list {
margin: 8px 0 0 0;
padding-left: 0;
list-style: none;
}
.act-item,
.person-item {
background-color: #ecf0f1;
margin: 3px 0;
padding: 4px 8px;
border-radius: 3px;
font-size: 0.85rem;
border-left: 3px solid #9b59b6;
}
.person-item {
border-left-color: #e74c3c;
}
/* Allegations */
.allegation-item {
margin: 8px 0;
padding: 8px;
background-color: #fff5f5;
border-radius: 4px;
border-left: 3px solid #e74c3c;
}
.allegation-preview {
margin: 4px 0 0 0;
font-size: 0.85rem;
color: #555;
font-style: italic;
}
/* Search Result Cards */
.search-result-card {
border: 1px solid #ddd;
padding: 18px;
margin: 15px 0;
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.result-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
/* Relevance Score */
.relevance-score {
display: flex;
flex-direction: column;
align-items: flex-end;
min-width: 120px;
}
.score-label {
font-size: 0.8rem;
color: #7f8c8d;
margin-bottom: 2px;
}
.score-value {
font-size: 1.1rem;
font-weight: bold;
color: #2c3e50;
margin-bottom: 4px;
}
.score-bar {
width: 100px;
height: 6px;
background-color: #ecf0f1;
border-radius: 3px;
overflow: hidden;
}
.score-fill {
height: 100%;
background: linear-gradient(
90deg,
#e74c3c 0%,
#f39c12 50%,
#27ae60 100%
);
transition: width 0.3s ease;
}
/* Buttons */
.btn-primary,
.btn-secondary {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.3s ease;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-secondary:hover {
background-color: #7f8c8d;
}
/* Document Details */
.document-details,
.search-details {
margin-top: 15px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.full-content pre {
white-space: pre-wrap;
word-wrap: break-word;
font-size: 0.85rem;
line-height: 1.4;
color: #495057;
background-color: white;
padding: 10px;
border-radius: 4px;
border: 1px solid #dee2e6;
max-height: 400px;
overflow-y: auto;
}
/* Search Info */
.search-info {
margin-bottom: 20px;
padding: 15px;
background-color: #e8f5e8;
border-radius: 6px;
border-left: 4px solid #4caf50;
}
.search-info em {
color: #2c3e50;
font-weight: 500;
}
/* No results */
.no-results {
text-align: center;
color: #7f8c8d;
font-style: italic;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px dashed #dee2e6;
}
/* Document Preview */
.document-preview {
margin-top: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.document-preview h4 {
margin: 0 0 8px 0;
font-size: 0.9rem;
color: #495057;
}
.document-preview p {
margin: 0;
font-size: 0.85rem;
color: #6c757d;
line-height: 1.4;
}
.search-results {
margin-top: 20px;
}
.chat-container {
margin-top: 20px;
border-top: 1px solid #ddd;
padding-top: 20px;
}
.loading {
display: none;
color: #666;
font-style: italic;
}
.error {
color: red;
margin: 10px 0;
}
.tabs {
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
margin-right: 5px;
background-color: #f0f0f0;
border: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
}
.tab-button.active {
background-color: #4caf50;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Chat Results */
.chat-result {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border: 1px solid #ddd;
}
.question-section,
.answer-section,
.related-docs-section {
margin-bottom: 20px;
padding: 15px;
border-radius: 6px;
}
.question-section {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
}
.answer-section {
background-color: #f1f8e9;
border-left: 4px solid #4caf50;
}
.related-docs-section {
background-color: #fff3e0;
border-left: 4px solid #ff9800;
}
.question-section h4,
.answer-section h4,
.related-docs-section h4 {
margin: 0 0 10px 0;
color: #2c3e50;
font-size: 1rem;
display: flex;
align-items: center;
gap: 8px;
}
.question-text {
margin: 0;
font-size: 1rem;
color: #34495e;
font-weight: 500;
}
.answer-text {
font-size: 0.95rem;
line-height: 1.6;
color: #2c3e50;
white-space: pre-wrap;
}
.related-docs {
margin-top: 10px;
}
.related-doc-item {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 12px;
margin-bottom: 8px;
transition: box-shadow 0.2s ease;
}
.related-doc-item:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.related-doc-item .doc-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.related-doc-item .doc-title {
font-weight: bold;
color: #2c3e50;
font-size: 0.9rem;
flex: 1;
}
.related-doc-item .doc-relevance {
background-color: #34495e;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: bold;
}
.related-doc-item .doc-preview {
font-size: 0.85rem;
color: #7f8c8d;
line-height: 1.4;
}
</style>
</head>
<body>
<div class="container">
<h1>AGC Document Chatbot</h1>
<!-- Tabs -->
<div class="tabs">
<button class="tab-button active" onclick="showTab('browse')">
Browse Documents
</button>
<button class="tab-button" onclick="showTab('search')">Search</button>
<button class="tab-button" onclick="showTab('chat')">Chat</button>
</div>
<!-- Browse Documents Tab -->
<div id="browse" class="tab-content active">
<div class="search-container">
<select id="docType">
<option value="">All Types</option>
</select>
<input
type="text"
id="titleFilter"
placeholder="Filter by title/content"
/>
<button onclick="listDocuments()">Filter</button>
</div>
<div id="documentsList"></div>
</div>
<!-- Search Tab -->
<div id="search" class="tab-content">
<div class="search-container">
<input
type="text"
id="searchQuery"
placeholder="Enter search terms"
/>
<button onclick="searchDocuments()">Search</button>
</div>
<div id="searchResults" class="search-results"></div>
</div>
<!-- Chat Tab -->
<div id="chat" class="tab-content">
<div class="search-container">
<input
type="text"
id="chatQuery"
placeholder="Ask about legal concepts"
/>
<button onclick="chatSearch()">Ask</button>
</div>
<div id="chatResults" class="chat-container"></div>
</div>
<div id="loading" class="loading">Loading...</div>
<div id="error" class="error"></div>
</div>
<script>
const API_URL = "http://localhost:8000";
// Add keyboard shortcuts
document.addEventListener("keydown", function (e) {
// Enter key in search input
if (e.key === "Enter") {
if (
document.getElementById("searchQuery") === document.activeElement
) {
searchDocuments();
} else if (
document.getElementById("chatQuery") === document.activeElement
) {
chatSearch();
} else if (
document.getElementById("titleFilter") === document.activeElement
) {
listDocuments();
}
}
// Alt + number to switch tabs
if (e.altKey) {
switch (e.key) {
case "1":
showTab("browse");
break;
case "2":
showTab("search");
break;
case "3":
showTab("chat");
break;
}
}
});
// Enhanced error handling
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(id);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
clearTimeout(id);
if (error.name === "AbortError") {
throw new Error("Request timed out");
}
throw error;
}
}
// Initialize document types with enhanced error handling
async function initializeDocTypes() {
try {
const response = await fetchWithTimeout(`${API_URL}/document-types`);
const data = await response.json();
const select = document.getElementById("docType");
data.document_types.forEach((type) => {
const option = document.createElement("option");
option.value = type;
option.textContent = type;
select.appendChild(option);
});
} catch (error) {
showError(`Error loading document types: ${error.message}`);
}
}
// Show/hide tabs
function showTab(tabId) {
document.querySelectorAll(".tab-content").forEach((tab) => {
tab.classList.remove("active");
});
document.querySelectorAll(".tab-button").forEach((button) => {
button.classList.remove("active");
});
document.getElementById(tabId).classList.add("active");
document
.querySelector(`button[onclick="showTab('${tabId}')"]`)
.classList.add("active");
}
// List documents
async function listDocuments() {
showLoading();
try {
const docType = document.getElementById("docType").value;
const titleFilter = document.getElementById("titleFilter").value;
const params = new URLSearchParams();
if (docType) params.append("doc_type", docType);
if (titleFilter) params.append("title_filter", titleFilter);
const response = await fetchWithTimeout(
`${API_URL}/documents?${params}`
);
const documents = await response.json();
if (documents.length === 0) {
showError("No documents found matching the criteria");
} else {
displayDocuments(documents);
}
} catch (error) {
showError(`Error loading documents: ${error.message}`);
}
hideLoading();
}
// Display documents
function displayDocuments(documents) {
const container = document.getElementById("documentsList");
container.innerHTML = "";
if (documents.length === 0) {
container.innerHTML =
'<p class="no-results">No documents found matching the criteria.</p>';
return;
}
documents.forEach((doc) => {
const card = document.createElement("div");
card.className = "document-card";
// Extract key information from content
const extractedData = extractLegalInfo(doc.content);
card.innerHTML = `
<div class="document-header">
<h3 class="document-title">${doc.title}</h3>
<div class="document-meta">
<span class="doc-type">${doc.doc_type}</span>
<span class="doc-date">${doc.created_at || "No date"}</span>
</div>
</div>
<div class="document-body">
${generateDocumentSummary(extractedData)}
<div class="document-preview">
<h4>Content Preview:</h4>
<p>${doc.content.substring(0, 200)}...</p>
</div>
</div>
<div class="document-footer">
<button class="btn-secondary" onclick="toggleDocumentDetails('${
doc.id
}')">
View Full Details
</button>
</div>
<div id="details-${
doc.id
}" class="document-details" style="display: none;">
<div class="full-content">
<h4>Full Document Content:</h4>
<pre>${doc.content}</pre>
</div>
</div>
`;
container.appendChild(card);
});
}
// Extract legal information from content
function extractLegalInfo(content) {
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 = [];
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(),
});
}
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>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>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>Persons Involved:</h4>";
summary += '<ul class="persons-list">';
data.persons.forEach((person) => {
summary += `<li class="person-item">${person.name}</li>`;
});
summary += "</ul></div>";
}
// Allegations count
if (data.allegations && data.allegations.length > 0) {
summary += '<div class="info-section">';
summary += `<h4>Allegations: (${data.allegations.length})</h4>`;
data.allegations.forEach((allegation, index) => {
summary += `<div class="allegation-item">
<strong>Allegation ${allegation.number}:</strong>
<p class="allegation-preview">${allegation.details}</p>
</div>`;
});
summary += "</div>";
}
summary += "</div>";
return summary;
}
// Toggle functions for expanding details
function toggleDocumentDetails(docId) {
const details = document.getElementById(`details-${docId}`);
if (details.style.display === "none") {
details.style.display = "block";
} else {
details.style.display = "none";
}
}
// Search documents
async function searchDocuments() {
const query = document.getElementById("searchQuery").value.trim();
if (!query) {
showError("Please enter a search query");
return;
}
showLoading();
try {
const response = await fetchWithTimeout(`${API_URL}/search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: query,
profile_search: false,
}),
});
const result = await response.json();
displaySearchResults(result);
} catch (error) {
showError(`Error performing search: ${error.message}`);
}
hideLoading();
}
// Display search results with enhanced formatting
function displaySearchResults(result) {
const container = document.getElementById("searchResults");
container.innerHTML = `
<div class="search-info">
<p><strong>Enhanced Query:</strong> <em>${result.enhanced_query}</em></p>
<h3>Results: (${result.documents.length} found)</h3>
</div>
`;
if (result.documents.length === 0) {
container.innerHTML +=
'<p class="no-results">No matching documents found.</p>';
return;
}
result.documents.forEach((doc, index) => {
const card = document.createElement("div");
card.className = "search-result-card";
const extractedData = extractLegalInfo(
doc.content || doc.content_preview
);
card.innerHTML = `
<div class="result-header">
<h3>${doc.title}</h3>
<div class="relevance-score">
<span class="score-label">Relevance:</span>
<span class="score-value">${doc.similarity.toFixed(3)}</span>
<div class="score-bar">
<div class="score-fill" style="width: ${(
doc.similarity * 100
).toFixed(0)}%"></div>
</div>
</div>
</div>
<div class="result-body">
${generateDocumentSummary(extractedData)}
</div>
<div class="result-footer">
<button class="btn-primary" onclick="toggleSearchDetails('${index}')">
View Full Content
</button>
</div>
<div id="search-details-${index}" class="search-details" style="display: none;">
<div class="full-content">
<h4>Full Content:</h4>
<pre>${doc.content_preview}</pre>
</div>
</div>
`;
container.appendChild(card);
});
}
// Toggle functions for expanding details
function toggleSearchDetails(index) {
const details = document.getElementById(`search-details-${index}`);
if (details.style.display === "none") {
details.style.display = "block";
} else {
details.style.display = "none";
}
}
// Chat search
async function chatSearch() {
const query = document.getElementById("chatQuery").value.trim();
if (!query) {
showError("Please enter a question");
return;
}
showLoading();
try {
const response = await fetchWithTimeout(`${API_URL}/search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: query,
profile_search: false,
}),
});
const result = await response.json();
displayChatResults(result);
} catch (error) {
showError(`Error in chat: ${error.message}`);
}
hideLoading();
}
// Display chat results
function displayChatResults(result) {
const container = document.getElementById("chatResults");
container.innerHTML = `
<div class="chat-result">
<div class="question-section">
<h4><i class="icon-question">❓</i> Question:</h4>
<p class="question-text">${result.query}</p>
</div>
<div class="answer-section">
<h4><i class="icon-answer">💡</i> Answer:</h4>
<div class="answer-text">${result.answer}</div>
</div>
${
result.documents && result.documents.length > 0
? `
<div class="related-docs-section">
<h4><i class="icon-docs">📚</i> Related Documents (${
result.documents.length
}):</h4>
<div class="related-docs">
${result.documents
.slice(0, 3)
.map(
(doc, index) => `
<div class="related-doc-item">
<div class="doc-header">
<span class="doc-title">${doc.title}</span>
<span class="doc-relevance">${doc.similarity.toFixed(
3
)}</span>
</div>
<div class="doc-preview">${doc.content_preview.substring(
0,
150
)}...</div>
</div>
`
)
.join("")}
</div>
${
result.documents.length > 3
? `
<button class="btn-secondary" onclick="toggleAllRelatedDocs()">
View All ${result.documents.length} Related Documents
</button>
<div id="all-related-docs" style="display: none;">
${result.documents
.slice(3)
.map(
(doc, index) => `
<div class="related-doc-item">
<div class="doc-header">
<span class="doc-title">${doc.title}</span>
<span class="doc-relevance">${doc.similarity.toFixed(
3
)}</span>
</div>
<div class="doc-preview">${doc.content_preview.substring(
0,
150
)}...</div>
</div>
`
)
.join("")}
</div>
`
: ""
}
</div>
`
: ""
}
</div>
`;
}
function toggleAllRelatedDocs() {
const allDocs = document.getElementById("all-related-docs");
if (allDocs.style.display === "none") {
allDocs.style.display = "block";
} else {
allDocs.style.display = "none";
}
}
// Utility functions
function showLoading() {
document.getElementById("loading").style.display = "block";
document.getElementById("error").style.display = "none";
}
function hideLoading() {
document.getElementById("loading").style.display = "none";
}
function showError(message) {
const errorDiv = document.getElementById("error");
errorDiv.textContent = message;
errorDiv.style.display = "block";
}
// Initialize the page
initializeDocTypes();
listDocuments();
</script>
</body>
</html>