agc-chatbot/agc-document-search.html

3571 lines
144 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AGC Document Search - Attorney General's Chambers</title>
<meta
name="description"
content="AI-powered document search system for Attorney General's Chambers"
/>
<!-- Favicon -->
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iOCIgZmlsbD0iIzFFNDBBRiIvPgo8cGF0aCBkPSJNOCAxMEgxNlY5SDE2VjEwSDE2VjIySDhWMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTggMTBIMjZWMjJIMThWMTBaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTMgMTJIMjFWMTRIMTNWMTJaIiBmaWxsPSIjMUU0MEFGIi8+CjxwYXRoIGQ9Ik0xMyAxNkgyMVYxOEgxM1YxNloiIGZpbGw9IiMxRTQwQUYiLz4KPC9zdmc+"
/>
<!-- CDN Libraries -->
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
"agc-primary": "#1E40AF",
"agc-secondary": "#D97706",
"agc-success": "#059669",
"agc-warning": "#D97706",
"agc-error": "#DC2626",
"agc-gray": {
50: "#F9FAFB",
100: "#F3F4F6",
200: "#E5E7EB",
300: "#D1D5DB",
400: "#9CA3AF",
500: "#6B7280",
600: "#4B5563",
700: "#374151",
800: "#1F2937",
900: "#111827",
},
},
fontFamily: {
inter: ["Inter", "system-ui", "sans-serif"],
mono: ["Fira Code", "monospace"],
},
animation: {
"fade-in": "fadeIn 0.3s ease-in-out",
"slide-up": "slideUp 0.3s ease-out",
"slide-down": "slideDown 0.3s ease-out",
"scale-in": "scaleIn 0.2s ease-out",
"pulse-slow": "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
},
keyframes: {
fadeIn: {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
slideUp: {
"0%": { transform: "translateY(100%)", opacity: "0" },
"100%": { transform: "translateY(0)", opacity: "1" },
},
slideDown: {
"0%": { transform: "translateY(-100%)", opacity: "0" },
"100%": { transform: "translateY(0)", opacity: "1" },
},
scaleIn: {
"0%": { transform: "scale(0.95)", opacity: "0" },
"100%": { transform: "scale(1)", opacity: "1" },
},
},
},
},
};
</script>
<!-- Alpine.js -->
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Google Fonts - Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap"
rel="stylesheet"
/>
<!-- Heroicons -->
<script
src="https://unpkg.com/heroicons@2.0.16/24/outline/index.js"
type="module"
></script>
<!-- Custom Styles -->
<style>
/* Enhanced animations and transitions */
* {
font-family: "Inter", system-ui, sans-serif;
}
.glass-morphism {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.loading-skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.hover-lift {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* Status indicators */
.status-online {
color: #059669;
}
.status-offline {
color: #dc2626;
}
.status-loading {
color: #d97706;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.mobile-hide {
display: none;
}
.mobile-full {
width: 100% !important;
}
}
/* Enhanced Component Styles for Phase 2-3 */
.btn-primary {
@apply bg-gradient-to-r from-agc-primary to-blue-600 text-white px-4 py-2 rounded-lg font-medium shadow-md hover:shadow-lg transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none;
}
.btn-secondary {
@apply bg-agc-gray-100 text-agc-gray-700 px-4 py-2 rounded-lg font-medium hover:bg-agc-gray-200 transition-all duration-200 border border-agc-gray-200;
}
.btn-ghost {
@apply text-agc-gray-500 hover:text-agc-gray-700 hover:bg-agc-gray-100 transition-all duration-200 rounded-lg;
}
.input-field {
@apply w-full px-3 py-2 border border-agc-gray-300 rounded-lg focus:ring-2 focus:ring-agc-primary focus:border-agc-primary transition-all duration-200 bg-white;
}
.search-input {
@apply w-full pl-12 pr-24 py-4 text-lg border-2 border-agc-gray-200 rounded-xl focus:ring-4 focus:ring-agc-primary focus:ring-opacity-20 focus:border-agc-primary transition-all duration-300 bg-white shadow-sm;
}
.agc-card {
@apply bg-white rounded-xl shadow-sm border border-agc-gray-100 transition-all duration-200;
}
.grid-responsive {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
}
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
.badge-primary {
@apply bg-agc-primary text-white;
}
.badge-secondary {
@apply bg-agc-gray-100 text-agc-gray-800;
}
.badge-success {
@apply bg-agc-success text-white;
}
.badge-warning {
@apply bg-agc-warning text-white;
}
.badge-error {
@apply bg-agc-error text-white;
}
.doc-type-icon {
@apply w-12 h-12 rounded-lg flex items-center justify-center text-xs font-bold text-white;
}
.doc-type-lkk {
@apply bg-gradient-to-br from-agc-primary to-blue-600;
}
.doc-type-case {
@apply bg-gradient-to-br from-agc-success to-green-600;
}
.doc-type-legal {
@apply bg-gradient-to-br from-agc-warning to-orange-600;
}
.doc-type-default {
@apply bg-gradient-to-br from-agc-gray-500 to-agc-gray-600;
}
.text-gradient {
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.status-indicator {
@apply w-3 h-3 rounded-full;
}
.status-indicator.status-online {
@apply bg-agc-success;
animation: pulse 2s infinite;
}
.status-indicator.status-offline {
@apply bg-agc-error;
}
.status-indicator.status-loading {
@apply bg-agc-warning;
animation: pulse 1s infinite;
}
.pill-button {
@apply inline-flex items-center px-3 py-2 bg-agc-gray-100 text-agc-gray-700 rounded-full text-sm hover:bg-agc-primary hover:text-white transition-all duration-200 cursor-pointer;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Search result highlighting */
.search-highlight {
@apply bg-yellow-200 px-1 rounded font-medium;
}
/* Responsive utilities */
@media (max-width: 640px) {
.grid-responsive {
@apply grid-cols-1;
}
}
/* AI Assistant Styles - Phase 4 */
.ai-suggestion-btn {
@apply flex items-center justify-center px-4 py-3 bg-gradient-to-br from-purple-50 to-indigo-50 text-purple-700 rounded-lg border border-purple-200 hover:from-purple-100 hover:to-indigo-100 hover:border-purple-300 transition-all duration-200 text-sm font-medium;
}
.ai-suggestion-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.15);
}
/* Chat message animations */
.chat-message-enter {
animation: chatMessageSlide 0.3s ease-out;
}
@keyframes chatMessageSlide {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Chart container styles */
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
.chart-legend {
@apply flex items-center justify-center space-x-4 text-sm text-agc-gray-600 mt-4;
}
.chart-legend-item {
@apply flex items-center space-x-2;
}
.chart-legend-color {
@apply w-3 h-3 rounded-full;
}
</style>
</head>
<body class="bg-agc-gray-50 font-inter antialiased">
<!-- Main Application Container -->
<div
id="app"
x-data="agcApp()"
x-init="initializeApp()"
class="min-h-screen flex flex-col"
>
<!-- Loading Overlay -->
<div
x-show="appState.isInitializing"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-white z-50 flex items-center justify-center"
>
<div class="text-center">
<div
class="w-16 h-16 border-4 border-agc-primary border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<h2 class="text-xl font-semibold text-agc-gray-800 mb-2">
AGC Document Search
</h2>
<p class="text-agc-gray-600">Initializing system...</p>
</div>
</div>
<!-- Header -->
<header
class="bg-white shadow-lg border-b border-agc-gray-200 sticky top-0 z-40"
style="background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)"
>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-18">
<!-- Enhanced Logo and Branding -->
<div class="flex items-center space-x-4">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-gradient-to-br from-agc-primary to-blue-600 rounded-xl flex items-center justify-center shadow-lg hover:shadow-xl transition-all duration-300"
>
<svg
class="w-7 h-7 text-white"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
/>
</svg>
</div>
</div>
<div class="py-2">
<h1 class="text-xl font-bold text-agc-gray-900 tracking-tight">
<span class="text-gradient">AGC Document Search</span>
</h1>
<p class="text-sm text-agc-gray-600 mobile-hide font-medium">
Attorney General's Chambers Malaysia
</p>
</div>
</div>
<!-- Enhanced Status and Info Panel -->
<div class="flex items-center space-x-6">
<!-- Connection Status with Enhanced Indicator -->
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2">
<div
class="status-indicator"
:class="{
'status-online': appState.isOnline && appState.apiStatus === 'connected',
'status-loading': appState.apiStatus === 'connecting',
'status-offline': !appState.isOnline || appState.apiStatus === 'disconnected'
}"
></div>
<div class="mobile-hide">
<p
class="text-sm font-medium text-agc-gray-900"
x-text="appState.isOnline ? 'System Online' : 'System Offline'"
></p>
<p
class="text-xs text-agc-gray-500"
x-text="'API: ' + appState.apiStatus"
></p>
</div>
</div>
</div>
<!-- Enhanced Stats Display -->
<div
class="mobile-hide bg-agc-gray-50 rounded-lg px-4 py-2 border border-agc-gray-200"
>
<div class="flex items-center space-x-4 text-sm">
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-agc-primary rounded-full"></div>
<span class="font-medium text-agc-gray-700">
<span x-text="appState.stats.totalDocuments"></span>
<span class="text-agc-gray-500">Documents</span>
</span>
</div>
<div class="w-px h-4 bg-agc-gray-300"></div>
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-agc-secondary rounded-full"></div>
<span class="font-medium text-agc-gray-700">
<span x-text="appState.stats.searchCount"></span>
<span class="text-agc-gray-500">Searches</span>
</span>
</div>
</div>
</div>
<!-- Settings/Menu Button -->
<button
class="btn-ghost p-2 rounded-lg hover:bg-agc-gray-100 transition-colors"
>
<svg
class="w-5 h-5 text-agc-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
></path>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
</button>
</div>
</div>
</div>
<!-- Optional: Quick Stats Bar (shown on mobile) -->
<div
class="lg:hidden bg-agc-gray-50 border-t border-agc-gray-200 px-4 py-2"
>
<div class="flex justify-between items-center text-sm">
<span class="text-agc-gray-600">
<span x-text="appState.stats.totalDocuments"></span> docs
</span>
<span class="text-agc-gray-600">
<span x-text="appState.stats.searchCount"></span> searches
</span>
<span
:class="appState.apiStatus === 'connected' ? 'text-agc-success' : 'text-agc-error'"
>
<span x-text="appState.apiStatus"></span>
</span>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-8">
<!-- Enhanced Navigation Tabs -->
<div class="mb-8">
<div
class="border-b border-agc-gray-200 bg-white rounded-t-lg shadow-sm"
>
<nav class="flex space-x-8 px-6" aria-label="Tabs">
<template x-for="tab in appState.tabs" :key="tab.id">
<button
@click="switchTab(tab.id)"
:class="{
'border-agc-primary text-agc-primary bg-blue-50': appState.activeTab === tab.id,
'border-transparent text-agc-gray-500 hover:text-agc-gray-700 hover:border-agc-gray-300': appState.activeTab !== tab.id
}"
class="relative whitespace-nowrap py-4 px-6 border-b-3 font-semibold text-sm transition-all duration-200 rounded-t-lg"
>
<div class="flex items-center space-x-2">
<!-- Tab Icons -->
<div x-show="tab.id === 'search'">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div x-show="tab.id === 'browse'">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<div x-show="tab.id === 'analytics'">
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
</div>
<span x-text="tab.name"></span>
<!-- Enhanced Badge -->
<span
x-show="tab.badge"
x-text="tab.badge"
class="badge badge-primary"
></span>
</div>
<!-- Active Tab Indicator -->
<div
x-show="appState.activeTab === tab.id"
class="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-agc-primary to-blue-500 rounded-full"
></div>
</button>
</template>
</nav>
</div>
</div>
<!-- Tab Content -->
<div class="space-y-6">
<!-- Search Tab -->
<div
x-show="appState.activeTab === 'search'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
>
<!-- Main Search Interface -->
<div class="agc-card p-8">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold text-agc-gray-900 mb-3">
<span class="text-gradient"
>AI-Powered Legal Document Search</span
>
</h2>
<p class="text-lg text-agc-gray-600 max-w-2xl mx-auto">
Intelligent search through Attorney General's Chambers legal
documents using advanced AI technology
</p>
</div>
<!-- Search Input Section -->
<div class="max-w-4xl mx-auto mb-8">
<div class="relative">
<div
class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"
>
<svg
class="h-6 w-6 text-agc-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<input
type="text"
placeholder="Ask me anything about legal documents... (e.g., 'Find cases about cybercrime from 2023')"
class="search-input w-full"
x-model="appState.searchQuery"
@keydown.enter="performSearch(appState.searchQuery)"
/>
<div
class="absolute inset-y-0 right-0 flex items-center pr-3"
>
<button
class="btn-primary px-6 py-2 rounded-lg flex items-center space-x-2"
@click="performSearch(appState.searchQuery)"
:disabled="appState.loading.search"
>
<span x-show="!appState.loading.search">Search</span>
<span x-show="appState.loading.search">Searching...</span>
</button>
</div>
</div>
</div>
<!-- Quick Search Suggestions -->
<div class="max-w-4xl mx-auto">
<h3 class="text-lg font-semibold text-agc-gray-900 mb-4">
Quick Searches
</h3>
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
<template
x-for="suggestion in [
'Recent criminal cases',
'Constitutional law decisions',
'Corporate litigation',
'Human rights cases',
'Tax law judgments',
'Employment disputes'
]"
:key="suggestion"
>
<button
class="agc-card p-4 text-left hover-lift border-l-4 border-agc-primary"
@click="appState.searchQuery = suggestion; performSearch(suggestion)"
>
<div class="flex items-center space-x-3">
<div
class="w-10 h-10 bg-agc-primary bg-opacity-10 rounded-lg flex items-center justify-center"
>
<svg
class="w-5 h-5 text-agc-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div>
<p
class="font-medium text-agc-gray-900"
x-text="suggestion"
></p>
<p class="text-sm text-agc-gray-500">
Click to search
</p>
</div>
</div>
</button>
</template>
</div>
</div>
</div>
<!-- Search Results Section -->
<div
x-show="appState.searchResults && appState.searchResults.documents"
class="space-y-6"
>
<!-- Results Header -->
<div class="agc-card p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-agc-gray-900">
Search Results
</h3>
<div class="flex items-center space-x-4">
<span class="text-sm text-agc-gray-500">
<span
x-text="appState.searchResults.documents?.length || 0"
></span>
results found
</span>
<button
class="btn-secondary text-sm"
@click="exportResults()"
>
Export Results
</button>
</div>
</div>
<!-- AI Answer Section -->
<div
x-show="appState.searchResults.answer"
class="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-6 mb-6"
>
<div class="flex items-start space-x-3">
<div
class="w-8 h-8 bg-agc-primary rounded-lg flex items-center justify-center flex-shrink-0"
>
<svg
class="w-5 h-5 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
</div>
<div class="flex-1">
<h4 class="font-semibold text-agc-gray-900 mb-2">
AI Analysis
</h4>
<p
class="text-agc-gray-700 leading-relaxed"
x-text="appState.searchResults.answer"
></p>
<div class="mt-3 flex items-center space-x-4 text-sm">
<button
class="text-agc-primary hover:text-agc-primary font-medium"
>
<svg
class="w-4 h-4 inline mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
Copy Answer
</button>
<button
class="text-agc-primary hover:text-agc-primary font-medium"
>
<svg
class="w-4 h-4 inline mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
/>
</svg>
Share
</button>
</div>
</div>
</div>
</div>
<!-- Enhanced Query Display -->
<div
x-show="appState.searchResults.enhanced_query"
class="bg-agc-gray-50 border border-agc-gray-200 rounded-lg p-4"
>
<div class="flex items-center space-x-2 mb-2">
<svg
class="w-4 h-4 text-agc-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<span class="text-sm font-medium text-agc-gray-700"
>Enhanced Query</span
>
</div>
<p
class="text-sm text-agc-gray-600"
x-text="appState.searchResults.enhanced_query"
></p>
</div>
</div>
<!-- Search Results Filter Bar -->
<div class="agc-card p-4">
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-3 sm:space-y-0"
>
<div class="flex items-center space-x-4">
<label class="flex items-center space-x-2 text-sm">
<input
type="checkbox"
class="rounded border-agc-gray-300 text-agc-primary"
/>
<span>Show only high relevance (>70%)</span>
</label>
<select class="input-field py-1 text-sm w-auto">
<option>All document types</option>
<option>LKK only</option>
<option>Cases only</option>
<option>Legal documents only</option>
</select>
</div>
<div
class="flex items-center space-x-2 text-sm text-agc-gray-500"
>
<span>Sort by:</span>
<select class="input-field py-1 text-sm w-auto">
<option>Relevance</option>
<option>Date (newest)</option>
<option>Date (oldest)</option>
<option>Title A-Z</option>
</select>
</div>
</div>
</div>
<!-- Results List -->
<div class="space-y-4">
<template
x-for="(doc, index) in appState.searchResults.documents"
:key="doc.id || index"
>
<div
class="agc-card hover-lift p-6 transition-all duration-300 border-l-4"
:class="{
'border-green-400 bg-green-50': doc.similarity > 0.8,
'border-blue-400 bg-blue-50': doc.similarity > 0.6 && doc.similarity <= 0.8,
'border-yellow-400 bg-yellow-50': doc.similarity > 0.4 && doc.similarity <= 0.6,
'border-gray-400 bg-gray-50': doc.similarity <= 0.4 || !doc.similarity
}"
>
<div class="flex items-start space-x-4">
<!-- Document Type Icon -->
<div class="flex-shrink-0">
<div
class="doc-type-icon"
:class="getDocTypeClass(doc.doc_type)"
>
<span
x-text="formatDocumentType(doc.doc_type)"
></span>
</div>
</div>
<!-- Document Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between mb-3">
<div class="flex-1">
<h4
class="text-lg font-semibold text-agc-gray-900 mb-2 cursor-pointer hover:text-agc-primary"
@click="viewDocument(doc)"
x-text="doc.title"
></h4>
<!-- Document Metadata -->
<div
class="flex items-center space-x-4 text-sm text-agc-gray-500 mb-3"
>
<span
x-text="'Type: ' + (doc.doc_type || 'Document')"
></span>
<span
x-show="doc.created_at"
x-text="'Date: ' + utils.formatDate(doc.created_at)"
></span>
<span
x-show="doc.source"
x-text="'Source: ' + doc.source"
></span>
</div>
</div>
<!-- Action Buttons and Relevance -->
<div
class="flex items-center space-x-3 ml-4 flex-shrink-0"
>
<span
x-show="doc.similarity"
class="badge badge-primary text-xs"
>
<span
x-text="Math.round(doc.similarity * 100)"
></span
>% match
</span>
<button
class="btn-ghost p-2"
:class="{ 'text-agc-primary': isBookmarked(doc.id) }"
@click="toggleBookmark(doc.id)"
:title="isBookmarked(doc.id) ? 'Remove bookmark' : 'Add bookmark'"
>
<svg
class="w-4 h-4"
:fill="isBookmarked(doc.id) ? 'currentColor' : 'none'"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"
/>
</svg>
</button>
<div class="relative" x-data="{ open: false }">
<button
class="btn-ghost p-2"
@click="open = !open"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
</button>
<!-- Dropdown Menu -->
<div
x-show="open"
@click.away="open = false"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-agc-gray-200 z-10"
>
<div class="py-1">
<button
class="w-full text-left px-4 py-2 text-sm text-agc-gray-700 hover:bg-agc-gray-50"
@click="viewDocument(doc); open = false"
>
<svg
class="w-4 h-4 inline mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
View Full Document
</button>
<button
class="w-full text-left px-4 py-2 text-sm text-agc-gray-700 hover:bg-agc-gray-50"
>
<svg
class="w-4 h-4 inline mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"
/>
</svg>
Share Document
</button>
<button
class="w-full text-left px-4 py-2 text-sm text-agc-gray-700 hover:bg-agc-gray-50"
>
<svg
class="w-4 h-4 inline mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
Download PDF
</button>
<button
class="w-full text-left px-4 py-2 text-sm text-agc-gray-700 hover:bg-agc-gray-50"
>
<svg
class="w-4 h-4 inline mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
Find Similar
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Content Preview -->
<div class="mb-4">
<p
class="text-agc-gray-600 leading-relaxed"
x-html="highlightSearchTerms(doc.content_preview || utils.truncateText(doc.content, 300), appState.searchResults.query)"
></p>
</div>
<!-- Document Actions Bar -->
<div
class="flex items-center justify-between pt-4 border-t border-agc-gray-100"
>
<div class="flex items-center space-x-2">
<span
class="badge"
:class="{
'badge-primary': doc.doc_type === 'LKK',
'badge-success': doc.doc_type === 'Case',
'badge-warning': doc.doc_type === 'Legal',
'badge-error': !doc.doc_type
}"
x-text="doc.doc_type || 'Unknown'"
>
</span>
<span
x-show="doc.content"
class="text-xs text-agc-gray-500"
>
<span x-text="doc.content.length"></span>
characters
</span>
</div>
<div class="flex items-center space-x-2">
<button
class="btn-secondary text-sm px-3 py-1"
@click="viewDocument(doc)"
>
View Details
</button>
<button
class="btn-primary text-sm px-3 py-1"
@click="openDocument(doc)"
>
Open Document
</button>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Load More / Pagination -->
<div class="text-center py-8">
<button
class="btn-secondary"
x-show="appState.searchResults.documents && appState.searchResults.documents.length >= 10"
>
Load More Results
</button>
</div>
</div>
<!-- AI Assistant Panel - Phase 4 Feature -->
<div x-show="appState.searchResults && appState.searchResults.documents" class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<!-- AI Chat Interface -->
<div class="lg:col-span-2">
<div class="agc-card p-6">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-agc-gray-900">AI Legal Assistant</h3>
<p class="text-sm text-agc-gray-600">Ask questions about legal concepts, document analysis, or get explanations</p>
</div>
</div>
<div class="flex items-center space-x-2">
<div class="status-indicator status-online"></div>
<span class="text-sm text-agc-gray-600">AI Ready</span>
</div>
</div>
<!-- Chat Messages -->
<div class="mb-6 h-64 overflow-y-auto custom-scrollbar bg-agc-gray-50 rounded-lg p-4" x-ref="chatContainer">
<div class="space-y-4" x-show="appState.aiChat.messages.length === 0">
<div class="text-center py-8">
<div class="w-16 h-16 bg-gradient-to-br from-purple-100 to-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
</div>
<h4 class="text-lg font-medium text-agc-gray-900 mb-2">Start a conversation</h4>
<p class="text-agc-gray-600 mb-4">Ask me anything about legal documents, case analysis, or legal concepts.</p>
<!-- Quick Start Options -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-lg mx-auto">
<button class="ai-suggestion-btn" @click="askAI('Explain the difference between civil and criminal law')">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Legal Concepts
</button>
<button class="ai-suggestion-btn" @click="askAI('Summarize the key points from my recent search results')">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Document Analysis
</button>
<button class="ai-suggestion-btn" @click="askAI('How do I search for cases related to intellectual property?')">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Search Tips
</button>
<button class="ai-suggestion-btn" @click="askAI('What are the recent trends in Malaysian case law?')">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
Legal Trends
</button>
</div>
</div>
</div>
<!-- Chat Messages -->
<div class="space-y-4" x-show="appState.aiChat.messages.length > 0">
<template x-for="(message, index) in appState.aiChat.messages" :key="index">
<div class="flex" :class="message.role === 'user' ? 'justify-end' : 'justify-start'">
<div class="flex items-start space-x-3 max-w-3xl">
<div x-show="message.role === 'assistant'" class="w-8 h-8 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-full flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<div
class="rounded-lg px-4 py-3 text-sm"
:class="{
'bg-agc-primary text-white': message.role === 'user',
'bg-white border border-agc-gray-200 text-agc-gray-800': message.role === 'assistant'
}"
>
<div x-html="formatAIMessage(message.content)"></div>
<div class="text-xs opacity-75 mt-2" x-text="utils.formatDate(message.timestamp)"></div>
</div>
<div x-show="message.role === 'user'" class="w-8 h-8 bg-agc-primary rounded-full flex items-center justify-center flex-shrink-0">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
</div>
</div>
</template>
</div>
<!-- Typing Indicator -->
<div x-show="appState.aiChat.isTyping" class="flex justify-start">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<div class="bg-white border border-agc-gray-200 rounded-lg px-4 py-3">
<div class="flex space-x-1">
<div class="w-2 h-2 bg-agc-gray-400 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-agc-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-agc-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Input -->
<div class="flex items-end space-x-3">
<div class="flex-1">
<textarea
x-model="appState.aiChat.currentMessage"
@keydown.enter.prevent="$event.ctrlKey || $event.metaKey ? sendAIMessage() : null"
placeholder="Ask about legal concepts, document analysis, or search guidance... (Ctrl+Enter to send)"
class="input-field resize-none h-20"
:disabled="appState.aiChat.isTyping"
></textarea>
</div>
<button
class="btn-primary px-4 py-2 h-12"
@click="sendAIMessage()"
:disabled="!appState.aiChat.currentMessage.trim() || appState.aiChat.isTyping"
>
<svg x-show="!appState.aiChat.isTyping" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
<div x-show="appState.aiChat.isTyping" class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</button>
</div>
</div>
</div>
<!-- Legal Concept Explorer -->
<div class="lg:col-span-1">
<div class="agc-card p-6">
<h4 class="text-lg font-semibold text-agc-gray-900 mb-4">Legal Concept Explorer</h4>
<!-- Recent Concepts -->
<div class="mb-6">
<h5 class="text-sm font-medium text-agc-gray-700 mb-3">Recently Explained</h5>
<div class="space-y-2">
<template x-for="concept in appState.aiChat.recentConcepts" :key="concept.id">
<button
class="w-full text-left p-3 bg-agc-gray-50 hover:bg-agc-gray-100 rounded-lg transition-colors"
@click="askAI('Explain ' + concept.name)"
>
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-agc-gray-900" x-text="concept.name"></span>
<svg class="w-4 h-4 text-agc-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
<p class="text-xs text-agc-gray-600 mt-1" x-text="concept.description"></p>
</button>
</template>
</div>
</div>
<!-- Common Legal Terms -->
<div class="mb-6">
<h5 class="text-sm font-medium text-agc-gray-700 mb-3">Common Legal Terms</h5>
<div class="flex flex-wrap gap-2">
<template x-for="term in ['Tort', 'Injunction', 'Precedent', 'Jurisdiction', 'Plaintiff', 'Defendant']" :key="term">
<button
class="pill-button text-xs"
@click="askAI('Define ' + term + ' in Malaysian law context')"
x-text="term"
></button>
</template>
</div>
</div>
<!-- Quick Actions -->
<div>
<h5 class="text-sm font-medium text-agc-gray-700 mb-3">Quick Actions</h5>
<div class="space-y-2">
<button class="w-full btn-secondary text-sm py-2" @click="generateDocumentSummary()">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Summarize Results
</button>
<button class="w-full btn-secondary text-sm py-2" @click="analyzeLegalTrends()">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
Analyze Trends
</button>
<button class="w-full btn-secondary text-sm py-2" @click="clearAIChat()">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Clear Chat
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Search History Section -->
<div x-show="appState.searchHistory.length > 0" class="agc-card p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-agc-gray-900">Recent Searches</h3>
<button class="text-sm text-agc-primary hover:text-agc-primary font-medium" @click="clearSearchHistory()">
Clear History
</button>
</div>
<div class="flex flex-wrap gap-2">
<template x-for="(search, index) in appState.searchHistory.slice(0, 10)" :key="search.id">
<button class="pill-button" @click="appState.searchQuery = search.query; performSearch(search.query)">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span x-text="search.query"></span>
<span class="text-xs text-agc-gray-400 ml-2" x-text="utils.formatDate(search.timestamp)"></span>
</button>
</template>
</div>
</div>
</div>
<!-- Browse Tab -->
<div
x-show="appState.activeTab === 'browse'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
>
<!-- Browse Header and Filters -->
<div class="agc-card p-6 mb-6">
<div
class="flex flex-col lg:flex-row lg:items-center lg:justify-between mb-6"
>
<div>
<h2 class="text-2xl font-bold text-agc-gray-900 mb-2">
Document Library
</h2>
<p class="text-agc-gray-600">
Browse and filter through all available legal documents
</p>
</div>
<!-- Bulk Actions and View Toggle -->
<div class="flex items-center space-x-4 mt-4 lg:mt-0">
<!-- Bulk Actions -->
<div
x-show="appState.selectedDocuments.length > 0"
class="flex items-center space-x-2"
>
<span class="text-sm text-agc-gray-600">
<span x-text="appState.selectedDocuments.length"></span>
selected
</span>
<button
class="btn-secondary text-sm px-3 py-1"
@click="bulkExport()"
>
Export Selected
</button>
<button
class="btn-ghost text-sm px-3 py-1"
@click="clearSelection()"
>
Clear
</button>
</div>
<!-- View Toggle -->
<div class="flex items-center space-x-2">
<span class="text-sm text-agc-gray-500">View:</span>
<div class="flex bg-agc-gray-100 rounded-lg p-1">
<button
class="px-3 py-1 rounded-md text-sm font-medium transition-all"
:class="appState.preferences.viewMode === 'grid' ? 'bg-white text-agc-gray-700 shadow-sm' : 'text-agc-gray-500 hover:text-agc-gray-700'"
@click="setViewMode('grid')"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"
/>
</svg>
</button>
<button
class="px-3 py-1 rounded-md text-sm font-medium transition-all"
:class="appState.preferences.viewMode === 'list' ? 'bg-white text-agc-gray-700 shadow-sm' : 'text-agc-gray-500 hover:text-agc-gray-700'"
@click="setViewMode('list')"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 10h16M4 14h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Advanced Search and Filters Row -->
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<!-- Search Documents -->
<div class="md:col-span-2">
<label
class="block text-sm font-medium text-agc-gray-700 mb-2"
>Search Documents</label
>
<div class="relative">
<input
type="text"
placeholder="Search by title, content, or metadata..."
class="input-field pr-10"
x-model="appState.filters.title_filter"
@input="debouncedFilter()"
/>
<div
class="absolute inset-y-0 right-0 pr-3 flex items-center"
>
<svg
class="w-4 h-4 text-agc-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
</div>
</div>
<!-- Document Type Filter -->
<div>
<label
class="block text-sm font-medium text-agc-gray-700 mb-2"
>Document Type</label
>
<select
class="input-field"
x-model="appState.filters.doc_type"
@change="loadDocuments(appState.filters)"
>
<template
x-for="type in appState.documentTypes"
:key="type"
>
<option :value="type" x-text="type"></option>
</template>
</select>
</div>
<!-- Date Range Filter -->
<div>
<label
class="block text-sm font-medium text-agc-gray-700 mb-2"
>Date Range</label
>
<select
class="input-field"
x-model="appState.filters.dateRange"
@change="loadDocuments(appState.filters)"
>
<option value="">All Dates</option>
<option value="today">Today</option>
<option value="week">Last 7 days</option>
<option value="month">Last 30 days</option>
<option value="year">Last year</option>
</select>
</div>
<!-- Sort Options -->
<div>
<label
class="block text-sm font-medium text-agc-gray-700 mb-2"
>Sort By</label
>
<select
class="input-field"
x-model="appState.filters.sortBy"
@change="loadDocuments(appState.filters)"
>
<option value="created_at">Date Added</option>
<option value="title">Title A-Z</option>
<option value="doc_type">Document Type</option>
<option value="content_length">Content Length</option>
</select>
</div>
</div>
<!-- Active Filters Display -->
<div x-show="hasActiveFilters()" class="mt-4">
<div class="flex items-center space-x-2 flex-wrap">
<span class="text-sm text-agc-gray-500">Active filters:</span>
<template x-if="appState.filters.title_filter">
<span class="badge badge-primary">
Search:
<span x-text="appState.filters.title_filter"></span>
<button
class="ml-1 text-white hover:text-agc-gray-200"
@click="appState.filters.title_filter = ''; loadDocuments(appState.filters)"
>
×
</button>
</span>
</template>
<template x-if="appState.filters.doc_type !== 'All Types'">
<span class="badge badge-primary">
Type: <span x-text="appState.filters.doc_type"></span>
<button
class="ml-1 text-white hover:text-agc-gray-200"
@click="appState.filters.doc_type = 'All Types'; loadDocuments(appState.filters)"
>
×
</button>
</span>
</template>
<template x-if="appState.filters.dateRange">
<span class="badge badge-primary">
Date: <span x-text="appState.filters.dateRange"></span>
<button
class="ml-1 text-white hover:text-agc-gray-200"
@click="appState.filters.dateRange = ''; loadDocuments(appState.filters)"
>
×
</button>
</span>
</template>
<button
class="text-sm text-agc-primary hover:text-agc-primary font-medium"
@click="clearAllFilters()"
>
Clear all filters
</button>
</div>
</div>
<!-- Results Summary -->
<div
class="mt-4 flex items-center justify-between text-sm text-agc-gray-600"
>
<span>
Showing <span x-text="getDisplayedCount()"></span> of
<span x-text="appState.stats.totalDocuments"></span> documents
</span>
<div class="flex items-center space-x-2">
<span>Items per page:</span>
<select
class="input-field py-1 text-sm w-auto"
x-model="appState.preferences.resultsPerPage"
@change="loadDocuments(appState.filters)"
>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
</div>
<!-- Documents Grid/List Display -->
<div
:class="appState.preferences.viewMode === 'grid' ? 'grid-responsive' : 'space-y-4'"
>
<template
x-for="(doc, index) in appState.documents"
:key="doc.id || index"
>
<div
class="agc-card hover-lift transition-all duration-300 relative"
:class="appState.preferences.viewMode === 'grid' ? 'p-6' : 'p-4'"
>
<!-- Selection Checkbox -->
<div class="absolute top-4 left-4 z-10">
<input
type="checkbox"
class="rounded border-agc-gray-300 text-agc-primary"
:checked="isDocumentSelected(doc.id)"
@change="toggleDocumentSelection(doc.id)"
/>
</div>
<!-- Grid View Layout -->
<div x-show="appState.preferences.viewMode === 'grid'">
<!-- Document Header -->
<div class="flex items-start justify-between mb-4 ml-8">
<div class="doc-type-icon doc-type-lkk">
<span x-text="doc.doc_type || 'DOC'"></span>
</div>
<div class="flex items-center space-x-2">
<button
class="btn-ghost p-2"
:class="{ 'text-agc-primary': isBookmarked(doc.id) }"
@click="toggleBookmark(doc.id)"
:title="isBookmarked(doc.id) ? 'Remove bookmark' : 'Add bookmark'"
>
<svg
class="w-4 h-4"
:fill="isBookmarked(doc.id) ? 'currentColor' : 'none'"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"
/>
</svg>
</button>
<button
class="btn-ghost p-2"
title="More actions"
@click="showDocumentMenu(doc)"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
/>
</svg>
</button>
</div>
</div>
<!-- Document Content -->
<div class="mb-4 ml-8">
<h3
class="text-lg font-semibold text-agc-gray-900 mb-2 line-clamp-2 cursor-pointer hover:text-agc-primary"
x-text="doc.title"
@click="viewDocument(doc)"
></h3>
<p
class="text-agc-gray-600 text-sm mb-3 line-clamp-3"
x-text="utils.truncateText(doc.content, 150)"
></p>
<!-- Document Metadata -->
<div class="space-y-2">
<div
class="flex items-center justify-between text-xs text-agc-gray-500"
>
<span
x-text="'Type: ' + (doc.doc_type || 'Document')"
></span>
<span
x-show="doc.created_at"
x-text="utils.formatDate(doc.created_at)"
></span>
</div>
<!-- Document Stats -->
<div
class="flex items-center space-x-4 text-xs text-agc-gray-500"
>
<span
x-text="doc.content ? (doc.content.length + ' chars') : 'No content'"
></span>
<span
x-show="doc.source"
x-text="'Source: ' + doc.source"
></span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div
class="flex items-center justify-between pt-4 border-t border-agc-gray-100 ml-8"
>
<div class="flex items-center space-x-2">
<span
class="badge"
:class="{
'badge-primary': doc.doc_type === 'LKK',
'badge-success': doc.doc_type === 'Case',
'badge-warning': doc.doc_type === 'Legal',
'badge-error': !doc.doc_type
}"
x-text="doc.doc_type || 'Unknown'"
>
</span>
</div>
<div class="flex items-center space-x-2">
<button
class="btn-secondary text-xs px-3 py-1"
@click="viewDocument(doc)"
>
View Details
</button>
<button
class="btn-primary text-xs px-3 py-1"
@click="openDocument(doc)"
>
Open
</button>
</div>
</div>
</div>
<!-- List View Layout -->
<div
x-show="appState.preferences.viewMode === 'list'"
class="flex items-center space-x-4 ml-8"
>
<div class="doc-type-icon doc-type-lkk flex-shrink-0">
<span x-text="doc.doc_type || 'DOC'"></span>
</div>
<div class="flex-1 min-w-0">
<h3
class="text-base font-semibold text-agc-gray-900 cursor-pointer hover:text-agc-primary truncate"
x-text="doc.title"
@click="viewDocument(doc)"
></h3>
<p
class="text-sm text-agc-gray-600 truncate"
x-text="utils.truncateText(doc.content, 100)"
></p>
</div>
<div
class="flex items-center space-x-4 text-sm text-agc-gray-500 flex-shrink-0"
>
<span x-text="doc.doc_type || 'Unknown'"></span>
<span
x-show="doc.created_at"
x-text="utils.formatDate(doc.created_at)"
></span>
<div class="flex items-center space-x-2">
<button
class="btn-ghost p-1"
:class="{ 'text-agc-primary': isBookmarked(doc.id) }"
@click="toggleBookmark(doc.id)"
>
<svg
class="w-4 h-4"
:fill="isBookmarked(doc.id) ? 'currentColor' : 'none'"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"
/>
</svg>
</button>
<button
class="btn-secondary text-xs px-2 py-1"
@click="viewDocument(doc)"
>
View
</button>
<button
class="btn-primary text-xs px-2 py-1"
@click="openDocument(doc)"
>
Open
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Pagination -->
<div class="mt-8 flex items-center justify-between">
<div class="text-sm text-agc-gray-600">
Showing
<span
x-text="((appState.pagination.currentPage - 1) * appState.preferences.resultsPerPage) + 1"
></span>
to
<span
x-text="Math.min(appState.pagination.currentPage * appState.preferences.resultsPerPage, appState.stats.totalDocuments)"
></span>
of <span x-text="appState.stats.totalDocuments"></span> results
</div>
<div class="flex items-center space-x-2">
<button
class="btn-ghost px-3 py-1"
:disabled="appState.pagination.currentPage <= 1"
@click="goToPage(appState.pagination.currentPage - 1)"
>
Previous
</button>
<template x-for="page in getPaginationPages()" :key="page">
<button
class="px-3 py-1 rounded"
:class="page === appState.pagination.currentPage ? 'bg-agc-primary text-white' : 'text-agc-gray-600 hover:bg-agc-gray-100'"
@click="goToPage(page)"
x-text="page"
></button>
</template>
<button
class="btn-ghost px-3 py-1"
:disabled="appState.pagination.currentPage >= appState.pagination.totalPages"
@click="goToPage(appState.pagination.currentPage + 1)"
>
Next
</button>
</div>
</div>
</div>
<!-- Analytics Tab -->
<div
x-show="appState.activeTab === 'analytics'"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
>
<!-- Analytics Header -->
<div class="agc-card p-6 mb-6">
<div
class="flex flex-col lg:flex-row lg:items-center lg:justify-between"
>
<div>
<h2 class="text-2xl font-bold text-agc-gray-900 mb-2">
Document Analytics
</h2>
<p class="text-agc-gray-600">
Insights and statistics about your document collection
</p>
</div>
<button class="btn-secondary text-sm mt-4 lg:mt-0">
Export Report
</button>
</div>
</div>
<!-- Key Metrics Cards -->
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
>
<div
class="agc-card p-6 bg-gradient-to-br from-blue-50 to-blue-100"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-agc-primary rounded-lg flex items-center justify-center mr-4"
>
<svg
class="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div>
<p class="text-sm text-agc-gray-600">Total Documents</p>
<p
class="text-2xl font-bold"
x-text="appState.stats.totalDocuments"
></p>
</div>
</div>
</div>
<div
class="agc-card p-6 bg-gradient-to-br from-green-50 to-green-100"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-agc-success rounded-lg flex items-center justify-center mr-4"
>
<svg
class="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div>
<p class="text-sm text-agc-gray-600">Total Searches</p>
<p
class="text-2xl font-bold"
x-text="appState.stats.searchCount"
></p>
</div>
</div>
</div>
<div
class="agc-card p-6 bg-gradient-to-br from-amber-50 to-amber-100"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-agc-secondary rounded-lg flex items-center justify-center mr-4"
>
<svg
class="w-6 h-6 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<div>
<p class="text-sm text-agc-gray-600">Document Types</p>
<p
class="text-2xl font-bold"
x-text="appState.documentTypes.length - 1"
></p>
</div>
</div>
</div>
<div
class="agc-card p-6 bg-gradient-to-br from-purple-50 to-purple-100"
>
<div class="flex items-center">
<div
class="w-12 h-12 bg-purple-600 rounded-lg flex items-center justify-center mr-4"
>
<div
class="status-indicator"
:class="appState.apiStatus === 'connected' ? 'status-online' : 'status-offline'"
></div>
</div>
<div>
<p class="text-sm text-agc-gray-600">System Status</p>
<p
class="text-lg font-bold"
:class="appState.apiStatus === 'connected' ? 'text-agc-success' : 'text-agc-error'"
x-text="appState.apiStatus === 'connected' ? 'Online' : 'Offline'"
></p>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="agc-card p-6">
<h3 class="text-lg font-semibold text-agc-gray-900 mb-4">
Document Distribution
</h3>
<div class="chart-container">
<canvas id="documentDistributionChart" x-ref="documentDistributionChart"></canvas>
</div>
<div class="chart-legend">
<div class="chart-legend-item">
<div class="chart-legend-color bg-agc-primary"></div>
<span>LKK Documents</span>
</div>
<div class="chart-legend-item">
<div class="chart-legend-color bg-agc-success"></div>
<span>Legal Cases</span>
</div>
<div class="chart-legend-item">
<div class="chart-legend-color bg-agc-warning"></div>
<span>Criminal Cases</span>
</div>
</div>
</div>
<div class="agc-card p-6">
<h3 class="text-lg font-semibold text-agc-gray-900 mb-4">
Search Activity Trends
</h3>
<div class="chart-container">
<canvas id="searchActivityChart" x-ref="searchActivityChart"></canvas>
</div>
<div class="chart-legend">
<div class="chart-legend-item">
<div class="chart-legend-color bg-blue-500"></div>
<span>Daily Searches</span>
</div>
<div class="chart-legend-item">
<div class="chart-legend-color bg-green-500"></div>
<span>AI Interactions</span>
</div>
</div>
</div>
</div>
<!-- Advanced Analytics -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-8">
<!-- Search Performance -->
<div class="agc-card p-6">
<h4 class="text-lg font-semibold text-agc-gray-900 mb-4">Search Performance</h4>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-sm text-agc-gray-600">Average Response Time</span>
<span class="font-semibold text-agc-gray-900">245ms</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-agc-gray-600">Success Rate</span>
<span class="font-semibold text-agc-success">98.5%</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-agc-gray-600">AI Accuracy</span>
<span class="font-semibold text-agc-primary">94.2%</span>
</div>
<div class="w-full bg-agc-gray-200 rounded-full h-2">
<div class="bg-gradient-to-r from-agc-primary to-blue-500 h-2 rounded-full" style="width: 94.2%"></div>
</div>
</div>
</div>
<!-- Top Search Terms -->
<div class="agc-card p-6">
<h4 class="text-lg font-semibold text-agc-gray-900 mb-4">Popular Search Terms</h4>
<div class="space-y-3">
<template x-for="(term, index) in ['cybercrime', 'intellectual property', 'contract law', 'constitutional rights', 'criminal procedure']" :key="term">
<div class="flex items-center justify-between">
<span class="text-sm text-agc-gray-700" x-text="term"></span>
<div class="flex items-center space-x-2">
<div class="w-16 bg-agc-gray-200 rounded-full h-1.5">
<div class="bg-agc-primary h-1.5 rounded-full" :style="'width: ' + (100 - index * 15) + '%'"></div>
</div>
<span class="text-xs text-agc-gray-500" x-text="(100 - index * 15) + '%'"></span>
</div>
</div>
</template>
</div>
</div>
<!-- User Activity -->
<div class="agc-card p-6">
<h4 class="text-lg font-semibold text-agc-gray-900 mb-4">Recent Activity</h4>
<div class="space-y-3">
<div class="flex items-start space-x-3">
<div class="w-2 h-2 bg-agc-primary rounded-full mt-2"></div>
<div>
<p class="text-sm text-agc-gray-900">New search performed</p>
<p class="text-xs text-agc-gray-500">2 minutes ago</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-2 h-2 bg-agc-success rounded-full mt-2"></div>
<div>
<p class="text-sm text-agc-gray-900">Document bookmarked</p>
<p class="text-xs text-agc-gray-500">15 minutes ago</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-2 h-2 bg-purple-500 rounded-full mt-2"></div>
<div>
<p class="text-sm text-agc-gray-900">AI assistant consulted</p>
<p class="text-xs text-agc-gray-500">1 hour ago</p>
</div>
</div>
<div class="flex items-start space-x-3">
<div class="w-2 h-2 bg-agc-warning rounded-full mt-2"></div>
<div>
<p class="text-sm text-agc-gray-900">Collection created</p>
<p class="text-xs text-agc-gray-500">3 hours ago</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- AI Assistant Panel -->
<div
x-show="appState.aiChat.isOpen"
class="fixed bottom-0 right-0 w-96 bg-white shadow-lg rounded-t-xl border border-agc-gray-200 overflow-hidden"
>
<!-- Chat Header -->
<div class="p-4 border-b border-agc-gray-200 flex justify-between items-center bg-gradient-to-r from-agc-primary to-blue-600">
<h3 class="text-lg font-semibold text-white">AI Assistant</h3>
<button
@click="toggleAIChat()"
class="text-white hover:text-agc-gray-200 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Chat Messages -->
<div class="h-96 overflow-y-auto p-4 space-y-4 bg-agc-gray-50">
<template x-for="message in appState.aiChat.messages" :key="message.timestamp">
<div
:class="message.role === 'user' ? 'ml-auto bg-agc-primary text-white' : 'mr-auto bg-white'"
class="max-w-[80%] rounded-lg p-3 shadow-sm"
>
<p x-text="message.content" class="text-sm"></p>
<span class="text-xs opacity-75 mt-1 block" x-text="new Date(message.timestamp).toLocaleTimeString()"></span>
</div>
</template>
<!-- Typing Indicator -->
<div
x-show="appState.aiChat.isTyping"
class="flex space-x-2 items-center text-agc-gray-500"
>
<span class="w-2 h-2 bg-agc-gray-400 rounded-full animate-pulse"></span>
<span class="w-2 h-2 bg-agc-gray-400 rounded-full animate-pulse delay-100"></span>
<span class="w-2 h-2 bg-agc-gray-400 rounded-full animate-pulse delay-200"></span>
</div>
</div>
<!-- Error Message -->
<div
x-show="appState.aiChat.error"
class="p-2 bg-red-100 text-red-700 text-sm"
x-text="appState.aiChat.error"
></div>
<!-- Chat Input -->
<div class="p-4 border-t border-agc-gray-200">
<form @submit.prevent="sendMessage(appState.aiChat.currentMessage)">
<div class="flex space-x-2">
<input
type="text"
x-model="appState.aiChat.currentMessage"
@input="updateCurrentMessage($event.target.value)"
placeholder="Type your message..."
class="flex-1 px-3 py-2 border border-agc-gray-300 rounded-lg focus:ring-2 focus:ring-agc-primary focus:border-agc-primary"
:disabled="appState.aiChat.isTyping"
>
<button
type="submit"
class="px-4 py-2 bg-agc-primary text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
:disabled="!appState.aiChat.currentMessage.trim() || appState.aiChat.isTyping"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
</button>
</div>
</form>
</div>
</div>
<!-- AI Chat Toggle Button -->
<button
@click="toggleAIChat()"
class="fixed bottom-4 right-4 p-3 bg-agc-primary text-white rounded-full shadow-lg hover:bg-blue-700 transition-colors"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
</svg>
</button>
</div>
<!-- JavaScript Application Logic -->
<script>
// Phase 1: Architecture & Foundation Implementation
// Global Configuration
const CONFIG = {
API_BASE_URL: "http://localhost:8000",
API_TIMEOUT: 10000,
STORAGE_PREFIX: "agc_",
VERSION: "1.0.0",
RETRY_ATTEMPTS: 3,
RETRY_DELAY: 1000,
};
// Utility Functions
const Utils = {
// Debounce function for search inputs
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// Format date for display
formatDate(dateString) {
if (!dateString) return "N/A";
return new Date(dateString).toLocaleDateString("en-MY", {
year: "numeric",
month: "short",
day: "numeric",
});
},
// Generate unique ID
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
},
// Truncate text with ellipsis
truncateText(text, maxLength = 100) {
if (!text || text.length <= maxLength) return text;
return text.substr(0, maxLength) + "...";
},
// Check if device is mobile
isMobile() {
return window.innerWidth <= 768;
},
};
// API Service Class
class AGCApiService {
constructor(baseUrl = CONFIG.API_BASE_URL) {
this.baseUrl = baseUrl;
this.isOnline = navigator.onLine;
this.cache = new Map();
this.cacheExpiry = new Map();
this.pendingRequests = new Map();
this.setupNetworkListeners();
}
setupNetworkListeners() {
window.addEventListener("online", () => {
this.isOnline = true;
console.log("AGC API: Network connection restored");
this.retryFailedRequests();
});
window.addEventListener("offline", () => {
this.isOnline = false;
console.log("AGC API: Network connection lost");
});
}
async retryFailedRequests() {
// Retry any pending requests when connection is restored
if (this._retryQueue && this._retryQueue.length > 0) {
console.log(`AGC API: Retrying ${this._retryQueue.length} failed requests`);
const requests = [...this._retryQueue];
this._retryQueue = [];
for (const req of requests) {
try {
const result = await this.request(req.endpoint, req.options);
req.resolve(result);
} catch (error) {
req.reject(error);
}
}
}
}
async request(endpoint, options = {}) {
const {
method = "GET",
data = null,
headers = {},
timeout = CONFIG.API_TIMEOUT,
retries = CONFIG.RETRY_ATTEMPTS,
cacheDuration = 60000, // Default cache of 1 minute
bypassCache = false,
} = options;
const url = `${this.baseUrl}${endpoint}`;
const cacheKey = this.getCacheKey(url, method, data);
// Check if there's already a pending request for this exact resource
if (this.pendingRequests.has(cacheKey)) {
console.log(`AGC API: Reusing pending request for ${method} ${url}`);
return this.pendingRequests.get(cacheKey);
}
// Return cached response if available and valid
if (
!bypassCache &&
method === "GET" &&
this.cache.has(cacheKey) &&
this.cacheExpiry.has(cacheKey) &&
this.cacheExpiry.get(cacheKey) > Date.now()
) {
console.log(`AGC API: Cache hit for ${method} ${url}`);
return JSON.parse(JSON.stringify(this.cache.get(cacheKey))); // Deep clone to prevent mutation
}
const config = {
method,
headers: {
"Content-Type": "application/json",
...headers,
},
};
// Only set AbortSignal when a timeout is provided
if (timeout) {
config.signal = AbortSignal.timeout(timeout);
}
if (data && method !== "GET") {
config.body = JSON.stringify(data);
}
// Create a promise that will be used for duplicate request coalescing
const requestPromise = new Promise(async (resolve, reject) => {
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
console.log(`AGC API: ${method} ${url} (attempt ${attempt + 1})`);
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.detail ||
`HTTP ${response.status}: ${response.statusText}`
);
}
const result = await response.json();
console.log(`AGC API: Success for ${method} ${url}`);
// Cache successful GET responses
if (method === "GET") {
this.cache.set(cacheKey, result);
this.cacheExpiry.set(cacheKey, Date.now() + cacheDuration);
}
resolve(result);
return;
} catch (error) {
lastError = error;
console.warn(
`AGC API: Attempt ${attempt + 1} failed for ${method} ${url}:`,
error.message
);
if (attempt < retries) {
const delay = CONFIG.RETRY_DELAY * Math.pow(2, attempt); // Exponential backoff
console.log(`AGC API: Waiting ${delay}ms before retry`);
await new Promise((r) => setTimeout(r, delay));
}
}
}
// If we got here, all attempts failed
console.error(
`AGC API: All ${retries + 1} attempts failed for ${method} ${url}:`,
lastError
);
// Queue for retry when online if currently offline
if (!this.isOnline) {
if (!this._retryQueue) this._retryQueue = [];
this._retryQueue.push({
endpoint,
options,
resolve,
reject
});
console.log(`AGC API: Queued request for retry when online: ${method} ${url}`);
} else {
reject(lastError);
}
});
// Store the pending request and clean it up when done
this.pendingRequests.set(cacheKey, requestPromise);
try {
const result = await requestPromise;
return result;
} finally {
this.pendingRequests.delete(cacheKey);
}
}
getCacheKey(url, method, data) {
if (method === "GET") {
return url;
}
return `${method}:${url}:${JSON.stringify(data)}`;
}
invalidateCache(endpoint) {
const prefixToInvalidate = `${this.baseUrl}${endpoint}`;
// Find and remove all matching cache entries
for (const key of this.cache.keys()) {
if (key.startsWith(prefixToInvalidate)) {
this.cache.delete(key);
this.cacheExpiry.delete(key);
}
}
}
// API Endpoints
async getRoot() {
return this.request("/");
}
async getDocuments(filters = {}, bypassCache = false) {
const params = new URLSearchParams();
if (filters.doc_type && filters.doc_type !== "All Types") {
params.append("doc_type", filters.doc_type);
}
if (filters.title_filter) {
params.append("title_filter", filters.title_filter);
}
const queryString = params.toString() ? `?${params.toString()}` : "";
return this.request(`/documents${queryString}`, {
bypassCache,
cacheDuration: 300000 // 5 minutes cache for documents list
});
}
async getDocument(id) {
return this.request(`/documents/${id}`, {
cacheDuration: 600000 // 10 minutes cache for document details
});
}
async searchDocuments(query, profileSearch = false) {
return this.request("/search", {
method: "POST",
data: { query, profile_search: profileSearch },
bypassCache: true // Always bypass cache for searches
});
}
async getDocumentTypes() {
return this.request("/document-types", {
cacheDuration: 3600000 // 1 hour cache for document types
});
}
async checkHealth() {
try {
await this.request("/", {
timeout: 5000, // Short timeout for health check
bypassCache: true
});
return "connected";
} catch (error) {
return "disconnected";
}
}
}
// State Management Class
class AGCStateManager {
constructor() {
this.version = "1.0"; // State version for compatibility checks
this.state = this.getInitialState();
this.listeners = [];
this.setupStorageListener();
this.setupThemeDetection();
}
getInitialState() {
const savedState = this.loadFromStorage();
// Deep merge of saved state with default state
return {
// App State
isInitializing: true,
activeTab: savedState.activeTab || "search",
isOnline: navigator.onLine,
apiStatus: "connecting",
// AI Chat State
aiChat: {
isTyping: false,
currentMessage: "",
messages: savedState.aiChat?.messages || [],
recentConcepts: savedState.aiChat?.recentConcepts || [],
suggestions: [],
isOpen: false,
error: null
},
// Search State
searchQuery: "",
searchResults: null,
searchHistory: savedState.searchHistory || [],
selectedDocuments: [],
documents: [],
documentTypes: ["All Types"],
selectedDocument: null,
bookmarks: savedState.bookmarks || [],
// Loading States
loading: {
search: false,
documents: false,
export: false,
},
// UI State
ui: {
viewMode: savedState.ui?.viewMode || "grid",
resultsPerPage: savedState.ui?.resultsPerPage || 20,
sidebar: {
collapsed: savedState.ui?.sidebar?.collapsed || false,
width: savedState.ui?.sidebar?.width || 280,
},
...savedState.ui,
},
// Data State
stats: {
totalDocuments: 0,
searchCount: savedState.stats?.searchCount || 0,
lastSearchTime: savedState.stats?.lastSearchTime || 0,
lastUpdated: null,
},
// Error Handling
error: {
show: false,
message: "",
type: "error",
},
// Tabs Configuration
tabs: [
{ id: "search", name: "Search", badge: null },
{ id: "browse", name: "Browse", badge: null },
{ id: "analytics", name: "Analytics", badge: null },
],
// User Preferences
preferences: {
theme: this.detectPreferredTheme(savedState),
fontSize: savedState.preferences?.fontSize || "medium",
language: savedState.preferences?.language || "en",
saveSearchHistory: savedState.preferences?.saveSearchHistory !== false, // Default to true
enableAIFeatures: savedState.preferences?.enableAIFeatures !== false, // Default to true
...savedState.preferences,
},
// Pagination
pagination: {
currentPage: 1,
totalPages: 1,
pageSize: savedState.pagination?.pageSize || 20,
},
// Filters
filters: {
doc_type: savedState.filters?.doc_type || "All Types",
title_filter: savedState.filters?.title_filter || "",
dateRange: savedState.filters?.dateRange || "",
sortBy: savedState.filters?.sortBy || "created_at",
sortDirection: savedState.filters?.sortDirection || "desc",
},
};
}
detectPreferredTheme(savedState) {
// First check saved preference
if (savedState.preferences?.theme) {
return savedState.preferences.theme;
}
// Then check system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
// Default to light
return 'light';
}
setupThemeDetection() {
// Listen for system theme changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
// Only update if user hasn't explicitly set a preference
if (!this.state.preferences.hasExplicitTheme) {
this.setState({
preferences: {
...this.state.preferences,
theme: e.matches ? 'dark' : 'light'
}
});
}
});
}
}
setState(updates) {
const oldState = JSON.parse(JSON.stringify(this.state)); // Deep copy for comparison
// Deep merge updates with current state
this.state = this.deepMerge(this.state, updates);
// Save critical state to localStorage
this.saveToStorage();
// Notify listeners
this.listeners.forEach((listener) => {
try {
listener(this.state, oldState);
} catch (error) {
console.error("State listener error:", error);
}
});
}
// Deep merge objects helper
deepMerge(target, source) {
const output = Object.assign({}, target);
if (this.isObject(target) && this.isObject(source)) {
Object.keys(source).forEach(key => {
if (this.isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] });
} else {
output[key] = this.deepMerge(target[key], source[key]);
}
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
saveToStorage() {
try {
const persistentState = {
version: this.version,
timestamp: Date.now(),
activeTab: this.state.activeTab,
searchHistory: this.state.preferences.saveSearchHistory ? this.state.searchHistory.slice(-20) : [], // Save last 20 searches if enabled
aiChat: {
messages: this.state.aiChat.messages.slice(-100), // Save last 100 messages
recentConcepts: this.state.aiChat.recentConcepts
},
bookmarks: this.state.bookmarks,
stats: {
searchCount: this.state.stats.searchCount,
lastSearchTime: this.state.stats.lastSearchTime
},
preferences: this.state.preferences,
ui: {
viewMode: this.state.ui.viewMode,
resultsPerPage: this.state.ui.resultsPerPage,
sidebar: {
collapsed: this.state.ui.sidebar?.collapsed,
width: this.state.ui.sidebar?.width
}
},
pagination: {
pageSize: this.state.pagination.pageSize
},
filters: this.state.filters
};
// Compress the state to reduce storage size
const stateStr = JSON.stringify(persistentState);
localStorage.setItem(
CONFIG.STORAGE_PREFIX + "state",
stateStr
);
// Also keep a backup in session storage in case local storage fails
sessionStorage.setItem(
CONFIG.STORAGE_PREFIX + "state_backup",
stateStr
);
return true;
} catch (error) {
console.warn("Failed to save state to localStorage:", error);
return false;
}
}
loadFromStorage() {
try {
// Try getting from localStorage first
let saved = localStorage.getItem(CONFIG.STORAGE_PREFIX + "state");
// If not available, try from sessionStorage
if (!saved) {
saved = sessionStorage.getItem(CONFIG.STORAGE_PREFIX + "state_backup");
if (saved) {
console.log("Restored state from session storage backup");
}
}
if (!saved) {
return {};
}
const parsed = JSON.parse(saved);
// Check version compatibility for potential migrations
if (parsed.version !== this.version) {
console.log(`State version mismatch: saved=${parsed.version}, current=${this.version}`);
// Future: Migration logic could go here
}
return parsed;
} catch (error) {
console.warn("Failed to load state from storage:", error);
return {};
}
}
setupStorageListener() {
window.addEventListener("storage", (e) => {
if (e.key === CONFIG.STORAGE_PREFIX + "state") {
// Another tab updated the state
try {
const savedState = JSON.parse(e.newValue);
// Don't apply changes originating from this instance
if (savedState.timestamp !== this.state.timestamp) {
console.log("Syncing state from another tab");
this.setState(savedState);
}
} catch (error) {
console.warn("Failed to parse state from storage event:", error);
}
}
});
}
// Convenience methods for common state updates
setLoading(key, value) {
this.setState({
loading: { ...this.state.loading, [key]: value },
});
}
setError(message, type = "error") {
this.setState({
error: { show: true, message, type },
});
// Auto-clear error after 5 seconds
setTimeout(() => {
// Only clear if it's the same error (prevent clearing new errors)
if (this.state.error.message === message) {
this.setState({
error: { show: false, message: "", type: "error" },
});
}
}, 5000);
}
clearError() {
this.setState({
error: { show: false, message: "", type: "error" },
});
}
// Reset application state for logged out users or "forget me" feature
resetState(preservePreferences = true) {
const preferences = preservePreferences ? this.state.preferences : {};
localStorage.removeItem(CONFIG.STORAGE_PREFIX + "state");
sessionStorage.removeItem(CONFIG.STORAGE_PREFIX + "state_backup");
this.state = this.getInitialState();
if (preservePreferences) {
this.setState({ preferences });
}
return true;
}
}
// Main Alpine.js Application
function agcApp() {
return {
appState: null,
stateManager: null,
apiService: null,
// Initialize the application
async initializeApp() {
// Initialize services
this.apiService = new AGCApiService();
this.stateManager = new AGCStateManager();
this.appState = this.stateManager.state;
// Check API connection
const apiStatus = await this.apiService.checkHealth();
this.stateManager.setState({ apiStatus });
// Load initial data
await this.loadInitialData();
// Initialize charts
this.initializeCharts();
// Mark initialization as complete
this.stateManager.setState({ isInitializing: false });
},
// Load initial application data
async loadInitialData() {
try {
// Load document types (simulated for now)
const documentTypes = ["All Types", "LKK", "Case", "Legal"];
this.stateManager.setState({ documentTypes });
// Load initial documents
await this.loadDocuments(this.appState.filters);
// Set initial stats
this.stateManager.setState({
stats: {
...this.appState.stats,
totalDocuments: 156, // Simulated count until API is connected
lastUpdated: new Date().toISOString()
}
});
} catch (error) {
console.error("Failed to load initial data:", error);
this.stateManager.setState({
error: {
show: true,
message: "Failed to load initial data. Please refresh the page.",
type: "error"
}
});
}
},
// Load documents based on filters
async loadDocuments(filters = {}) {
this.stateManager.setLoading("documents", true);
try {
// For demo purposes, generating mock documents
// In a real implementation, this would fetch from the API
const mockDocuments = Array.from({ length: 20 }, (_, i) => ({
id: `doc-${i}`,
title: `Sample Document ${i + 1}`,
content: `This is a sample document with some content for demonstration purposes. It contains information relevant to the Attorney General's Chambers.`,
doc_type: ["LKK", "Case", "Legal"][Math.floor(Math.random() * 3)],
created_at: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
source: "AGC Database"
}));
this.stateManager.setState({ documents: mockDocuments });
// Set pagination
this.stateManager.setState({
pagination: {
...this.appState.pagination,
currentPage: 1,
totalPages: 5 // Simulated for demo
}
});
return mockDocuments;
} catch (error) {
console.error("Failed to load documents:", error);
this.stateManager.setError("Failed to load documents. Please try again.");
return [];
} finally {
this.stateManager.setLoading("documents", false);
}
},
// Perform search with query
async performSearch(query) {
if (!query.trim()) return;
this.stateManager.setLoading("search", true);
try {
// For demo purposes, generating mock search results
// In a real implementation, this would fetch from the API
const mockDocuments = Array.from({ length: 5 }, (_, i) => ({
id: `result-${i}`,
title: `Result for "${query}" ${i + 1}`,
content: `This document contains information related to "${query}" and is highly relevant to your search query.`,
content_preview: `...information related to <span class="search-highlight">${query}</span> and is highly relevant...`,
doc_type: ["LKK", "Case", "Legal"][Math.floor(Math.random() * 3)],
created_at: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
similarity: Math.random() * 0.5 + 0.5, // Random score between 0.5 and 1.0
source: "AGC Database"
}));
// Update search results
this.stateManager.setState({
searchResults: {
query: query,
enhanced_query: `Advanced search for documents containing "${query}" with relevant legal context`,
documents: mockDocuments,
answer: `Based on the available documents, "${query}" appears in several legal contexts. The most relevant documents are from recent cases where this topic was discussed in detail.`
}
});
// Add to search history if enabled
if (this.appState.preferences.saveSearchHistory) {
const history = [
{
id: Utils.generateId(),
query: query,
timestamp: new Date().toISOString(),
count: mockDocuments.length
},
...this.appState.searchHistory
].slice(0, 20); // Keep last 20 searches
this.stateManager.setState({
searchHistory: history,
stats: {
...this.appState.stats,
searchCount: this.appState.stats.searchCount + 1,
lastSearchTime: Date.now()
}
});
}
return mockDocuments;
} catch (error) {
console.error("Search failed:", error);
this.stateManager.setError("Search failed. Please try again.");
return [];
} finally {
this.stateManager.setLoading("search", false);
}
},
// View a document's details
viewDocument(doc) {
if (!doc) return;
this.stateManager.setState({ selectedDocument: doc });
// In a real implementation, this would open a modal or navigate to detail page
console.log("Viewing document:", doc.title);
alert(`Viewing document: ${doc.title}`);
},
// Open the full document
openDocument(doc) {
if (!doc) return;
// In a real implementation, this would open the document in a viewer
console.log("Opening document:", doc.title);
alert(`Opening document: ${doc.title} (ID: ${doc.id})`);
},
// Toggle document bookmark status
toggleBookmark(docId) {
if (!docId) return;
const bookmarks = [...this.appState.bookmarks];
const index = bookmarks.indexOf(docId);
if (index === -1) {
bookmarks.push(docId);
} else {
bookmarks.splice(index, 1);
}
this.stateManager.setState({ bookmarks });
},
// Check if document is bookmarked
isBookmarked(docId) {
return this.appState.bookmarks.includes(docId);
},
// Toggle document selection for bulk actions
toggleDocumentSelection(docId) {
if (!docId) return;
const selectedDocuments = [...this.appState.selectedDocuments];
const index = selectedDocuments.indexOf(docId);
if (index === -1) {
selectedDocuments.push(docId);
} else {
selectedDocuments.splice(index, 1);
}
this.stateManager.setState({ selectedDocuments });
},
// Check if document is selected
isDocumentSelected(docId) {
return this.appState.selectedDocuments.includes(docId);
},
// Clear all selected documents
clearSelection() {
this.stateManager.setState({ selectedDocuments: [] });
},
// Export selected documents or search results
exportResults() {
alert("Export functionality would be implemented here");
},
// Get document type CSS class
getDocTypeClass(type) {
switch(type) {
case 'LKK': return 'doc-type-lkk';
case 'Case': return 'doc-type-case';
case 'Legal': return 'doc-type-legal';
default: return 'doc-type-default';
}
},
// Format document type for display
formatDocumentType(type) {
return type || "DOC";
},
// Highlight search terms in text
highlightSearchTerms(text, query) {
if (!text || !query) return text;
// Simple implementation - in a real app, use a more robust approach
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return text.replace(regex, '<span class="search-highlight">$1</span>');
},
// Switch between tabs
switchTab(tabId) {
if (this.appState.activeTab === tabId) return;
this.stateManager.setState({ activeTab: tabId });
},
// Set view mode (grid or list)
setViewMode(mode) {
if (mode !== 'grid' && mode !== 'list') return;
this.stateManager.setState({
preferences: {
...this.appState.preferences,
viewMode: mode
}
});
},
// Check if any filters are active
hasActiveFilters() {
return (
this.appState.filters.title_filter ||
this.appState.filters.doc_type !== 'All Types' ||
this.appState.filters.dateRange
);
},
// Clear all filters
clearAllFilters() {
this.stateManager.setState({
filters: {
doc_type: 'All Types',
title_filter: '',
dateRange: '',
sortBy: 'created_at',
sortDirection: 'desc'
}
});
this.loadDocuments(this.appState.filters);
},
// Get number of displayed documents
getDisplayedCount() {
return this.appState.documents.length;
},
// Get pagination pages for display
getPaginationPages() {
const totalPages = this.appState.pagination.totalPages;
const currentPage = this.appState.pagination.currentPage;
if (totalPages <= 5) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
// Show current page and pages around it
if (currentPage <= 3) {
return [1, 2, 3, 4, 5];
} else if (currentPage >= totalPages - 2) {
return [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
} else {
return [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2];
}
},
// Go to specific page
goToPage(page) {
if (page < 1 || page > this.appState.pagination.totalPages) return;
this.stateManager.setState({
pagination: {
...this.appState.pagination,
currentPage: page
}
});
this.loadDocuments(this.appState.filters);
},
// Clear search history
clearSearchHistory() {
this.stateManager.setState({ searchHistory: [] });
},
// Debounced filter function for search input
debouncedFilter: Utils.debounce(function() {
this.loadDocuments(this.appState.filters);
}, 300),
// Ask AI a specific question
askAI(question) {
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
currentMessage: question,
isOpen: true
}
});
this.sendAIMessage();
},
// Send message to AI
sendAIMessage() {
this.sendMessage(this.appState.aiChat.currentMessage);
},
// Format AI message with markdown or HTML
formatAIMessage(message) {
// Simple implementation - in a real app, use a proper markdown parser
return message;
},
// Generate document summary
generateDocumentSummary() {
this.askAI("Summarize the documents in my current search results");
},
// Analyze legal trends
analyzeLegalTrends() {
this.askAI("What are the trends in the legal documents I'm viewing?");
},
// Clear AI chat
clearAIChat() {
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
messages: [],
error: null
}
});
},
// Initialize charts for analytics
initializeCharts() {
// Only initialize when the DOM elements exist and charts are needed
this.$watch('appState.activeTab', (tab) => {
if (tab === 'analytics') {
this.$nextTick(() => {
this.renderDocumentDistributionChart();
this.renderSearchActivityChart();
});
}
});
},
// Render document distribution chart
renderDocumentDistributionChart() {
const chartElement = this.$refs.documentDistributionChart;
if (!chartElement) return;
// Cleanup existing chart if it exists
if (this.documentDistributionChart) {
this.documentDistributionChart.destroy();
}
// Prepare data
const docTypes = ['LKK', 'Case', 'Legal', 'Other'];
const data = [42, 65, 29, 20]; // Mock data
// Create chart
this.documentDistributionChart = new Chart(chartElement, {
type: 'doughnut',
data: {
labels: docTypes,
datasets: [{
data: data,
backgroundColor: [
'#1E40AF', // Primary
'#059669', // Success
'#D97706', // Warning
'#6B7280' // Gray
],
borderColor: '#FFFFFF',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(255, 255, 255, 0.8)',
titleColor: '#1F2937',
bodyColor: '#1F2937',
borderColor: '#E5E7EB',
borderWidth: 1,
cornerRadius: 8,
boxPadding: 6,
usePointStyle: true,
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((acc, val) => acc + val, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '60%',
animation: {
animateScale: true,
animateRotate: true
}
}
});
},
// Render search activity chart
renderSearchActivityChart() {
const chartElement = this.$refs.searchActivityChart;
if (!chartElement) return;
// Cleanup existing chart if it exists
if (this.searchActivityChart) {
this.searchActivityChart.destroy();
}
// Generate mock data for the last 7 days
const labels = Array.from({ length: 7 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() - (6 - i));
return date.toLocaleDateString('en-MY', { month: 'short', day: 'numeric' });
});
const searchData = [12, 19, 15, 23, 28, 25, 32]; // Mock data
const aiData = [5, 8, 10, 14, 16, 19, 23]; // Mock data
// Create chart
this.searchActivityChart = new Chart(chartElement, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Daily Searches',
data: searchData,
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 3,
tension: 0.3,
fill: true
},
{
label: 'AI Interactions',
data: aiData,
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 3,
tension: 0.3,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: 'rgba(255, 255, 255, 0.8)',
titleColor: '#1F2937',
bodyColor: '#1F2937',
borderColor: '#E5E7EB',
borderWidth: 1,
cornerRadius: 8,
boxPadding: 6
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(226, 232, 240, 0.5)'
}
},
x: {
grid: {
display: false
}
}
}
}
});
},
// AI Chat Methods
toggleAIChat() {
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
isOpen: !this.appState.aiChat.isOpen
}
});
},
async sendMessage(message) {
if (!message.trim()) return;
// Add user message
const messages = [...this.appState.aiChat.messages];
messages.push({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
// Update state with user message and typing status
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
messages,
currentMessage: '',
isTyping: true,
error: null
}
});
try {
// Simulate AI response (replace with actual API call)
await new Promise(resolve => setTimeout(resolve, 1000));
// Add AI response
const updatedMessages = [...messages];
updatedMessages.push({
role: 'assistant',
content: 'This is a simulated response. Replace with actual API integration.',
timestamp: new Date().toISOString()
});
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
messages: updatedMessages,
isTyping: false
}
});
} catch (error) {
console.error('AI Chat Error:', error);
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
isTyping: false,
error: 'Failed to get response from AI'
}
});
}
},
updateCurrentMessage(message) {
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
currentMessage: message
}
});
},
clearChat() {
this.stateManager.setState({
aiChat: {
...this.appState.aiChat,
messages: [],
error: null
}
});
},
// ... existing code ...
};
}
// Global error handler
window.addEventListener("error", (event) => {
console.error("Global error:", event.error);
});
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
});
// Performance monitoring
window.addEventListener("load", () => {
console.log("AGC Document Search: Page loaded");
if (window.performance) {
const loadTime =
window.performance.timing.loadEventEnd -
window.performance.timing.navigationStart;
console.log(`AGC Document Search: Load time: ${loadTime}ms`);
}
});
console.log(
"AGC Document Search: Scripts loaded, waiting for Alpine.js initialization..."
);
</script>
</body>
</html>