3571 lines
144 KiB
HTML
3571 lines
144 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 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>
|