From b84bdf17f0407adec66368104ee3ebadaa7cde6c Mon Sep 17 00:00:00 2001 From: ammarhamzi2019278344 Date: Wed, 4 Jun 2025 06:36:03 +0800 Subject: [PATCH] add ui interface utilizing fast api --- README.md | 383 ++- agc-document-search.html | 3570 +++++++++++++++++++++++++++ api.py | 166 ++ embedding/enhanced_rag_service.py | 160 +- embedding/rag_service.py | 100 - embedding/simple_search_service.py | 121 + frontend/assets/images/agc-logo.png | Bin 0 -> 40232 bytes frontend/assets/images/favicon.ico | Bin 0 -> 62 bytes frontend/components.html | 885 +++++++ frontend/css/animations.css | 286 +++ frontend/css/styles.css | 1094 ++++++++ frontend/favicon.ico | Bin 0 -> 62 bytes frontend/index.html | 2350 ++++++++++++++++++ frontend/index.html.bak | 1822 ++++++++++++++ frontend/js/api.js | 118 + frontend/js/main.js | 1820 ++++++++++++++ frontend/js/main.js.backup | 1315 ++++++++++ frontend/js/main.js.bak | 1315 ++++++++++ plan.md | 298 +++ serve.py | 180 ++ templates/old_index.html | 1067 ++++++++ test_api.py | 139 ++ 22 files changed, 16954 insertions(+), 235 deletions(-) create mode 100644 agc-document-search.html create mode 100644 api.py delete mode 100644 embedding/rag_service.py create mode 100644 embedding/simple_search_service.py create mode 100644 frontend/assets/images/agc-logo.png create mode 100644 frontend/assets/images/favicon.ico create mode 100644 frontend/components.html create mode 100644 frontend/css/animations.css create mode 100644 frontend/css/styles.css create mode 100644 frontend/favicon.ico create mode 100644 frontend/index.html create mode 100644 frontend/index.html.bak create mode 100644 frontend/js/api.js create mode 100644 frontend/js/main.js create mode 100644 frontend/js/main.js.backup create mode 100644 frontend/js/main.js.bak create mode 100644 plan.md create mode 100644 serve.py create mode 100644 templates/old_index.html create mode 100644 test_api.py diff --git a/README.md b/README.md index db52861..c63eff1 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,335 @@ -# AGC Document Chatbot +# AGC Document Search System -A Streamlit-based web application that provides intelligent search and chat capabilities for Attorney General's Chambers (AGC) documents. The system uses Retrieval-Augmented Generation (RAG) to enhance search accuracy and provide context-aware responses. +A comprehensive AI-powered document search and chat system for the Attorney General's Chambers (AGC) of Malaysia. This system provides intelligent document discovery, natural language search capabilities, and an AI assistant for legal research and analysis. -## Features +## ๐Ÿ—๏ธ Project Overview -- **Document Browsing**: Browse through all available AGC documents with filtering by document type and title/content search -- **Enhanced RAG Search**: Search documents using AI-enhanced query understanding -- **Document Detail View**: View full document details with contextual information -- **Chat Interface**: Chat with AI about document content with context-awareness +The AGC Document Search System is designed for Malaysian government legal staff, prosecutors, and legal researchers to efficiently search, browse, and interact with legal documents including: -## Architecture +- **LKK (Laporan Keputusan Kes)** - Case Decision Reports +- **Legal Cases** - Various legal precedents and case law +- **Criminal Cases** - Criminal law documents and decisions +- **Government Legal Documents** - Various AGC documents and legal resources -The application consists of several key components: +## ๐Ÿš€ Key Features -- **Web Interface**: Built with Streamlit -- **Document Database**: MySQL database for storing document metadata and content -- **Embedding Services**: Vector embeddings for semantic search capabilities -- **RAG Enhancement**: Improved search using OpenAI's capabilities +### ๐Ÿ” Advanced Search Capabilities -## Project Structure +- **AI-Enhanced Search**: OpenAI-powered semantic search with query enhancement +- **Natural Language Processing**: Ask questions in plain language +- **Relevancy Scoring**: Documents ranked by similarity and relevance +- **Query Enhancement**: AI automatically improves search queries for better results +- **Search History**: Track and revisit previous searches + +### ๐Ÿ“š Document Management + +- **Smart Document Browser**: Grid and list view options with advanced filtering +- **Document Types Filtering**: Filter by Legal, Criminal, LKK, and other types +- **Metadata Display**: Comprehensive document information and classification +- **Document Viewer**: Rich document display with structured content presentation +- **Bookmark System**: Save and organize important documents + +### ๐Ÿค– AI Assistant + +- **Legal Chat Interface**: Interactive AI assistant for legal queries +- **Document Analysis**: AI-powered document summaries and key point extraction +- **Legal Concept Explanations**: Get definitions and explanations of legal terms +- **Cross-Reference Analysis**: Find related documents and cases +- **Precedent Search**: Locate relevant legal precedents + +### ๐ŸŽจ Modern User Interface + +- **Responsive Design**: Optimized for desktop, tablet, and mobile devices +- **Government-Appropriate Styling**: Professional navy and gold color scheme +- **Accessibility Compliant**: WCAG 2.1 AA standards +- **Smooth Animations**: Modern micro-interactions and transitions +- **Inter Font Family**: Modern, readable typography + +## ๐Ÿ›๏ธ System Architecture ``` -. -โ”œโ”€โ”€ app.py # Main Streamlit application -โ”œโ”€โ”€ config.py # Configuration settings -โ”œโ”€โ”€ db/ # Database utilities -โ”‚ โ””โ”€โ”€ import_lkk_data.py # Script for importing LKK data -โ”œโ”€โ”€ embedding/ # Embedding and RAG services -โ”‚ โ”œโ”€โ”€ embedding_service.py -โ”‚ โ”œโ”€โ”€ enhanced_rag_service.py -โ”‚ โ””โ”€โ”€ rag_service.py -โ”œโ”€โ”€ utils/ # Utility functions -โ”œโ”€โ”€ Data/ # Document data -โ””โ”€โ”€ requirements.txt # Python dependencies +AGC Document Search System +โ”œโ”€โ”€ Frontend (Web Interface) +โ”‚ โ”œโ”€โ”€ HTML5 with modern CSS3 +โ”‚ โ”œโ”€โ”€ Vanilla JavaScript (ES6+) +โ”‚ โ””โ”€โ”€ Responsive design with animations +โ”œโ”€โ”€ Backend API (FastAPI) +โ”‚ โ”œโ”€โ”€ RESTful API endpoints +โ”‚ โ”œโ”€โ”€ Document retrieval and search +โ”‚ โ””โ”€โ”€ AI integration layer +โ”œโ”€โ”€ Database (MySQL) +โ”‚ โ”œโ”€โ”€ Document storage and metadata +โ”‚ โ”œโ”€โ”€ Search history and logs +โ”‚ โ””โ”€โ”€ Vector embeddings for AI search +โ”œโ”€โ”€ AI/ML Components +โ”‚ โ”œโ”€โ”€ OpenAI integration for enhanced search +โ”‚ โ”œโ”€โ”€ Text embeddings and similarity matching +โ”‚ โ””โ”€โ”€ Fallback simple search service +โ””โ”€โ”€ Additional Interfaces + โ”œโ”€โ”€ Streamlit dashboard (app.py) + โ””โ”€โ”€ Direct API testing tools ``` -## Installation - -1. Clone the repository -2. Create a virtual environment: - ``` - python -m venv venv - source venv/bin/activate # On Windows: venv\Scripts\activate - ``` -3. Install dependencies: - ``` - pip install -r requirements.txt - ``` -4. Configure environment variables (create a `.env` file based on requirements) - -## Database Setup - -1. Install and run XAMPP: - - - Download XAMPP from [https://www.apachefriends.org/](https://www.apachefriends.org/) - - Install and launch XAMPP Control Panel - - Start the Apache and MySQL services - - Access phpMyAdmin at [http://localhost/phpmyadmin](http://localhost/phpmyadmin) - - Create a new database named `agc` - -2. Configure the database connection in your `.env` file: - ``` - MYSQL_HOST=localhost - MYSQL_USER=root - MYSQL_PASSWORD= - MYSQL_DATABASE=agc - ``` - -## Data Import (Optional) - -To import LKK (Laporan Keputusan Kes) data into the system: +## ๐Ÿ“ Project Structure ``` -python -m db.import_lkk_data +agc-chatbot/ +โ”œโ”€โ”€ ๐Ÿ“„ Frontend +โ”‚ โ”œโ”€โ”€ index.html # Main web interface +โ”‚ โ”œโ”€โ”€ js/ +โ”‚ โ”‚ โ”œโ”€โ”€ main.js # Core application logic +โ”‚ โ”‚ โ””โ”€โ”€ api.js # API service layer +โ”‚ โ”œโ”€โ”€ css/ # Styling (embedded in HTML) +โ”‚ โ””โ”€โ”€ assets/ # Images, fonts, static files +โ”œโ”€โ”€ ๐Ÿ”ง Backend API +โ”‚ โ”œโ”€โ”€ api.py # FastAPI REST API +โ”‚ โ”œโ”€โ”€ serve.py # Alternative server implementation +โ”‚ โ””โ”€โ”€ config.py # Configuration settings +โ”œโ”€โ”€ ๐Ÿ—„๏ธ Database +โ”‚ โ”œโ”€โ”€ db/ +โ”‚ โ”‚ โ”œโ”€โ”€ db_utils.py # Database utilities and connections +โ”‚ โ”‚ โ”œโ”€โ”€ schema.sql # Database schema definition +โ”‚ โ”‚ โ””โ”€โ”€ import_lkk_data.py # Data import utilities +โ”œโ”€โ”€ ๐Ÿค– AI/ML Components +โ”‚ โ”œโ”€โ”€ embedding/ +โ”‚ โ”‚ โ”œโ”€โ”€ enhanced_rag_service.py # OpenAI-powered search +โ”‚ โ”‚ โ””โ”€โ”€ simple_search_service.py # Fallback keyword search +โ”‚ โ””โ”€โ”€ utils/ +โ”‚ โ””โ”€โ”€ text_processing.py # Text utilities +โ”œโ”€โ”€ ๐Ÿ“Š Streamlit Interface +โ”‚ โ””โ”€โ”€ app.py # Alternative Streamlit dashboard +โ”œโ”€โ”€ ๐Ÿ“ Data +โ”‚ โ””โ”€โ”€ [Various legal document folders organized by type] +โ”œโ”€โ”€ ๐Ÿ“‹ Documentation +โ”‚ โ”œโ”€โ”€ plan.md # Comprehensive project planning +โ”‚ โ””โ”€โ”€ templates/ # Template files and examples +โ””โ”€โ”€ โš™๏ธ Configuration + โ”œโ”€โ”€ requirements.txt # Python dependencies + โ”œโ”€โ”€ .gitignore # Git ignore rules + โ””โ”€โ”€ test_api.py # API testing utilities ``` -This script will: +## ๐Ÿ› ๏ธ Installation & Setup -- Set up required database tables -- Import available data from SQL or PDF files in the Data directory -- Generate document embeddings for search functionality +### Prerequisites -## Usage +- **Python 3.8+** +- **MySQL Database** +- **OpenAI API Key** (optional, for enhanced AI features) +- **Modern Web Browser** -Run the Streamlit application: +### 1. Clone the Repository +```bash +git clone https://github.com/your-repo/agc-chatbot.git +cd agc-chatbot ``` + +### 2. Install Python Dependencies + +```bash +# Create virtual environment (recommended) +python -m venv venv +source venv/bin/activate # Linux/Mac +# or +venv\Scripts\activate # Windows + +# Install dependencies +pip install -r requirements.txt +``` + +### 3. Database Setup + +```bash +# Create MySQL database +mysql -u root -p < db/schema.sql + +# Import sample data (optional) +python db/import_lkk_data.py +``` + +### 4. Environment Configuration + +Create a `.env` file in the root directory: + +```env +# Database Configuration +MYSQL_HOST=localhost +MYSQL_USER=root +MYSQL_PASSWORD=your_password +MYSQL_DATABASE=agc +MYSQL_PORT=3306 + +# OpenAI Configuration (optional) +OPENAI_API_KEY=your_openai_api_key +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 +OPENAI_CHAT_MODEL=gpt-3.5-turbo + +# Application Settings +MAX_SEARCH_RESULTS=10 +SIMILARITY_THRESHOLD=0.7 +``` + +### 5. Start the Backend API + +```bash +# Method 1: Using FastAPI directly +uvicorn api:app --reload --host 0.0.0.0 --port 8000 + +# Method 2: Using the custom server +python serve.py + +# Method 3: Using the Streamlit interface streamlit run app.py ``` -The application will be available at http://localhost:8501 by default. +### 6. Access the Frontend -## Requirements +- **Web Interface**: Open `frontend/index.html` in a web browser +- **API Documentation**: Visit `http://localhost:8000/docs` +- **Streamlit Dashboard**: Visit `http://localhost:8501` (if using Streamlit) -- Python 3.7+ -- MySQL database (via XAMPP) -- OpenAI API key (for embedding and RAG features) +## ๐Ÿ”Œ API Endpoints + +### Document Management + +- `GET /documents` - List all documents with optional filtering +- `GET /documents/{id}` - Get specific document by ID +- `GET /document-types` - Get available document types + +### Search + +- `POST /search` - Perform AI-enhanced or simple search +- `GET /ping` - Health check endpoint + +### Example API Usage + +```javascript +// Search for documents +const searchResults = await fetch("http://localhost:8000/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: "cross-border financial fraud investigations", + profile_search: false, + }), +}); + +// Get document by ID +const document = await fetch("http://localhost:8000/documents/1"); +``` + +## ๐ŸŽฏ Usage Guide + +### Web Interface + +1. **Browse Documents**: Use the "Browse Documents" tab to explore all available documents with filtering options +2. **Search**: Use the "Search" tab for AI-powered document search with natural language queries +3. **AI Assistant**: Use the "AI Assistant" tab to chat with the AI about legal concepts and documents + +### Search Features + +- **Natural Language**: "Find cases about money laundering in Malaysia" +- **Specific Terms**: "AMLA 2001 cross-border investigations" +- **Question Format**: "What is the procedure for international evidence collection?" + +### AI Assistant Tools + +- **Analyze Document**: Get detailed analysis of legal documents +- **Summarize**: Create concise summaries of lengthy documents +- **Legal Concepts**: Explain complex legal terminology +- **Cross Reference**: Find related documents and precedents +- **Case Analysis**: Comprehensive case law analysis + +## ๐Ÿ”ง Technical Details + +### Frontend Technologies + +- **HTML5** with semantic markup +- **CSS3** with modern features (Grid, Flexbox, Custom Properties) +- **Vanilla JavaScript** (ES6+) with modular architecture +- **Font Awesome** icons +- **Inter** font family for modern typography + +### Backend Technologies + +- **FastAPI** - Modern Python web framework +- **Pydantic** - Data validation and serialization +- **MySQL** - Relational database for document storage +- **OpenAI API** - For enhanced search and AI chat features +- **LangChain** - For AI/ML pipeline management + +### AI/ML Features + +- **Text Embeddings** - Vector representations for semantic search +- **Similarity Matching** - Cosine similarity for relevance scoring +- **Query Enhancement** - AI-powered query improvement +- **RAG (Retrieval-Augmented Generation)** - Context-aware AI responses + +## ๐Ÿงช Testing + +```bash +# Test API endpoints +python test_api.py + +# Manual testing using the web interface +# Open frontend/index.html in a browser + +# API documentation and testing +# Visit http://localhost:8000/docs +``` + +## ๐Ÿš€ Deployment + +### Production Deployment + +1. **Environment Setup**: + + - Configure production database + - Set up environment variables + - Configure CORS settings for production domains + +2. **Backend Deployment**: + + ```bash + # Using gunicorn for production + gunicorn -w 4 -k uvicorn.workers.UvicornWorker api:app + ``` + +3. **Frontend Deployment**: + + - Serve static files through a web server (nginx, Apache) + - Update API base URL in `frontend/js/api.js` + +4. **Database**: + - Set up production MySQL instance + - Run migrations and import data + - Configure backup strategies + +## ๐Ÿ” Security Considerations + +- **API Security**: Implement authentication and authorization +- **Database Security**: Use secure connections and proper credentials +- **Environment Variables**: Keep sensitive data in environment files +- **CORS Configuration**: Restrict to allowed domains in production +- **Input Validation**: All user inputs are validated and sanitized + +## ๐Ÿค Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/new-feature`) +3. Commit your changes (`git commit -am 'Add new feature'`) +4. Push to the branch (`git push origin feature/new-feature`) +5. Create a Pull Request + +## ๐Ÿ“„ License + +This project is developed for the Attorney General's Chambers of Malaysia. All rights reserved. + +## ๐Ÿ“ž Support + +For technical support or questions about the AGC Document Search System, please contact the development team or refer to the project documentation. + +--- + +**Built with โค๏ธ for the Attorney General's Chambers of Malaysia** diff --git a/agc-document-search.html b/agc-document-search.html new file mode 100644 index 0000000..9efe058 --- /dev/null +++ b/agc-document-search.html @@ -0,0 +1,3570 @@ + + + + + + AGC Document Search - Attorney General's Chambers + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+

+ AGC Document Search +

+

Initializing system...

+
+
+ + +
+
+
+ +
+
+
+ + + +
+
+
+

+ AGC Document Search +

+

+ Attorney General's Chambers Malaysia +

+
+
+ + +
+ +
+
+
+
+

+

+
+
+
+ + +
+
+
+
+ + + Documents + +
+
+
+
+ + + Searches + +
+
+
+ + + +
+
+
+ + +
+
+ + docs + + + searches + + + + +
+
+
+ + +
+ +
+
+ +
+
+ + +
+ +
+ +
+
+

+ AI-Powered Legal Document Search +

+

+ Intelligent search through Attorney General's Chambers legal + documents using advanced AI technology +

+
+ + +
+
+
+ + + +
+ + + +
+ +
+
+
+ + +
+

+ Quick Searches +

+
+ +
+
+
+ + +
+ +
+
+

+ Search Results +

+
+ + + results found + + +
+
+ + +
+
+
+ + + +
+
+

+ AI Analysis +

+

+
+ + +
+
+
+
+ + +
+
+ + + + Enhanced Query +
+

+
+
+ + +
+
+
+ + +
+
+ Sort by: + +
+
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ + +
+
+
+
+
+ + + +
+
+

AI Legal Assistant

+

Ask questions about legal concepts, document analysis, or get explanations

+
+
+ +
+
+ AI Ready +
+
+ + +
+
+
+
+ + + +
+

Start a conversation

+

Ask me anything about legal documents, case analysis, or legal concepts.

+ + +
+ + + + +
+
+
+ + +
+ +
+ + +
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+

Legal Concept Explorer

+ + +
+
Recently Explained
+
+ +
+
+ + +
+
Common Legal Terms
+
+ +
+
+ + +
+
Quick Actions
+
+ + + +
+
+
+
+
+ + +
+
+

Recent Searches

+ +
+ +
+ +
+
+
+ + +
+ +
+
+
+

+ Document Library +

+

+ Browse and filter through all available legal documents +

+
+ + +
+ +
+ + + selected + + + +
+ + +
+ View: +
+ + +
+
+
+
+ + +
+ +
+ +
+ +
+ + + +
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+
+ Active filters: + + + + +
+
+ + +
+ + Showing of + documents + +
+ Items per page: + +
+
+
+ + +
+ +
+ + +
+
+ Showing + + to + + of results +
+ +
+ + + + + +
+
+
+ + +
+ +
+
+
+

+ Document Analytics +

+

+ Insights and statistics about your document collection +

+
+ +
+
+ + +
+
+
+
+ + + +
+
+

Total Documents

+

+
+
+
+ +
+
+
+ + + +
+
+

Total Searches

+

+
+
+
+ +
+
+
+ + + +
+
+

Document Types

+

+
+
+
+ +
+
+
+
+
+
+

System Status

+

+
+
+
+
+ + +
+
+

+ Document Distribution +

+
+ +
+
+
+
+ LKK Documents +
+
+
+ Legal Cases +
+
+
+ Criminal Cases +
+
+
+ +
+

+ Search Activity Trends +

+
+ +
+
+
+
+ Daily Searches +
+
+
+ AI Interactions +
+
+
+
+ + +
+ +
+

Search Performance

+
+
+ Average Response Time + 245ms +
+
+ Success Rate + 98.5% +
+
+ AI Accuracy + 94.2% +
+
+
+
+
+
+ + +
+

Popular Search Terms

+
+ +
+
+ + +
+

Recent Activity

+
+
+
+
+

New search performed

+

2 minutes ago

+
+
+
+
+
+

Document bookmarked

+

15 minutes ago

+
+
+
+
+
+

AI assistant consulted

+

1 hour ago

+
+
+
+
+
+

Collection created

+

3 hours ago

+
+
+
+
+
+
+ + +
+ +
+

AI Assistant

+ +
+ + +
+ + + +
+ + + +
+
+ + +
+ + +
+
+
+ + +
+
+
+
+ + + +
+ + + + + diff --git a/api.py b/api.py new file mode 100644 index 0000000..86948e0 --- /dev/null +++ b/api.py @@ -0,0 +1,166 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, field_serializer +from typing import List, Optional, Dict, Any +from datetime import datetime +from db.db_utils import get_all_documents, get_document +import uvicorn +import os + +app = FastAPI( + title="AGC Document Chatbot API", + description="API for Attorney General's Chambers Document Search and Chat System", + version="1.0.0" +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins + allow_credentials=True, + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers +) + +# Pydantic models for request/response +class DocumentResponse(BaseModel): + id: int + title: str + content: str + doc_type: str + created_at: Optional[datetime] = None + source: Optional[str] = None + + # Serialize datetime to string + @field_serializer('created_at') + def serialize_created_at(self, value: Optional[datetime]) -> Optional[str]: + if value is None: + return None + return value.strftime("%Y-%m-%d %H:%M:%S") + +class SearchRequest(BaseModel): + query: str + profile_search: bool = False + +class SearchResponse(BaseModel): + query: str + enhanced_query: str + documents: List[Dict[str, Any]] + answer: str + +# Routes +@app.get("/") +async def root(): + return {"message": "Welcome to AGC Document Chatbot API"} + +@app.get("/ping") +async def ping(): + """Health check endpoint""" + return {"status": "ok", "message": "API is running"} + +@app.get("/documents", response_model=List[DocumentResponse]) +async def list_documents( + doc_type: Optional[str] = None, + title_filter: Optional[str] = None +): + """Get all documents with optional filtering""" + try: + documents = get_all_documents() + + # Apply filters + if doc_type and doc_type != "All Types": + documents = [doc for doc in documents if doc.get('doc_type') == doc_type] + + if title_filter: + documents = [ + doc for doc in documents + if title_filter.lower() in doc.get('title', '').lower() or + title_filter.lower() in doc.get('content', '').lower() + ] + + return documents + except Exception as e: + print(f"Error in list_documents: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error fetching documents: {str(e)}") + +@app.get("/documents/{document_id}", response_model=DocumentResponse) +async def get_document_by_id(document_id: int): + """Get a specific document by ID""" + try: + document = get_document(document_id) + if not document: + raise HTTPException(status_code=404, detail="Document not found") + return document + except HTTPException: + raise + except Exception as e: + print(f"Error in get_document_by_id: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error fetching document: {str(e)}") + +@app.get("/document/{document_id}", response_model=DocumentResponse) +async def get_document_by_id_alt(document_id: str): + """Alternative endpoint to get a specific document by ID (supports string IDs)""" + try: + # If the document_id starts with 'doc', extract the numeric part + if document_id.startswith('doc'): + try: + numeric_id = int(document_id[3:]) + document = get_document(numeric_id) + if document: + return document + except ValueError: + pass # Not a numeric ID after 'doc', continue with normal lookup + + # Try to convert the entire ID to an integer + try: + numeric_id = int(document_id) + document = get_document(numeric_id) + if document: + return document + except ValueError: + pass # Not a numeric ID, continue with not found + + raise HTTPException(status_code=404, detail="Document not found") + except HTTPException: + raise + except Exception as e: + print(f"Error in get_document_by_id_alt: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error fetching document: {str(e)}") + +@app.post("/search", response_model=SearchResponse) +async def search_documents(request: SearchRequest): + """Search documents using enhanced RAG or simple search as fallback""" + try: + # Check if OpenAI API key is available + if os.getenv('OPENAI_API_KEY'): + print("Using OpenAI-enhanced search") + try: + from embedding.enhanced_rag_service import enhanced_rag_search + results = enhanced_rag_search(request.query, request.profile_search) + return results + except Exception as e: + print(f"OpenAI search failed: {e}, falling back to simple search") + + # Fallback to simple search + print("Using simple keyword search") + from embedding.simple_search_service import simple_search + results = simple_search(request.query, request.profile_search) + return results + + except Exception as e: + print(f"Error in search_documents: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error performing search: {str(e)}") + +@app.get("/document-types") +async def get_document_types(): + """Get list of available document types""" + try: + documents = get_all_documents() + doc_types = list(set([doc.get('doc_type', 'Unknown') for doc in documents])) + return {"document_types": sorted(doc_types)} + except Exception as e: + print(f"Error in get_document_types: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error fetching document types: {str(e)}") + +if __name__ == "__main__": + uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/embedding/enhanced_rag_service.py b/embedding/enhanced_rag_service.py index 2d3a764..b2674df 100644 --- a/embedding/enhanced_rag_service.py +++ b/embedding/enhanced_rag_service.py @@ -3,47 +3,54 @@ import json import numpy as np from typing import List, Dict, Any from db.db_utils import get_all_documents, get_document -import openai +from openai import OpenAI from dotenv import load_dotenv # Load environment variables load_dotenv() -# Configure OpenAI -openai.api_key = os.getenv('OPENAI_API_KEY') +# Configure OpenAI client +client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) EMBEDDING_MODEL = "text-embedding-ada-002" CHAT_MODEL = "gpt-3.5-turbo" def generate_embedding(text: str) -> List[float]: """Generate embedding for the given text using OpenAI's API""" - response = openai.Embedding.create( - model=EMBEDDING_MODEL, - input=text - ) - return response['data'][0]['embedding'] + try: + response = client.embeddings.create( + model=EMBEDDING_MODEL, + input=text + ) + return response.data[0].embedding + except Exception as e: + print(f"Error generating embedding: {e}") + # Return a dummy embedding of appropriate size if API fails + return [0.0] * 1536 def cosine_similarity(a: List[float], b: List[float]) -> float: """Calculate cosine similarity between two vectors""" a = np.array(a) b = np.array(b) - return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) + norm_a = np.linalg.norm(a) + norm_b = np.linalg.norm(b) + if norm_a == 0 or norm_b == 0: + return 0.0 + return np.dot(a, b) / (norm_a * norm_b) def enhance_search_query(query: str) -> str: """Enhance the search query using OpenAI's chat model""" try: - messages = [ - {"role": "system", "content": "You are a legal search expert. Your task is to enhance the given search query to improve search results in a legal document database. Keep the enhanced query concise and focused on the key legal concepts and facts."}, - {"role": "user", "content": f"Please enhance this search query for searching in legal documents: {query}"} - ] - - response = openai.ChatCompletion.create( + response = client.chat.completions.create( model=CHAT_MODEL, - messages=messages, + messages=[ + {"role": "system", "content": "You are a legal search expert. Your task is to enhance the given search query to improve search results in a legal document database. Keep the enhanced query concise and focused on the key legal concepts and facts."}, + {"role": "user", "content": f"Please enhance this search query for searching in legal documents: {query}"} + ], temperature=0.3, max_tokens=100 ) - enhanced_query = response['choices'][0]['message']['content'].strip() + enhanced_query = response.choices[0].message.content.strip() return enhanced_query except Exception as e: print(f"Error enhancing query: {e}") @@ -51,60 +58,69 @@ def enhance_search_query(query: str) -> str: def get_relevant_documents(query: str, documents: List[Dict[str, Any]], top_k: int = 5) -> List[Dict[str, Any]]: """Get the most relevant documents for the query""" - # Generate embedding for the query - query_embedding = generate_embedding(query) - - # Calculate similarities with all documents - results = [] - for doc in documents: - try: - doc_embedding = json.loads(doc['embedding']) - similarity = cosine_similarity(query_embedding, doc_embedding) - - # Create a preview of the content - content = doc['content'] - content_preview = content[:300] + "..." if len(content) > 300 else content - - results.append({ - 'id': doc['id'], - 'title': doc['title'], - 'content': doc['content'], - 'content_preview': content_preview, - 'doc_type': doc['doc_type'], - 'similarity': similarity - }) - except Exception as e: - print(f"Error processing document {doc.get('id')}: {e}") - continue - - # Sort by similarity and return top_k results - results.sort(key=lambda x: x['similarity'], reverse=True) - return results[:top_k] + try: + # Generate embedding for the query + query_embedding = generate_embedding(query) + + # Calculate similarities with all documents + results = [] + for doc in documents: + try: + if doc.get('embedding'): + doc_embedding = json.loads(doc['embedding']) + similarity = cosine_similarity(query_embedding, doc_embedding) + + # Create a preview of the content + content = doc.get('content', '') + content_preview = content[:300] + "..." if len(content) > 300 else content + + results.append({ + 'id': doc['id'], + 'title': doc.get('title', 'Untitled'), + 'content': content, + 'content_preview': content_preview, + 'doc_type': doc.get('doc_type', 'Unknown'), + 'similarity': similarity + }) + except Exception as e: + print(f"Error processing document {doc.get('id')}: {e}") + continue + + # Sort by similarity and return top_k results + results.sort(key=lambda x: x['similarity'], reverse=True) + return results[:top_k] + + except Exception as e: + print(f"Error in get_relevant_documents: {e}") + return [] def generate_answer(query: str, relevant_docs: List[Dict[str, Any]]) -> str: """Generate an answer based on the query and relevant documents""" - # Prepare context from relevant documents - context = "\n\n".join([ - f"Document {i+1}:\nTitle: {doc['title']}\nContent: {doc['content']}" - for i, doc in enumerate(relevant_docs) - ]) + if not relevant_docs: + return "I couldn't find any relevant documents to answer your question. Please try rephrasing your query." try: - messages = [ - {"role": "system", "content": """You are a legal assistant helping to answer questions about legal cases. - Use the provided document context to answer questions accurately and professionally. - If the information is not available in the context, say so clearly."""}, - {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}\n\nPlease provide a clear and accurate answer based on the above context:"} - ] + # Prepare context from relevant documents (limit context size) + context_parts = [] + for i, doc in enumerate(relevant_docs[:3]): # Limit to top 3 documents + content = doc.get('content', '')[:500] # Limit content length + context_parts.append(f"Document {i+1}:\nTitle: {doc.get('title', 'Untitled')}\nContent: {content}") - response = openai.ChatCompletion.create( + context = "\n\n".join(context_parts) + + response = client.chat.completions.create( model=CHAT_MODEL, - messages=messages, + messages=[ + {"role": "system", "content": """You are a legal assistant helping to answer questions about legal cases. + Use the provided document context to answer questions accurately and professionally. + If the information is not available in the context, say so clearly."""}, + {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}\n\nPlease provide a clear and accurate answer based on the above context:"} + ], temperature=0.5, max_tokens=500 ) - return response['choices'][0]['message']['content'].strip() + return response.choices[0].message.content.strip() except Exception as e: print(f"Error generating answer: {e}") return "I apologize, but I encountered an error while generating the answer. Please try rephrasing your question." @@ -121,17 +137,37 @@ def enhanced_rag_search(query: str, profile_search: bool = False) -> Dict[str, A Dict containing search results and generated answer """ try: + print(f"Processing search query: {query}") + + # Check if OpenAI API key is available + if not os.getenv('OPENAI_API_KEY'): + print("OpenAI API key not found") + return { + "query": query, + "enhanced_query": query, + "documents": [], + "answer": "OpenAI API key is not configured. Please set the OPENAI_API_KEY environment variable." + } + # Enhance the query enhanced_query = enhance_search_query(query) + print(f"Enhanced query: {enhanced_query}") # Get all documents with their embeddings documents = get_all_documents(include_embeddings=True) + print(f"Found {len(documents)} documents") + + # Filter documents that have embeddings + docs_with_embeddings = [doc for doc in documents if doc.get('embedding')] + print(f"Documents with embeddings: {len(docs_with_embeddings)}") # Get relevant documents - relevant_docs = get_relevant_documents(enhanced_query, documents) + relevant_docs = get_relevant_documents(enhanced_query, docs_with_embeddings) + print(f"Found {len(relevant_docs)} relevant documents") # Generate answer answer = generate_answer(query, relevant_docs) + print(f"Generated answer: {answer[:100]}...") return { "query": query, @@ -145,5 +181,5 @@ def enhanced_rag_search(query: str, profile_search: bool = False) -> Dict[str, A "query": query, "enhanced_query": query, "documents": [], - "answer": "I apologize, but I encountered an error while processing your query. Please try again." + "answer": f"I apologize, but I encountered an error while processing your query: {str(e)}. Please try again." } \ No newline at end of file diff --git a/embedding/rag_service.py b/embedding/rag_service.py deleted file mode 100644 index a53b6bd..0000000 --- a/embedding/rag_service.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -import requests -from embedding.embedding_service_http import generate_embedding, search_similar_documents -from db.db_utils import get_all_embeddings, log_search -from config import OPENAI_API_KEY, OPENAI_CHAT_MODEL, MAX_SEARCH_RESULTS, SIMILARITY_THRESHOLD - -def rag_search(query): - """ - Perform RAG (Retrieval Augmented Generation) search: - 1. Generate embedding for the query - 2. Find similar documents - 3. Generate an answer based on retrieved documents - """ - # Generate embedding for the query - query_embedding = generate_embedding(query) - - # Get all document embeddings from the database - document_embeddings = get_all_embeddings() - - # Find similar documents - similar_docs = search_similar_documents( - query_embedding, - document_embeddings, - top_k=MAX_SEARCH_RESULTS, - threshold=SIMILARITY_THRESHOLD - ) - - # Extract document contents for context - contexts = [] - result_docs = [] - - print("Debug - Similar docs:", [(doc['document_id'], doc['title']) for doc, _ in similar_docs]) - - for doc, similarity in similar_docs: - # Add document to contexts for the LLM - contexts.append(f"Document Title: {doc['title']}\nContent: {doc['content']}\nSource: {doc['source'] or 'Unknown'}\nRelevance: {similarity:.4f}") - - # Add document to results for display - ensure we use the right ID - result_docs.append({ - "id": doc['document_id'], # This should be the ID from documents table - "title": doc['title'], - "content_preview": doc['content'][:200] + "..." if len(doc['content']) > 200 else doc['content'], - "source": doc['source'], - "similarity": float(similarity) - }) - - # Generate answer using LLM with retrieved contexts - if contexts: - context_text = "\n\n---\n\n".join(contexts) - - # Prepare the system message - system_message = """ - You are a helpful assistant that answers questions based on the provided document contexts. - If the context doesn't contain relevant information, say so clearly. - If the information is incomplete, explain what's missing. - Always cite your sources when providing information. - Format your answers to be clear and concise. - Always answer in formal Bahasa Malaysia. - """ - - # Use direct HTTP request to OpenAI API instead of client library - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {OPENAI_API_KEY}" - } - - payload = { - "model": OPENAI_CHAT_MODEL, - "messages": [ - {"role": "system", "content": system_message}, - {"role": "user", "content": f"Context documents:\n\n{context_text}\n\nBased on these documents, please answer the following query: {query}"} - ], - "temperature": 0.5 - } - - print(f"Requesting chat completion using model: {OPENAI_CHAT_MODEL}") - - response = requests.post( - "https://api.openai.com/v1/chat/completions", - headers=headers, - json=payload - ) - - if response.status_code != 200: - raise Exception(f"Error from OpenAI API: {response.text}") - - result = response.json() - answer = result["choices"][0]["message"]["content"] - else: - answer = "I couldn't find any relevant documents to answer your question." - - # Log the search - search_results = { - "query": query, - "documents": result_docs, - "answer": answer - } - log_search(query, search_results) - - return search_results \ No newline at end of file diff --git a/embedding/simple_search_service.py b/embedding/simple_search_service.py new file mode 100644 index 0000000..805b2a1 --- /dev/null +++ b/embedding/simple_search_service.py @@ -0,0 +1,121 @@ +import json +import re +from typing import List, Dict, Any +from db.db_utils import get_all_documents + +def simple_keyword_search(query: str, documents: List[Dict[str, Any]], top_k: int = 5) -> List[Dict[str, Any]]: + """Simple keyword-based search without AI""" + query_words = query.lower().split() + + results = [] + for doc in documents: + score = 0 + content = doc.get('content', '').lower() + title = doc.get('title', '').lower() + + # Count keyword matches + for word in query_words: + score += content.count(word) * 1 # Content match worth 1 point + score += title.count(word) * 2 # Title match worth 2 points + + if score > 0: + content_preview = doc.get('content', '')[:300] + "..." if len(doc.get('content', '')) > 300 else doc.get('content', '') + + results.append({ + 'id': doc['id'], + 'title': doc.get('title', 'Untitled'), + 'content': doc.get('content', ''), + 'content_preview': content_preview, + 'doc_type': doc.get('doc_type', 'Unknown'), + 'similarity': score / 100.0 # Normalize score + }) + + # Sort by score and return top results + results.sort(key=lambda x: x['similarity'], reverse=True) + return results[:top_k] + +def generate_simple_answer(query: str, relevant_docs: List[Dict[str, Any]]) -> str: + """Generate a simple answer based on keyword matching""" + if not relevant_docs: + return "I couldn't find any relevant documents to answer your question. Please try rephrasing your query." + + # Find the most relevant document + best_doc = relevant_docs[0] + + # Extract relevant sentences containing query keywords + query_words = query.lower().split() + content = best_doc.get('content', '') + sentences = re.split(r'[.!?]+', content) + + relevant_sentences = [] + for sentence in sentences: + sentence_clean = sentence.strip() + if any(word in sentence_clean.lower() for word in query_words): + relevant_sentences.append(sentence_clean) + if len(relevant_sentences) >= 3: # Limit to 3 sentences + break + + if relevant_sentences: + answer = f"Based on the document '{best_doc.get('title', 'Untitled')}', here's what I found:\n\n" + answer += ". ".join(relevant_sentences[:2]) + "." + + if len(relevant_docs) > 1: + answer += f"\n\nI found {len(relevant_docs)} relevant documents in total." + + return answer + else: + return f"I found relevant documents but couldn't extract specific information about '{query}'. You may want to review the document '{best_doc.get('title', 'Untitled')}' for more details." + +def simple_search(query: str, profile_search: bool = False) -> Dict[str, Any]: + """ + Perform simple keyword-based search without AI + + Args: + query: The search query + profile_search: Whether to search in user profiles (not used currently) + + Returns: + Dict containing search results and generated answer + """ + try: + print(f"Processing simple search query: {query}") + + # Get all documents + documents = get_all_documents(include_embeddings=False) + print(f"Found {len(documents)} documents") + + # Enhanced query (simple expansion) + enhanced_query = query + if len(query.split()) == 1: + # Add common legal terms for single word queries + legal_expansions = { + 'seksyen': 'seksyen section akta', + 'jenayah': 'jenayah criminal crime', + 'hukuman': 'hukuman punishment penalty', + 'kesalahan': 'kesalahan offense offence', + 'mahkamah': 'mahkamah court tribunal' + } + enhanced_query = legal_expansions.get(query.lower(), query) + + # Get relevant documents + relevant_docs = simple_keyword_search(enhanced_query, documents) + print(f"Found {len(relevant_docs)} relevant documents") + + # Generate answer + answer = generate_simple_answer(query, relevant_docs) + print(f"Generated answer: {answer[:100]}...") + + return { + "query": query, + "enhanced_query": enhanced_query, + "documents": relevant_docs, + "answer": answer + } + except Exception as e: + print(f"Error in simple search: {e}") + return { + "query": query, + "enhanced_query": query, + "documents": [], + "answer": f"I apologize, but I encountered an error while processing your query: {str(e)}. Please try again." + } \ No newline at end of file diff --git a/frontend/assets/images/agc-logo.png b/frontend/assets/images/agc-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6a86b6dbd00589328c7fe3f971cc45795013d715 GIT binary patch literal 40232 zcmXtAb97u^w4RA=+qSL7#>9;pqiJlju^Tr@V>C7z+iYxSlE%iH-+SxLT6f*Kf82ZS z*=OV1dw=IfsjJGNA`u}0002}4d1+0^xdQ+IN+7^N{_WSJ!az=cZ}qN#W%3&C1rnlETf~$&x}+U7ZsEK*-!vRGU;{9aGj}U>H*ypHMi^1VEc( zvtcV?D-}vZLqj7&BXIC=aPYw-%Stp0f*582uo)TI#lV6#{rXJC@6)gVwRT(^V%FtY z0DuCZAT0^<$^qwk`h8x^eCR&fv?Yr3X>u5CX2EaUh8lu{h9fax2o9UkR9M5=q;-(u zv!qN+bik96M_^PMiAZG01t{?@2E`Fys5sU;tR%-&ZI*1@ygOgyQdP$BOonY8QBUNy z9Za-!zpNjLXkTLP!ZA7n8-kF7M|-665z5GRXZm}{@?rJRtAI<$y*mSO_j%}4i_iju zl(aj8OwA;B5txEb1M9=Pp?h`+5ipwr)k0jEm93c$W0$%(vIA zpiUXh#i%@lTz+Y2+HrfXT`P=Vh7yVP0iZ##l4AxwK7I#(>Pg*A39{{3>?sXK3A%7R zz8Iso*{^eNh}0hE_IfHO+1yG3?i*Nt-?4)_9#Y`v-&p8t!*}ES+MW&uTXB6J9=Rp3 zF|awZ>@n|ok;uhxi@$FPSiZtsfe%hqhMt0bhs}WAzU#Xxk;=+aTxsm>AD_0odc5(@ zc!j4y>%qky?MI+GhFbWIDG@Yy6YqL(_m$=ILVv5X%KrXRN*bJ-k%GHldd5@ z{jE8i35TB^hSVnz>S26+E8T$vk%Tn>5W87*u^&Q5;sLPtd0~*vU2chdu?(kdB z_iR(2LFWGcfJgT21S!)ZyV&v`aom;Sec5eb&-qR7+Bcn>GZWRl#TJnuiWeFU{Rm`o z#hmHfoujiYp&s79U22YG0OM_iZDc9Gk$_u@$5P6UR1p+EV16a``(w|^`g14~jZkKW z*atdgY8#fs%+HAYi0eBm3C4AZ3G`Jp3b7(yb!QvJ(K`;e$!%qxGR2Lb2ScW z=mbsztG>FX@ybz)qyBVkCZ)XvJck;>eUXEcLSJMSSTh+|B^$m`@A*woTN^n@S%knQ zlWv0?kucd08l;rLEQ+F9L3Pe05LbnGByOk2cK4}dOb&Z`&}_F zTIAD<{y4CZ5a#>i=FjT6)twpAhZm%}-NDcoSSC_UPE@ZnxaS(Y1t@K7>D?kX?rHfW zdRKHEMn|mJXaCtszLlf^puD_m z4-Ey3I4}tPf+i@!=jQG1>Px_Vir~UIF|<00A9<@(fV{<^f|>_oIa~k|%l&=dobJ^A zkvQ3O0pHuFBTN+;dm$xvR2gA1!Xmna%W9pwBjNR+S!_a=aMXj z9!%YV`N^nj=j53FFf%}QqDb`C(c67)=iLA<(bRkwr@tHC^l^-2yJb#uV!y4vUpxez z0r?zqhNlC$qWFLKOW)2?5RYE?yq zD9q4V~P&Q3u@wlPf6%R1EWL=A@R$F$zL^ z0U64~)rVNOviiB%@e&6Wo}`G5z* z@rRe>EkB1s6JiJZN9#lCu}WS-X_|$&qvV7`=1W_y9+9&%^#~PRgme)$6%JZuLsF8! zO2Np!KHJ|uT!lVdw0JWKY&+-h3c3MeosC#UxHi-xZ+tvRPKlwp4mVl~LWx@yHcofN zaUz#9M2d=Q5{pGuSRse0Xl$}MAc%^Y09#<7YH!o@KYI*1aADdixyhtjnKCocvEt=O z(qYfkrI3aFeVYC4+jbYpJTl4)SVHPOVIM>(!jy?a*!Zq>gHTdoJN)oNX>QPo86csOE|D`uVZ zV{JwQN4gL?Z)Bx^4=oO=?l?b3H zN&eg_IJB#8A0Ei*MjnDkxt^P4(|-FSm_GxaZH;};wcr1J7{jnpWK(G#pgYK!&)!mt zw17pCV%h=s1qgZfXr(E^U=!dwV0kmZ73nC@K{6|ofHok0zMuKC>tyNE<1qWds(sDi zfy>|>lVP*NPEFt&bNADoKTJqzk_;`>%M3gn;QV;~Zg1oDa+n$8RQ5G4g~D6dUtk`6 z0;-T5YA=B#VvaLUx<8cu>mg`&PoS%_Pq+IS_s><_H!jbM^)RFJktAx7`(^Xj_lJiD zUr#Z8WmUu-);Isb>$IFU8}uOZ#|n&B-jmQMpsjt@4=jGBi((OippSNY&crlXX?`i` z?z8H=>#l!q!mfuC3dgugii(e~N8;0--7o%h$^y#p9qQU&l9?OAp0;GHCx4v~06ez0 ziY+uX;jyUaet^rIf^LB4aeph?{(0^>70rh5NC-pTQH9?>W;*a48KkXEDZQ(Zo|iYh zsB1F$&>cvemL_L=1Sr?5s(37Pcz=0_`JGS^^e?M`#5SstB%}P#I-K%K19K?3pC{$Z zsgbSby%fbsaSX5DiIVQksl9w?v@wOKQ+E&g3 z^tX)}uvXtfch%<_F44YVf&+5`ySbPCusKX^-haXEVlMzE>l!4*gx>_rW9VUfl7K!zhFRp^QA@m;39M@gd*G(jdLS0 z*jk5`mDlHB7LuYEe~{@mJfHI@TMVUYC>@UP14`n1yZ zL?<^#60Ib3MXlhxPgM$3e!5nVcI|RBy#76r*GT9F!&kp zXG&YwKbm`5gnO}H5XirA`vE|hQ)b!>C!>khSlX>HuvmY>~Y1NBWZ)xq~+;Cc{$lc(BpM_ zckJm~wvYv-_o+8JFYi18)7}hjPqN!(RY@vXa~CLvio-)PwsU|#ZmU`;Q6tZzsiPz7 z?d@Fw&qZ-KnGMf?ZCjDQ$tdKxl{6jUo+9ZCu1tL9A}Rj?)(U1Pf&*_FjRtOHf08x8 zGJ=Y97RXVE3M6E#YEezZ)3mLfl&W_ujhxILo-OxTX93`4Z(gNRT3YbC-?0O{hvN@! z`Wj?{m?y^-OK+BYII1Mt7Q_;DK>Oc5xYJix8JC8)0GjRF3dI(AaO(speHXcVtq<>h z>$W(HkC?KN4m8B}4SEd~sqr4Dse|Wzpwo+TnwNBdnxVZ|!dn-PQ;EXsc#pg_E*yf4 zaYwf{jyR&|A?+DlBy4ctM%^kAbVWY z>-ll0Nh*;T9C~-w;_~QZm&*rT@cTC^K-fIdHos4=R!1=JWgVt-Gm-P~5EM&`Ywz5% zVtAA^Uox7aUIP=(m9JL)a3W8=(x?dNUah-I8KEIB+u!sJ9DTk!OJY2ZURzokstLGbe!ohv9^FQobJg&n z;z|;^JW}*oRGLMCQ%1s4^=QxsF9wwch<9EaBi^TEfw6+Km8}{aR)k;Q{smf3{}fdq zmDJV6M;Cjf@Z9p}4{@zcNm)2cfD^)Ux_;%xu1c8tkz8akK=Y|SFNU!7V*>jIAlh}x z9E$)aM7I8U>g{1`nmR$Guleg*GSyCYR+iUp6usB=D1FeAg@$UyCm-|3-oOu!lBT3^ z7als2QHmE_31?6I&f7U@0o(RfR8-=&*0!9Ei}o1bi?(db@gKCeKB%auBDXW2{GP^5 z4nxu1>N8`i7DB>W z+EU`2GfoBE!RUOVqwwTal;Yh%&U-_HO@zUJ&nzdictu_>`(O9z#eq{)F;OLjXZut_ z42WmYipX37N1dVh6~vAmH>w`A&+SnIRHh<5*AH|JHAy^7J~-mbBfb5l%griIZ+Cxh zdZ5uS*IlP)7Z-U?UB9ijCW*hDsI3RQ1UgNNK5TD)sV^6>jY7jap&8#~9Ig!Mk5+dN z#flCaG|fCc3zyJ0Vs+Tik$;^>>O*xKM9d47iQu5c`l}0~PGPayDNbH<>_BZH{`1f! zoFV2fbaOaqK3BB!_vGLCx#iRKfdgc?H0G@%@((XO0aEn8`EeD&de0N-sLM?oy=O}F zPAl}M4m~7{jF=(Rh;0;6tntGAuX}F?iw=6WF)7c#95d|tmOmx#d7aS6^Uol=NtX4L zVq0FhB}(E6J=s`L)@C~1d2K33^+X}Vt5z2jc7zLGw1(E#3FdS>?kQ(-rMJqa-DR+}zXH1(8QIN^G5QP5196E^UgK!vh}G;SAW z1B@j2E6ifu#o+C_j%h+2cIpe+YlF&6U}DdlNa z@Cz&8)jdtAUJ}-1HRAaF`a?)cBcWjgQT)Stv~`UXoShLhE3$u`3DtC7Be}b;Mcuor z(uHz4#7g#u4^W5|r61ZDS!jjq`=F6uUmX_`tD}CHE>)G|;Qh!yGvk#;h-+@PvU~Vi za-xU90AI4R`j74p)>!GOqPL#;z(lSe4F^!Y|AM5thZ{Zf_H*%B)K zM0$)SNh-C|faRa62c?a7k$AG;T`Wb7!FpA;`t0VrHi?V)uR_`U6%kE!{94!uJSaZL z#dzZOpSB>Ve`VdNpY*d+FDwWYJIsL+hC#m{WpF6bk5KRTg!D7;T`=Hf4gT1yrEOT6 zD%8OjGmc1H}1H7R=&o!hM-uP zqNh2Lv^BO+>bFQ8o%7SVo?ClBEB{Egqg>|PO+gC@Be0otT-&0fmmOm}{|vRvOfH@Vg(`+5SB4YQ(G6L-HnwqZ4<9*oCwoH#)yo-Hz#P5&msG zFpeY#bXT6cxz(6(Yp~SD(=MnOqzq0NfMppch@EkCBkA7{110cfEc$l# zlkmUUN$&l9@DaTDBk=C-4q`vH3)1k#)M~jxX&wLm5a&$~&lM}RU9_JuFs#e)C(KTn z*p`;7wK}l4^hQH57Ns9p)oHNa=1StjZ4B(O?heZd3)>u-L3K_{WbU=yObOfC*qAdD|VZ#{2b<3I`3HSvc`xTkB)`r-H-KTlq4zy~Qv@H*DdOu9pB1Y*$rmZrL*Mx22G)ARs&n67F86SO{w zRz7+_uR3n@Q3CMtj54n@r6yW9ek5En*(;17j1~SJMBIg~t*OM0t-(rcZ8WyOzZrY& zz>)<#nk`iuN&3HGI(!bxrutH#jUIxPo^B%67-uiufV(ZaZ75YsySb+VvtkX64zLz| znE342>4UT(;EXEep^%7{74%oY`byvLHCd@`Ct|v&s`^)I@_%AE7*BV-G!+&0a47`*&xHte9Z}y&qT2h%$W=QSP=Hv|5hOXaTyI^VH4wj<;1+pVZ+QK;^?_eT zmqK{lN)R8lplNy0@j$Tk-ck%e?deAB#l9l5rot;r2`Uc;kcb&5GRI36`vj@BuS}&f ziiurC{c|~Q`s;jJUP@=G*B^-y0+CVJe+R_%E!CK)si>fKb#=M9HEQX$pfjREpi%AG zISy_b;7R$XA0{E8BN7sl*GX}D*-9OL_GB>qxTO7ffCEl8uThUArdWluAiC3CsbhX? zD?-iR;XnROZl0c)C~(Vv{E_#Db(3?ABy^Uf6_u2n+qek$9a*@or@jou6549Co@nJg zO2q5hZO!zf*=bElZ@+wbsgY(5 z>p5}z(y}%>H8phZF?>Uhf(WsjKmIQeHHS#S&4^xHT)YttP>iOMD}>vt%ti85IaecS z{J`Qsk$gpY*lq%An_#1J;=E;RinV23%F&T65{>j%Wo3*h$H@EJ3nLpF9W!%GRTYb( zQc8BA0|DIm`T6bb?abQRCyJRs@E;7QmbEyHkkwU#P46Sp_uK72t(I;AbnKZ32B@XD zeCe};2ep6O(Ur|*3EOK@m!)3sKfRRh70Lz~<>F%x$^&m(IB?}V9|fc&42M!Hn5?F9 zWE2z-mD1TPZw^)di>c^h1Onwx&u2A-MMYuZ$kV2)jke3@9?>x|j0_C&twpW^B%w<- zCfxyDPgX<;k#<6U%$b6&bofSYoXolbKLD1?3@|7TINM(k#h*si7#J9~x$~Y-%!ebQ z5jBjAa%Z;Y$bEc$KcTN*?G3N|@fQ~Mys-E$oG;g!_3Tx5zYC3xjp=nhem@@RaEqTQRU%?eWV|R-Pft%|@P6I!J+xua7QpGW z+_mj6>nwxY8f|cJFf~10OI;nKCXdgdO1~k3-EXHMh#36_qZNQ}X=#}g@O%OZSHoU! zcX0Kiz0tj{>e0O`>wY(rFi=3p&MVpL>+58p3&?u@6!jGdcol$0U%G*yq^o7~gohP- z%KkR`hs_sez~Qubnz9;RZK(08lV)5l=WVI7UUOyEc_v9!iY{DG+ff~~yALdE%B zP~}cy$|P}a5Z;bF7D40uxCd0phRBR?6zN)Y5DHhhY=3xbBO= z2>onn1D#~Is$pt(jJd|*`SwIPgM-p`&Uvvyd)?>Ev#h*)crx6^qOcF|TF?Vb0N8rJ zXYG7{E`}k9iSxVhgt!R!oJr3=H>4=kBao^TuZs&-Ug9hSasOa7w6U?-E>1VzPLg|f z4B$o{fm;REYDC4<9b05>&dc9+(Q6^hjITBs&l@HuNJ4kW2h-&uM>%OSXBGhBUyVi1|p$ZdaN)KD01dyVl zqlpM$z>F3V>;3T)98YJ5PZfUs6~p#rB7>8~xKn_VN!8Gh7=pX@bssjyuMdQsT`~Dw zCblSoNDK_(u@PX(s;f)tU7(tpSS?bNmRG+F%LpbnwB0TJqb%NsJm)Y~OEv1n3wZx; zYZ@{^h;()W->)fzoBzS7%Ixj6Izxg<|IZmC1kIRUl*WL5nCHzD77~k0$2KaIA*>=& z@eWQK^ZaYSOnIHJQ~8CiB$$1#ijWr5o!9F{D}lviMQ6*e1%hUW+}@3Dw!TldN=n;3 zL(<^X{1j4+AlJqVZ^=Xts0F}|4_h|h*WFC(pJp%jlehQJ0zS74GBPq276YYr*HgK| z-nX-IZ7&xe7IzgMT$yTaFo``evW||TH%IIi5O;`!hj+E^`e0r<2XbXOULKX=iq|#y zfF90H*z@QpFDv`5)I^1>xs zr42T-zc4EWULO7(rRf?Uc0Ft!%*v6=(Q3t#Zp)rR;>q0NV)*A)fMz(-=5Cj_ozVS11HnYP$2UqcAG%Hs%p|2!pR&j|^8yPY>v=DPz?p;Pj{ zzQW8pRi)M8#hb zNc#L8U;N|2584S`u#Kp}ip_K)qi%nHUsFQ^vp(BCOBNFRzkdDN*wRvGIWEK9wH#-|>Q*V`;~MH6|R91E@3UU!IYOsqC_ z52e7~s2M6NC@^MbW`cCphR;2+F=L4!p`xg%DcpQGzTR356>{}Szx8m>>?fhh3O`E0 zzckw30Gf{E7o%f~d^zWIb(T+4jeCMiC_emk1BS|-@rYiZYkiX{pc?ydf$k4=4`~Hv7|JbyR|N&QeM0zklTmg5kdBm6b$-RIA&&u;F20_(uK!}(j(C? zk?@Fpcto#h?=>?4&su+4vvzJZSEOLkUJ#?_!$a#*Wta2*y{%CrJkD0bm+&@zd%Zsr zezfE*^Y$L^{%s7lkU5Rtp#FoxrLcuRVKTBldAYyA zB?g)PxCr80r|Z~Y%hj!QH_yt)644HqE1ysHz5h_WW-tgaS5(%Xd6V+IyS%IX zgfpMpc~s)fD#NA(c(@V?O6a-46!)&*-Kc~aPTr%K*@xwj%YQqtBt9w!=jSISI8GHr z(A#O8TC+!ye4|SiHRkW?Qmhvy3CR_9VbMM$N(tmNFG_w>Vju_Pl8)ui&!>!6kgizJ zMS-ybQ!|zGGcO#pe@T^cD5fFKn3gOQqnD`VO|1*w1gVVRoSnaa=OTuhaWDh;)84WfUgbxkwIjl8H^RPn-PWWa!WrdzJrpP9cotcF5cSMbtG4^Pg-8n2Zovwq zEMO>8AYy%=;dG}f3BuDF(HPGuw0CScDk>@lTg8T*^@dVo?C|2oM%hTy z&5lS@lXy5}WY$Ivt!ksKu92k1q1M*0B~V9xl8vm1+xO#4F=EQQQV!xbsmyO(R`N{G z)gFgO#{ACDf?Gm+8zrld+GSwMj}2Ev-9ABceET2pE-ohPWHgc>h7rJ6#lp!?k$Vg{ zJ+`2vdfk2h{)P=bYCr9Je4#@(VmrU;by}xBu&*o_vK*cye_vxb{M+B898t`KZ`^p2 zM&Cjgd0rk@HUsgG&wv=5ha%$3WC-2Lyj(prNJodudWx{*h8Z_ZS9^)7VDBPDn5ix) zW+-EZK42+jq|FVQx!DVWNby@9*BwKPA3F|qQGpi$wye}WGT=uycZ4%;g>0&s#r05D z6%8+;y5$BGCY@XyQVKZ9BB=zIk|i#rv1DRX4C6nz981^`bY2tBFs+vHy>%`vcp8x;=-26 zw5(wq)nZqEY*I5#pNh6)N{^eH;rSUVJM&+;>5P?P-EGnZld}lCfHad&1`^LJ=9P8` zlmm|QZfL1lzlufnAs~*%gAI0#Z|&qHn0wmJ`hqb;d|lVpUq3dJh3E4C)1eru1zhOm zV~lo=XD=ocaQV+7P@%zH{uQ&?Js33?<$3&5k~|nQlQa3g5oGD&nx;mP;Z~1~(q1T1 zDn!@vzCEgfMadMCK~o`hn6(%Y?*rq!bc{ipq&ue3zi3g9DAvU;6WI zoe^I(f=$reT;RR-i%zPLvt1WMljW^*_h&z*hW_bBM2x1y1NnaZNsaNI6{KU24A&zY zuT-x=3%D5%|J*9ESi>3OXdK8Avwxfo6-7aiR92u7L8fSYfvnBFfmb15zmZRtOuLbW zF*bZc;w8iFd+%U1DKg&ZM3Zn?0pnHVFFO5s0bSa&HVKz@5}Jfg!~C8?P!`QfSHo&T z2uhyp>4wBU{R_^2j$0ABlSex*iW$xZIv!Y*a8`<06Xma5)|0* zbBzKhSG7uL)4O&Tv`Xm`kuNrC+${CkaXlpf7Jm&^jKjS0k&7F_V3dY#%U_h2a%CJL zT->GZPVK@IdhSh&Qb9{am0Y34uLlPQN$Kh61>gHD!q5<2d8DN0Vsb+$@kZyspRuNL zQJrRD(-d-;8axxAyUDF4HP+(bY5#Q-YpRvX6BbvZecw^MS5iOmbXYL}A{^$H(Dd1} z({q#2$tNz&{7^L`KavYfv}rALlu0yJtqM#gcntp4Cskh$_fPoOpNIKDX0H{QU%nlP zVdWuuW=Lpm(S)bQ6*7Tl3y)akV}Q;pO+2wYWFvU=ueZPIzC2l+ondXS@8npwatpza z2t%OkLn;u6ghWXY7DrBF?_7~t!Uh{mOnhOMdA5L>MOL?=oQRiPK>m`Hnu@o5g(9W~ z!7LK=aI9YXkwx4Eds=NJI}}23BCcP#7yJw=l$Q_5@;QUQDUMF5uoZzSCI-_Rp+bzB zEJ<{)n+X=voaIp<&1^=QaQNf#%_@s|yIu=d4eH{lX4KKyC0Ho(HL7N0IQ@*D2*Q*v zEXjL_?%$<9?K%lXRJ~~$8^>37sK; zsM{Y2t=Gnl%$VkC`%Wka=$%rj1y3rfll(Nhc2h^O=2Pou*#l{D|9svPLaqiOGcx9O z-Woe<<|tJBR9H?bN-|%fBAjYUTVcu3SW!y+X+|bYB%hzoS!@m|U68p==j4#o*XITy zcaZv#sOB$@AYme;Y^V;_<{DrTTJD7K-E0ng?10?g8jisri<*r5)|Qt<2zDH~&``N`8BtJ{6?A<>ZR7wn71MVmZM<>HrO_tLxf_mS4(bQ2Y-i}n z){V?hKbES?%JR!<5iTKVd&tI;BKiyVFER%IE>=aNGaQcBNRuxyE=zY<`HTfI@2jE} z_M@-dRY({ikYO4t^m^*bBHTm2s5i?ipfTM|wRzkL@YU$n>}HlS)c>oT98hFbrAbv1 z=9(8lC#D^s%Y^0Y8w^K@mp!S9`Xo$CTdYxB_7b|`C}%^oJ?z+WQ^Wru&h04jei}TW z#L+ilTSz6Frtm8z4lk(}Cw~K_Koy4O?5__8N?8t!&S1pk+1Dx~Q8ZBO3!^5QBar+Q zU$q}(c6vJ0;L?SLaF|nJ9B9ZlRBE-Wp6D;7o<{$zl}VYP8NoWc*Ez&1#WX}j-qDe4 zWhIpV3S!Xx2W~*V`i6$Kt0CfGv{w?^Kc~S5smO*+B+Ai(Y;s?2IZv8pQ(2?L`TP@u z%@9&yTS$RbpKG*q@ii^!zC`>)4=b>8##2AIk?P-p0<}7@Kfy9N1^$g+M#L9}k<6k> zhUMXIwWmp`Fpq&`1#YwsX0RDBcD)BgLS$}zVTk+t7UqMG0=cTuG04X|dO76s#eLF0 zkQ+H8mcPDW`ASnYUz_mI+i0+^-2O=mhqG3(m(6+tp++KKmgA4n;F!P<1W|MM)0YC< zgqx4*{bNXFYE_Fs%`2#ta+?i4j~G_(Ww7WTV+5n0=w@nViqZ*5_(SutsN@h^J= z&^0TZf7Z{qgdsM6t7|a#Wh;w!oQFexAq8`?$ceHpT#yc*{#`)rc;4j;Q(w7cHc|EK zi;$qkNcxQ7OS>X(Q?e)ByPas8rhu~IFCQJl9l7wARgb@-r3Jope)J&S1T9G1@ie$6 zpPEU*2v?G5MHKWNLx{OH34^uWT}us2u*C>2pBJ8-HoLPIYV!RQbD+=@#EJ(`?aN!y z3pgh=U*0N@eWo?vjERy1TwGitZ+}Km z1}oh9bevpV&@qqq_k|;04zX%#VHDvK&W5qoqgjDDYUT7=X;cueO!P?anjLiDv6A$9Km-yK$lwpp$KDvVocG&i1!l`$Z zW}w7_15Od zkfOlov1)Lhm8tOeH(l`Q&>j|>+t=fbz6m#r*-r1poL|NMwnM4k^jQ=XE;D-;f@<$( z_Yk%)ZwsAC0iO)swtKJV(cl#cW6~ePo7b4#C4s z!kly;ogUXgio>sFo)_a2DAVP*wB=_));V=Sn_}(+av>DI2{5nDn8Lm^7UB3yamHsM zZ$H+A#*=GRG^PUcvAY?cjSWt>kL$SXTkPO$)_E{iVk8JPgxjMLc6mNQ(X6Z}cE6e4 zA!_UFp(uqTMj|ZBLG0=0N<8Z>9AP8%p0XG3SaU`eRwdB|)`+ws?=*xm_1Sqat?Y7Z zHf)Y*lx=DI_s z49RhI-e3DvYhD>Do0cU)IJgxsH4Cq~E3Mt}mk2%1{QPBXeI9v#23jh~fB3LhG^8*T zHx0kJPap#uA6HiL({P;CYC0;~mjQ2$19}M^K9Dx53A^LBmRlEvPodYW$L&)YoMWpq z+`G|MP@#OW-!W_#f3q8-QA{cyX{p~3n(P$#-I-M`Ek_@_FRea&THz zE{>Z^j?Ne%%bdi?%s+P$Xg$nQE1bKR<&q5!=2xU$7@-OZ@whU_}yJij!B_qZ_@bcw<{Wyt!M<*dhW(E76KBUYUe z(8vqVeNHX+IlBU7gQ%MEDC0tSFuDyAp#)@&jXAYSl%u>I{4sSbJrdNv@gwZ=`XiZ| zOXYmRpsA2mD@r@lgJMFLKfPbUgesXtSi8X$Cv9wrWO`GDSJeaE%ia6sw-*!IJya~6 zE;+g0gd&)2#7@k7!Z6WmQto>$?^3HwdF-+J0V%f+H5G;dP9bIe*mBkmEHJ$l&%xm# zm+w6Z2)Lj(SyW*gii!DH{EI|8eDhI~zdsBYqziuZEk$2o)TR@re~sWi*IBr5pZg^h zP(i3zuM`#G1KXOF?YdIOTR$lTQaK;ALf*ZDvSLtur9_wuj{Re`Efh!K(TRgkcnvDSRMQ$-;?h&~k%*XuNKX6L~mT(G(4Q6U(?V zLT+ke(Du7d{lh7cOls&38BEfakDf*IMe&|wVx;c=o=T_tAo`h$dtPGR^xwrhdOSKx zh+8;KQnC~^B8UhHOHvk8Hw92}PQ4gdknE_gj&E+pgQF^#Ke6Zyd3yZfs6fquV4CsV zKD7)pRZ1=G0a3DlW6<sZX5DFVY2#&#d_8=akHA;6Jdb&)2-j8A#hV2X#*pvLM&s zu~8ros!uyME;L)>?d{E&%7l5ba-vyNZ$1!rmP<&@vIoTbW%h4JM+YL@8_L4(`Ms-J zryar`=kE&bFHvC3$`P_}xO9FVx!@TvA((&b$5jXlZ+GGxyjx^NnI+YV9|d$Cdn&Oo z;xEy1fBjTo?9B^jW$7x_er&!&RkBZ$@7RReZUP0VhOt<29}QctRDYBe-W#G52ixIf z5aGC%v6v}f6Y;+;hLO>vhR-(|NOL2D_YB9h@&rIUmBS#X*@&c-K{4uRG>dmSS3t$G zS573=FL_X;rPKRUU7~KcJkETWi-QbM8m6P{GuZ5>w zk-L{*ez`L%QO*?E|C3Z!z7jl*?s3!JyB+lne|#2)U@2RU;wl2KB%g>XrZwy{O1urT zZX{#i9yNt0xpuFq)GgOpgW~BD^;sRNUO!gyFrN zIVJWXf)b)aAtVE)SY%el^9@j&q#?+&YZk7Ku{F^x@=MkLqNn`TqU|-lx*wW0>~{+- zx>`Wbzi9wT0eh&a&bdx$%gQb*VJa>iL0SO6_d?xSf2cG)GV2#Jg8ZLJXXmG_eSJId zLBPi5W(jHc&)R70M2;=l(b0$;%Dh7q1oZ$x4;y0qezm zg$5H9Xv-F3LghFQWpa`dqQFJb_y%5jiKM2b74-STo7|S`h$J19A;TS)B_YVWeLl8w z`7Vhso73TCZxF<|_~x6U+i4%e@)O74N-ntz;Plu@NFw4yH7c?KCsh`1Xm9_@t(LYZ zF=((>ru-2;jpc=d}B`{b6io&^0Ts{gS2@# zEUZO=FjBuSv_I`D!`1H$lk5Cjs67%M(TI4FB|<3RmjSpDr^cO`9p%kCQk#-}#d3zebCOFUHNX z;z<67wWFyit-2bGg8nw#y*(QwB`tRFQKMm+n6TPTK%qd;UENY;t24S3(YjwP+}^*Dmu6 z*nAwGXu{4ySt^Q2D~mBEY!u7TxB!&U--Nk6$luzpo}~JIA>EW1ShSc~TE?LCxJy}X zu6LTs$3^j@yp~jw@r=zTS3u~SL~9izGXUQxBP7K9ObD|h;iT-x4?Ke`T0R({!3+;E zT4dptzq`?-iW5eACCkTK!B?5v0dEy}qHs-1s^k$t*-kP6pa2Q|Po=9R%WDGl&UmU0 zG!TKLfX@JMS+#XiTgz`5tVm8;WUe@&E*LY38J`d>P`ouLhLuYqUB&`qRRXFKZ;mS0 zDjjl@wk}0tSMObbLz6oUD(L;eCVmNJU?~JU!Ji^)iv%I|2?3kf4jd5GY?Dr?Oj9u)2c0iU9w0~)pu->Rk_<$6BWtHJHuP#VtyGVCFU4D z8!f-=h1xeivLL%9IL-RJ;FeY`Q%5!EnHAon--mxC#eKgvBO$f;*fE=Cv*abNs^0q} z>ft9Q%wVs)0M&%lgRm+v$1&2UE}_;U%YngO53eGsDrrsf)?}$hh5{kEWejND@9g-# zvmi$y4U&@GgID^0iVABKtMc?vhzFWRreZ*48<}qlCc{h^+!M7~J^UMhokK+xtw8m& zID6DHdyD>YSWrntzW$9G1L3!C1%>jxCLoX^@_-%1IKtq&*9jPEQK)2TeGcAX{HJo6 zM9D}`Z6S(yj*kA9s(WyMZxXc}#AyeSrhBLP?;jqb2Fy^@OF#h|PV4eva@Tv&{gSHrjq2LthUo-pD4brq`()C z;e7c`{7OR5%!JKTNc-Z7fErd^t(uh$y<8X2mM}39EPSTGc=ZBr%mYik0JGS@v*^mC z@mA9ZMS+27l83MyluHUlcz>G*Wmb<5Adp2%l#|t@8~%Q3TcKg4DsMMhO{0;dfHkpt z*-zauoeB=+eLRo(N!FCOK%KJ!*)c+B;|ZZ-2<*kmprroZx2_?AMw>@UQN$ez>5;XP zPeZKDmX24t;-B8GjdBNWHQ=4QkT-VlI@hMSWAJd z#OH4Gr9g~;Ogg+Dta>Luh|FiT+BxE6TVonRqC-fgCQts`hC_Jo9#10Jx6>b2_NU39 ztaBroRw;sHCrUl3kgG9`KlWm4G-T0Enh$8BImsS5Gm{e;oTYg|M8WoZBeDpiH zE_4*g491(Wx$ibSmdXBnZ`sL$ZALL5;62gTpzQZ=%k$MV2~H@{#valN$yzBB;)Wi4 zO*1#JMLdTKH)ricdsr1$U8cKSNW8}+k}G2f$BI&H{?KgysO!8T9Yb-SmpoXDjG5)7 zc6j}i$f}pTA7zo0Y;|>Yx!#gql_wSH*NB+!J(w~cM*=#jt&B=4Ja|2$J&+#WZiii2 zDLv0XHt$_PAfBw*42GA(yp)$wThD;v*!1Pl0``IUkV+@F<50Yxb{Yqnt2JAPB%h>X)1us{C-NOy-uO@Ej4x_7)1|T56*1dKTz# zp>A#&xqY9C&Iula#B`~d+GM1?>F&hPrP?L#R8ecfUwP=`)n*RJUsuDl+xkM#Yqqj-c6PSX=1T8GwDU(66+*_c z3?(bA;(ml=Tg9@O~Na$A4`-zenTGp28M!b<8>q>jikFkPbOX*_K)nQXvs4Lz;Zs zp%(5VFVBL(NQKCZ{gCWn9@yZxyTDZqO~E5=Z%2&Mmii9{CZ>11MkB<{^{IqF!<Va$wIDw=fc*5$k#NTFZ zZ$+ws3dC0ti=o&kF!wZ1_HsyzfumMF@4G2p7^wEnL_D-NuTP(b`yPw{a@DlpSbC%4 zc34)b6K)Nmjx25Ak;H1OnV=^?mRKhu7fLW^v@e?!P!i~~Y<}u+(kcN<>%3-;^ce{z zqM4MRpC7VKP}MZHO+XtJI*2kG9|0CB3i1Vl|NAZgNYn#)2Xq`3UPG+Ax}~)(oEVoR+NsVgU*IGY%WG z#@ru!=g9FZQHh|nrgCb zPMoI6)?{1Xet&%bJDz>)-CFm$abDK~DkK0bYqI2Knegipy=0kA(AU?uTWzj16k|5% z&T`nK=uhUzQ$m*I-mvSRmAdG<6e?G&p+XBaohUhPJ|3!jn{%S!K<&T>A;U)!z%YZV zL`q9yu+j~t!pu?e>7_4)odeX}6GgioTl zwNP8jbaIMX6r-;Foyg!7w{J^HO-=0wl^F#<-jvVGox)_onQRQmgH!-@24u{u}1Y@zApTS zG&M9N17%XC!{+z=cnb^V6N4d$e6VS^E2z0~;89t^fkMd0@%?iW23;QKNgfX~Gl~Y@ z-n8@XzaMYz>jM45N>TbOQHcfMp#JTe^R2{AD$1mTsr*giviY|UT~vTfz~uyhhT)q5 zGXXv_$le-ioGZ=7|N2MZ(%0txRm`gfF82({f|6M85<+S;;iC6gnYTNVWWdlaVA8$4(_mz&-7P~j~=SV{PuBX3#f77A&sc>l4Q z?f1-&NF7xjKCfr~AQ2J9e`lnXjPujb01V0vkRN4jZJ9~|H>ptGiCh`G5FwlewglFme*d?hfhZf>!#IViomKm%b8B>um8!FMV2GcJGz& zHF1CRc)qZRUltjOrdf!dF-A)*D46(g<=RYAt)#r$|8j_-CEzw+h&|3NIUXIxGEGFr zCI~5VLrWt2jaR<8MdArVsv5!eL$+WRmK)J?)BCdLsK9sgw%}G^nwod&-bWBvt^6Gw z9qay&8*#*HqL}nnAXr2kMW5Q>ZsXbp3PK*MqBtoHG|Yv5;4h?sWlD|ZQNcD=z*a?| z=7#t4cL$IA{Z8jX*NIOSpGCv|*7-^kY!V)v(DObffR!U+V7^rBK(F zLBew3|0oK6ghyB%==OH~FLpZiK2zzOPpno0xHLr3nRsQ?uflfMnziSG2Dv0;!AB=l zNuzTZ4cha&bndx>UkHeZO3!P6<`iJKHJ4j#zZeEQQ~ExZHw3&>H)EhEh$MbNfU;y* zH6AJ=Qq*wsM1f7Yt$=1ni3#>s$m2do84M(ILTi>DSGjsYp-H) z2|G_lesQIx-xqG!C^@WcuwfUX2%FZTsdP79P>n+`{z3taU>nwgt7T$jWX3S`avts&s`YX`d$$C191K6N9P)0=`0 zaSdS8Pt!|gG8#+3h9|&KhBgCl86Ha{qcn#x9bF$-ZZOSeHI=H6x(KmlWh`;;`m=Pe zh@T3kL9aC>sQWtL??dPTO><7Y;ri(95uE}7$hl#xq^eJQa4cUNKw&vKdsx`m=tM*t z2g!S*@uutT&K#Va4VJUSF0QVu*$RX_^@(q9dO+ughWV2gH5$L;w&E`BIwfT{IwppU z>B1!fl_a>ZkYr+FLLUR8qux>pU!-j@TP(L<{E@o|h)+Y2P06ljOfh!PTzo!Jltd`7 z9$m<*-cb{Un&ot25{UKYy^Tbva6&JH>S1%;ch@Y>#n=T^7}e`Qbj5L1fp_an|UG1`_5 zj5ZT==z1>o*bP!tCilsLRe~_*W;bSFLhGPU7&_3O$LqQZ)qz4Hq@b>j4T!F?6sXQT zx(H!C(HCjb<5$lD+Rz}*S9C5yXxKsm@Gq zU=KH^;Xt;dktomKelJ6pua=tkiA|(``PSJh8Vp54%P6>Ntm0m7a=zLE2i3U~-d$N$ zb@l+KxmM6A{ z33WVsn1X|yj{OzbSNEn^vwJN|2GDm7beca_nypy>- zM(!e~FapML;r3(s(NBlDjz21`rDkPbOhm8u@{Ezl*sQBw|AjZmo0(4@b28Ua9XYvvSkpwGUh5t7dDdp^gp8Q%3`gS|ndw&jF0(ZYH+y^#RFZ02u;=$cl6@RLkl}MJ051{#X2qV$Fh$ zKV*)5Cq4V^m4S>9WIAF`Hj6fF?Ch_cDHT`OV+kjw3OIGzj<7dEe{$!ilg z6|biE=hY?M0MA#k6{l;cCFCNIG&u;LQ_X8Ho_k_^{NJM&WOQ>g2hc`{?dRbBqN=K^ z*=LLXqNY`&fnrHg-V>KFfj4^B%)^!b?SasJJ$5pe2764VOLtkhI4aJhzcPAeP8O?u?}-WqzZm)rG@3JmHRT)hJd(y$x9inq zR=X|LUI?0(tc}(}At>*~d`T^DmtXvgNm0?QfRHHk!U90KghkG;FkKrn)pb2`Qc^fD z7|vVO@=@anMz0U&8^o1oNi7Y|KuYwysnTS1juAU0k0@d+rK=B zWs0Z)Pdl)!j%yGKu7^#&r{NHJw1a9pP6)1@FVY8r(-P92eZ_9O!|;|(;>x;ZW=lzE z$|dG}%-vUHK%Qf+i~MFZWXE;8s9F7V8CWPeG%^$4+{>t-3;x*}!kY}q-^E1Ykoe$n z&fx)H+VGlVa`XQ>(HRl4JC~yO}<^QN3saQTB1_*2DH5jPbo4_0+ z7G4R+!GnM5ceTd$A)Y6=4n&Bbe4HD%=;R-C89NZfArP@7b8qR50*(Q`3?#%>S<@DP zGMgWO+=cti{+x%wy}$%#AB{^IrBE~qt|68YEQz+-KJ%c9U=a<+vAREIbERSZV%?kj z@j{$%Civ%xlr426_Dhq;`Kl-*Ms;oNA)wYU0n~Z_vXl&o%{sbkEE5-;i9n!xqbRD> z==ax0($uz&wg@z3V>6!P;{(5sXUou>7>?X&yUp(GOv8ZWK{8~i65p1GV+8oUgH9h& zWe6C{=6OMr(JVB{=a(3fP*6}{knaW=@5+s>o5WOqI4gDWaI6z3Trw8t`hLj}H72s| z=g)n>C*%MSH;4{+|9II6*CYPfa{ujU2SGgyg|#a^C@9Eb2p3rSGx?Qwwb-f#Z@#Rt zZJR#1fK09pyTuy=CpH`?9B%hhokgqRwP73zb%h%WQbVWZ<^??=(O?eMhZ+I{9Gxz4kg{jXE z9TA7Ysf%!h2n!4A3OILfB8A_;aYKW(?N=&;2mg5ib@JJzWa`6&?#kR~QRzZgQ&949 z-{SIU4M&^>!u55KLzUgtCep|trNGm6s6sARcCKS@JOKJoDP+yn%_AZrK7YJk0*4x8 zVPG!?CE^^W$S0D11ii#ci~OOSbz))p7jrQh-L`~Ln-*0)220OjVub0=!gLY@CcurS zmz2@|3s6u`6$%%A|NdPlfwr`~Jfg1eBPX9<%^Qfs!FagQXC(m@j!BFQaeaN=;F+Go z`3T?e&d+p|Axgx+qfLD?mj6QdCgSH2$>SVu%=HtHL_8Y5T!>fz(J3~idZnQ>jEMhY z8<=1jA5N{r;d90SUNGB7OQCn({BKXCdcu94m&BWapO7GM=31x2XZ|YfJJeo9_4tdq z-tIPAEe4#)U-~y^@7Kc+2uQfRiE(kN9}jJP4X#HhDXD5Qa&jg>&gJFX)uv%>XSWZ8 z*Y9OzD#}T-K=F%&BszOQGcQ`G+Nv+US?MU1vM&^K7JA|15j@=%7(^?Pg=0b-z}SX<}oqz2A; zo?oabxNz1U;GCNhJD`+w`owyd13C;$Q(46VF(V^1QBl#b@NjetjNhZCYJiK0q$D)8 zVqQ^IRm#bU4X~n?JKbSf5U?{#C4CW4XG8`f`qL3l(4n%mJl5J^+WJcK^)Hn}QWIC= zbs(SNlH(wyQbx0Jxdw?khJ%B`)*FjPa_4!X4(3SNBT>x3Tq;sE5QBrIARx(AD*tFP zSWHf$s`)}tH<+WbfDweS@j9U>tKO(1E? z1rfj-P%Eyw-^%`#9|&R4uXW~<@Qhz4_emKY2#M{Y;=4i8<3{M7hm@6}*by1rL`W}F z*70$MuE4sM13)JmS>HJ!e|63GZ`hsa{x&aO*c`c0A*H*7{I!$B0e zxVRg7)rQ3|7EvPQL{ z3m-yM?@(rNdh30a&sp!@TFpDOh%`$g{DueB6C7_BOqf)_6(^_+MqF04Ofg=#pp zKp!jE<+)EGE`ME2DdO~i!6QYOoRx3BCnv`chXNGR*_nfaOiSd+R92XVoQIG)F`Pie zR;I5s_Tfj$@|>q3VR`m`4|{=mf`Vrug|=q$;G%*y4vPj2+Z%APAtMx(lC4xt135e5iE@XveS>oiO&YZ%aH1EG z#~9r?Z#7-6(8{~Wr{Gx)-&K2Ff-PyJPr-qP$^_<<2^SN|r4yQ+MOq^%WM`x@49hh$ zXO~6q;0Q|KU>_#0JG<6`q(~y=%@|-6@qK&bpvk2=@TlQUu?{N9N;~6=kx(@*rw3!)@NjQ?pkpE~_OJZ2xwsfU zpg22AW4mPKKB{EB*_lV)qyC~pJ-EndqJ?-eJasfc?86mtF0L$cXTLirrcqVdmI5P1 zXQN<{85RTIZ(^Kh?l0v&-QIcoAbQt&m9w|+Ju?p`yS*Z&O^e-`bYh3)8II*nmfdX7 zLa+Ylg`1#8|L@YAZbEk1vsK~B8&1m4 zpC}fOT;&Vhw-_x5hyA_Q{#U7R_whJn1uw>m;|8)Y7jv*d=kYPwoZrDWw0v-)dKsc< zfs@77;a6qh85Rcin1B7+!EC-~Fg1SttOuMG%hg`u8ZF>wQL*5>VxB1$X{WH%R@bt) zxPJZX<^%*wx@52#r9-LtHzNaPcl%p#el74@hJDvbHoXpW)fJEmLA*toPYVaWBFoNuo0dgXnZOK?{0(oy%BX-aLpXX zA~HY$Ri_(7e(!`Xwkhj*W}y$04a(JvgYEDa2dnNP&ISFo=M7*_ubd!mqfeWwbJB%H z%B0Lpc~Mbt)6rp4-v^Ij!Mw>`+ZG+|78`7|Y3A8EbBcsETR3RQ7(zjiNi}_ZBA=YsWxZB)(Pes2ieV$*BohvrC9X!7J2gFh1aO52 zQuyB5yj-bz1|sN_TJY65EQFj={0mLu*uhE1?ho~x9ilc~>)XChZq5&5*3~LD#zI9P zLYfmbtf15qS1Y|E(bL`WePJn??R?4awG!=0OkQU-06>cYN98aNvOcEKpdR1Hyp-f* z=rx#D=?A+Yymq6EO2$3|ReY&y_{Vl^1rZ7hER$Sl%iC0zQ4~T*HAC+MJfQ#!VbJ!r zRME&Y{tGtKxc(zNFt%8f^*MNBY2{8% z_}9MC^9m7(a8%d@_VLL{GFD?a4NDX)#q#h*SIep~tK%7}@r1_oA`ovFZwis*1NdWi zC!*|LjiRwlm35|_qi_WL`0ry7-D<>Zh&EZedVto{_L1huR0m}7`1}TUfxj>Wgw7u` zl2)hYW+;`kyfrx{QdV+x^+kQ$uAAoBzSF|sNa*&H6r4MsjFaoF($;WrS_-Uy8-czE2!$xmS#`>GYe7ZP7s)#C6F ziLh~JVBp4o)k4@PL6xe~Hkg(b$nEF(GF5AOrtAJHD@e_wwS;Mg`;iaT42c|E*~AD1 zt7+sAJ}E~cGEd%v*e(|`J^p|={;*!zkQ*!j{BJTk)N-{8SDQi-F?gD#DYz}+_NdHu zPSqmOF&VvhU)5m|AS}25A+&j3;D=+w%6jRtkQyik0zfqcKA!r(0p}W8pj(aS=t!cf zrUr}}9{KLi+BRCFqA}I@>@RqT&?J*nQcN~|US)NvB2j4uBHws)@dV59gagL~toCJE zU$=}P4i)XCO0DiM0rMdc>V03#j(SoI%9AW#e$(6#_-ibj(Wgxx(%t>%9SgF{2;lF5&o zzIAF;TfD9_J2$_W$1Ggi8b+IxX|T?=`U>kb14nmk;D^9^`*P7_qU=VtlyHpbnPQ9r zt4cJ%)$Br`^IL1QsnpOEnMea)mcK!Mg)uE9<=TwkH!G}d?*VOzxMNF5{AKe`h}Yc0 zmnoS`B|moKyYqPuis#RYijFp(FOmGI)e!Rgx9T4oP9Q%9EU&;Dm;cTciwCD3KpGer zczGlL-22De-P5B&lST~xvwWI;@6r%nM1M%RG0}##-Ci1^;hGC+R4Rzd%-3N^i6n<= zJE~Bag6jU_8_I=dk}D5V3;y7cZLo$V5@$WGjNPkYjV(sxhYRR!!VQVPLPqO z2Qk)!u+tNiO3B^@QISmdQ{FN>y-qY1wYZbo$t@OnE(GWVXYWm0aoAt`plo=1h!$z5 z$Ut6Q3(YLc;smUpeuTBzDYMwepvvx@L@U`ry*bTwK}#(;6_GYbW6!Y@K|pBGv8p8G z;BrWi78MgyGWR}_0lgft$h~`tViVloc;;tJNN0I7*^cB)4Jn5fG)(Yd9CsGxEWW+j z{Y6~KdmN%gp)Y!t9$K*9!ou5<*cRWBf-sgFbR^ZyDZpUri);aT+@(_7EXF46+o40#F)h7A&Jn$bT@lq_3WLExEz?a$`}=4F1Zx*E z{6_rtN7P>R97Tc_1-8cPVJ1@+G_>$iVu^pJT^+=X&YUoP^vg~)JH|cM!7FgPK+5E@ zX5dgz-%{AvGwvWBse8_0k=qy&!0_R&=t(I_gPIk3a#7_CK}HqP7BR_7;9m{?< zf3Vr?kzz;IbC%?AJ?@^6T8K^*Q>=adu?G zz@8T@>m2Xf8Oxo)@CtJwpdq>k7+s7tZCGNMrcNtlX~ireP>y_XYzI()VEMR8O1|=> zGdBGjT<&V@Q_ZvYf$@iS7N2Tv_Z}p}a1=V1R8Q|dCn8cGUseIeH1j-!35wAXGgNVe zyp52|-v^9%YL51gFEhk(VaX#k_x2FSCLy)n%`fFdboQ7B5P_q`cuGS<|~EZSSk@Wf?^(zDA3hpWMs4~o3H*> z#f3M~*~!O&j|_CLu)cgzGcXYP7w(7s$SxOD?U8opULe-9x(qvuBqA$W z_g1bnnnMD%W&iG9n%FEfX|RAf45ZV)IZ`Ucw66Wg;?XxeZM*2RpY?Z`*@VUpyRMxk4K6NC+|#tU7yo=At=B{rW`t8Hw4$VFXOx(g{_k-I z+HeqK!3zDXY~@*D*B6F%OR9IVsPTUmp-JhC+@@4SPLff&9MSSqnk_$3!(0JFPAmjy z?)57IvsIEs>%7DY7b^(#V)wYHhWS5Gxl0o-D+ot;+~jj}59A;F(xRcR2Jm}CadEUT z&x@JI&ANg|udC@8SIs08`;lo`nU+{EqgvlWky`gyqf>*0DQh6lwgg!=7u z7Cc2wwXwH1qp|V7X)YV}(MlW4F$zsk;-$3QJQI4lN)rvBV` z5)EdE*r4&3ETJPPo&1DY=C4##<=H=s!P0hqE}tU^;=KwTfr&%B(_f7xO8t?MY0m_y zUQf%BtEiAQm=eZC@y!HJ8Npm0=G%Xpg&-sS!;Mw%xFz(>hO^P&!6-j+*l3-g!$i(# zo>MxRf`;y{A`evH-I>#M@dCpvK5KfWNCdGAf9ELAd$_G?dkL|D55z@~EEag2(u~Py zc!S@2zJyzr#7aYgyHCn3#njA(#=!GhAQSvy20!`LgV3bC=f!rcBlFOt(GRHTEZ(SW zzE#ZVf)z9{$^&!G|I(!TIhG=@Y|)XBqZv_=cy60f7TiHtE{Tf{PRw|ioZRN_J2Q|G zB||ejGh|xk$&3C za)$466VY_942D^e(P*1X5`It|1&3QdAJjjt!iACY6O41*W0i58^&wxRfd+%c#*4Hp z(w!nMyqdXsXFPE?&ERc^jF~dV=IQ}oCnOv)$j0l} z3MOSYjKu78oQFfcX5HpAwv5UHo6BM?$MpDSEu@|f-hDOMHkBus|KkV3F6M1CJ`D2x zM;;Gr&a4pE&4Ki$Ujr3f$2mjRNfj7Ugb&W=1sJMeK~(5K^pDxJ&FM`eU`oKC-#|vk)*vp={`DS>(=NOz7>Oy$u3S6118IsnH`CGHR|E zm%ZlO3y(e8Z9}M<3T2XF@e+_^%z~JcYRotoI84q1Q!c$*_|cD`{eEBZ3e87kwYBpk{qi5il~3oa8L5tq?iL*M~~h z;-f~%Qm;EwlG=8@#8by*DI*bx>OSHDg3Bisv#9W=?>OFCya&tV>l;0WuzGb1tR}l* zr2<#uG@H*fK^x7uPFz+OJJ@G9^S0>li2GGPdw=2loS(XWDKM+b03|&gdhYAVZY0v z(HaB4Za?NFy+UvildBOkjaT31${{h{n(>-&O9F|QYhc~h zx)J%YA~Lye!_>i%yumL_ZSKO}?_(4Uy7NojxH$_6(ob22Rv;-{{}~%?WBxD6@8Wp@ zU2nuogZ|H-D2_kDAerTJ9ltliu&^u@+KPJDQ@GmqY2YFiKFBhRf_G5j?H7ds*9e+o zs_w7-r)MEyc*%!{=l|Scz5#=gAEGpPS9+sW*uk$COwG)8D%Y;5q1O0-Uuq><%;nx$>ph%Xkbyhbg;W+Ro zgm&#pirte_U?#Q3W0D$S+=M`brD0;qK7Zo(ynBmIqmG87I*q+VIJh($2a-|5(Qy1* z6MYyG8=EE2g7Kpi+!591v59nJnfF`R+@BbQ)X45Ae%;u0J!G7=O?RZ0RXIuC?i)p{ zZKFXm`iWEoFWVKuzxJu>;B9se#=rO9pI|wh_MJF%;^E60amuQqS4W@Mo%GQaOPVpq zJ&tkKxjaNc(;Wj?&Ic{NUS`%1!Uhf!31N2KuNl$2N9tF1t~MWDb>SViiR1{=eh(8! zy?S_x>5{QvB|pQ%8Uq_V2Ny!{Njo#%qacZR?fa=H@*tmLYzEj8U-qI) z1Y|>yZ}*Ezxr40v)xRq=u(|6%v!qdV`-&Q@7{YlqiaKoi^#M4y{SNKi$MR;Xj9EXk7W}H+?#NcR}g( zE3~bJP!<Tex^*w=UU;Sp{&3pXKl z3rh?f6jD+tF30AEw|tS!Hx{Jlub`{5-(2$%7G}O~1U&{lvKX*I4U3Ctgnsl!rUTwz zyYE(U$jB3$oW%ta1<`)ZxG^7Hkr^Fc=*G%!nH#+pEBiUu8$TjYednQJARN8g*;6t7 z=N>6{xlV4yO_P`!`+szIaA|f9f4YXSlcZMQ?){mHoh)Cb>M1=5=pFhRz zR=))WTSv{jqbZ4sdC&?w%vg+03Bp0>`7SQ{y@a>g=UWV<7kIF6^oy>0@khgtb$IQF zQYqC)l|swj9117K?-Zhrsi@yGiHZg>AaT`l-yX|nt|RvAii$?1k|9R%kBy%?){Pp6 ze)i-KUIqxqr1FU%3*1T&d#>k(&Rdxrd2p&(>PZtp&JC2)p*0%;q%=`{HS@kF#bSZRM~Xte80reEL;yoB;tOLek&tK;}DHbhm5 zlWA4LnZ!LoYsPhRUrax7y@=2*b#*2u>!{S!khu-G67Q{(gVGT#HE)(+7qU5za7!R! z5Y+v8$#Qfr7)(BzRRfmMCm#Ng5(;inbE{k;{q=?3B!jW@RX{%Jh$d(wVOn1_ufw)W#c3=+E zi0VTggCd9EH^J)w5|ozqQAA?y<3vDvGJD2Zy7DmNKg0I12V$^niWjPu-?Zao`$Zd- z(CZt13Z^7+?4>5BeI?9i1u2|KHW!A8KUkM>?*2wS9 zx$r1V+Z%0z9p(*>ptGtNdonOfm+5E_91Ru%@U2GxM0-3MlRAl_(vSg?E*+tIan#7F znv2xw6j>c9I_*?bBLM#wDm^C+T0Obb*87@lHbf&;9eyVoR!voyreW4Fx%^PaNvCP8 zVIMVIk755^%gU#?QNxh&ZgL>e!Cdl=!0nVYQgldFl~YFFm&rqtKX32Jdb^{Y1{oeG zmdDYAC%d^=D_z2A{=RQnEDag73kj?aULaTmfoxA$Y@xS3lE>S`!1c?9%PXkb-j@?_ z0(moB1k_Hf6+3&qKsCYnp%W{cFn?GnHD$&}7H*KUk)gn~toPY6{^aKd?ymtl$}b9z z2lJLqEI!rCePaa7%&_=1>F>uX0xxwCgb6r#<^m)S<_7+39-`aQvb$MgqRaQ(>aL$# z5hkAfhw;~Wt_FlIUz_7Uea8o%Z~tB|r^HrPYqHu)apE1jB&LE#ll-Lzd}9eQsT|1I z*b0R|*z~s>#4?_8GFJJ>@S22gZ|Ez<%nd z_G{8NN6p}FM1}HS$dbC-qA~Y=jg6zJf{E&>V8^+(*ZcN{(LoF7^qt$eA{j|fme zdum|5Jwc8CrMf+G6@~vt6?&~z!Q$`?e{_V0Cy5@G9HmRf={u^XVOPXBDVG9cfZw|$ zSu-enrroMKZmd-Z^b+hFPH4O;MQ)N$g?d;_b0SUXjJY4+n?c zZVUg{Ewgtqw2t%DRUaxQo^}L99QZzRMOMAW;gsXy^yjmxH$yaRhE@An3{ez9Fi~u2 z`(?1)9kr~yZ@#W$;s?GjRlRz(xy8*ez7gEx2PxbSOO2Ft)(MRQpOQRM6_1ESw<6VS zWx5iHG>~ByT!%9%Lu8!i{m}$I~g<~hj9rM|Hyc_ zn=mYHFj})jOLfj2VNP&$9Sd+X;>y3CZKuCZLZ2LP%D)lT-RwEl$`2aP(pTP&`gY)C zBw%C(UffXFErg0}(5WJD%tegW4F*JjNIazEq_=bV7XrcH-WAj%V zCuk<7p?LV}BO!=4%h@(&=Th;~YRO<+It{5ariqNCJUsh9t#?ilLO^rNf5xdvn1S0! zU{Jq6j}uwPs!81e9t+HAodA&02A_!8=J?BLub!&s%ksD zLPE7ox=5AQ%KDyst}h0t5BHjssIJ~MVX6?TCTe*V{d;083l(9y8$d-v1Gmht$!xR{ zzi`x9t>sXfEF+sdOrbm^P|L{7+r0+AFmtQQE+DQN|r z2ChxHbDzP0n1z!+h45n(ZL=RZO@`Tx0z7LAvzB+G9Zr|6}iPqlL?&h zL?WDh(&>O1SPK&nM;hqw2st_hG)Vxi{3t1Zu3FD>UKORsluSrS3@xJyut84XknPF${n0xQo!q{>*Fw9r zO4x&;z(BXTY`$RzVPnh1fmvzaeg$P3I7hui$2Z(62OVB?imCypmcqNxgv- zpZ=^!L>OFVCaP#!k3AV#TGd`n7md8${sLcAWVMz0+X{ zVlc`G+!x|Ibe#JNJ#7(wye-ji$YeD%*xOR6RS-#F?L^QYHNM3<#mNwB{X|&)@jV<_ zp^z1xnVFelEL#`(7zJnz0MY=<%R~-t#nIe(uaZD@E;IW3dy<`V_;5ac7z-8~ za|KcMF96AL;1R!3E4eHM`6Hy1-HJfQ%26CdH;<(sUSXa55OYz2Ek7<_P>5?e_w8@n z>LgrI6IB7Z?t1W2tg#iH*39iBIXZgaW9d-*`R^5YtcK;_Eo`2jo8jotR7xl@D8+IA z*~RHq*Y2;?_3jUbyEk(n3N)A@dB$$~49xQVY(`~<)S3eD&L(X)Ne(GOj*{(; z3KZ8dLH_yEogq)L=X3j8*6k}1qK9qAek3M=okdBeERBB8wKg)@dS6WjweoT!i6Zo1 zYuWqm78B;OvQ52SP=SSI1vFy(n;J2j&-3Tv+0eISx%nZ`}CeiNQYKUJu%A3X+)ipq1Tq4wLw_} zf4~$qjLotje7?b3k8L(`@zjsin2K

l=r8Y0LCoGWl8F+~a@-Vf+UPop~G|TeSUQY{yuPw5Y zP#gAVkn*ONJw~#)#qB<#tl#4Xi-aUz+xntV8X7J?SHuaOw&cY$DmNQl^NM#v;K*^1 zg0ZFLd(OF4ixJllWFQgbld(t%bg=KmPjb=#0&{aruMhK%f2J;ajNFV&6tBtx5GWFE zJaC6^uJK-_&}ZxSPUP^>c(rveSStlENRT#XGZj~`AbuL$az%P-Dm}9uXsQCA1JG>W z6EbE6kJsfn7DQ;d%h852u5!`P4B&L4ef`0EwnCW+#!bevcMJB>#eotF9TKurlc)-w zdt34uGY|$@zq_4XCJhXtwa2Wc+nl>!W}M=d^Ix$shNNWv-31R1pY5G8-?qPJ_!Iy$&DEEn{P3}qT~>5TY4jg7qcRDjt5#D$!QY{b3EDzAt8i% zjrG~TOXW2DaVE!A45so@NAB<FF|FC7A>MJqJed-4r{_>I;2LlDcdc72Q$i`O-)kU-! z=!tLW2siS}`+QO6&o}ysUnB!hX@nQg?>?hSGCR)|kykSd@B&YMH0+i{yR}_QEY_10 z5t5ck>AYcqLRbSSur@$n4Xi)Tj+Mt27TePvf|zC%FI%1A+vm*11(>O_8iKdpMrEjLZ(jNi^oCX9QJ zDh&j`nKj+&=;wv7$d)zMB z)2!*2hBKl-q;XVnM30O1Yh13}*jQK_+cdupsmQQ=;Vs=+%1l9XO?)sgLJ3(3(QoOj zRJ}pFyIvnYmrku`*RXcqDYCV@o?ZRk!IohdFrcb{mneu?0Y`(XqKr20&k7Rp?Kf4j zaGEeznu62FV_!fI$5m}p8i2tK(uZ~L z?qDeB@0S}L=5{?kfUT^Ycm3=OalS_QjQJ=APhNAZJUY~6#|2)aTY@-ua!#*=8!E3@CSkF^-|h#+h0n;c}?WpIIJa&gCqdW>(P`@$TZAWO+o)qqBIy; zY@qIp;b->rkNL(2MfLCq9J3IVrGPb9l2;6L-dx{+PoiY;(~b}BR~{Ai=!(MoPj%6* zLMsuY&7T$yod=+6cxh|9uvww(ky|AgQ&GE2@YLmO{6R;@4JGCJaCc!O%hwFJWXe8W=Db*(!=SeiEowN1$2)K27|b*ZlVr{14_@i! z;e=iUr>p(F=k>?7{I_=6XzyE0Fd(aUPt*NUp{ic_y;5Z;D@@0s?|>~M(B#Z$n5sW! z7;!e;`L-ta>LDV9jC@v*Kl8}&=7UpwL7D@^;+YW*OT0OK=6b+XeA!1d`}ND1^4BqZ zcUZRBs{=&c{{=uAzvWrHcsmzfQcmjj;oLN147miL#!gs(ghJ`$dKTf>F@Y9DjvW4+ zQL!eF`yi(Y!vD8-=3!A){~v#y8D@qV28Ml8NkI`6aX~@CjlwN8D=l+r^;>C+Wu@(= zrl$5af2n0@mSx$dU$w<0cdXoUM+C(cP?1fBff;7Izd!B_T4}ieF8O|*`@l1I?%aFM zd7pDX=d+Sk(`~Dj%28v-QZ!}^I-p}fpk0`Y6*xr*yDqene7ySq4f;X3a=sWjYWmiNYZlmrF(@Su4Y?y7PVc1qPP2>-?#1K zpf!;0|B;yc`63!dpW(fCJ8)0GrhuA$15Fr>uqUGdpu|rt@zv)mS-5;7pU>Tk+4bmJ zB-D7&=dN2=zAPTQ9oi+AGwGeh+}6QD8RDiu^A9*n^gUHX-?z5`?OHd3i>P@e052r; zz`8B|v~71g3%*Dowd)~%-u6F+4?FED{UEV@+un08eW(VSb3xQ9*@a*iFcY9f%N!<- z+0U6Xp6uKa#cLzG@!ShhfCq_<3yF-(WykN0Sp9`W_f#|6(&y8EP(CY{HRHh{v3Q*` z0G>#wP}8$lL)NU0$7X|e$u`DMT+D6lEtt6Jz7oYLPt&NRuQy_N7PBEv_{u(Bg@N1sfUrGSTTW z*s-kvUAkwHfBF`NKN`)lv^x0wL)hH8gzucP1C>h76F>U3JDK~L4>}Zh{L9yQclnq0 zZ7Uf!VIwJBPU3VzR%Z1O{ahFkgkxsqo*gucJ%lUwOGhEDJT=Cp;;NL5b2W6D5D*!4 zm`R@;MNUaeR4 zI@7KO(cvnQ-GzxjnocLtvrjf-N6W^ucFAT!LJKG}8u0aK6Bb@bxe1mnyOo~3B2nO3tx06dP%x;!k*A*7Pu+T_K+Y8KxDaGl z+f6PJSM_}^Nq7K{0n=?ZJT|P0yq^zkEfzAYWC)P8D66&oMg)m1 z%#5FsMW>WKs8ra2;lP|L&DVG(xN0jqz}T@k$BZEx@KY*j8y23Am$x5FzV}0gN43;N z6@Utfv~7BhX893bS4X{wlhlhSA*M+gF^v?Y{iMT)8ubmPtuZ37;b6kJe1?z6B))AX z8jZyEZMX2r$4R{MdMFBnD|1`*z=FVmjfrm`r+d%sXyibHkAU~8Bhk^7;Hr`xs>GQ? zK#WR7WW5L@2E8w<)_GN1jHz5l2=*WIz&uWW6auYu9jFR7$ zdsAVnOHh!BTk4lmw_X8Vx*2I4Wo5|^epKRt>N;KJ<<%UXe{t$u83RD) zfPe~hU|26c=(}Gj0H-zrW>fN zhCM@V!H&R$k^qWXwq_>}K9WgbU?CnJ@ax8gj31N4hciRSDUcJ+R`;gdh~UmUDtYPU zO*Cts4|08ii9+yAHQ(y8aZN2=t_|y(z$iey@}~%1c`2El`&8Awfpj2`hQ9}N?^DQY zV}7S;tgQZYIF$UF7EY(zngL;eo{o-XusFz0^;`O&SsznyXVe>eQLE1yShDte@cIiKShGBg zl5?a#PuDZmWJfxH3eI(A6Qm{;@a(AF^t$WBAHKVE(JkE7uLTeQIM5^2m>t1}yjV!JX}lp$rG?-VGCBTjwB~nOV&=|vc}3?sddHIUM|7pw4kz&k5u)EL~OK^i263# zC+E_;&lX}^$P>}&1cyUQ+PYv;yTt;*C|o43Nw~$pp^u(q*4)+Zk9PtO2*JJ@z4>M0 znk74cB$0qzr@oOS!4r>kVa{ivHBNYdN(7azu;PzrWznryDSm#Z@bZ$&5j&l*XQ!Uc zzct`!P6Lh|(~_Mb*q`p6ESLL)a#7w^O`SwFmIw*8Q8zrF#CF*vCgr1ZiCE?e1vYyy z8`k>s>W~Q1i$igHj@C5XM1Sm|Q+)L4D!0$eL16H;qIo$Vu4%FZND>2qNkF|yE4(_g z3-3-3sWtCK*^5$6(frbLdEC?g4D}<=pws4}KgSw#@&xSOQIDLQAj(YMn5}^vKLX|o zIDJY`QY29PskbFSt$}(CoG26!9Rq5W37w~r$jCfmi;Nr0+dQCjT-;)yweFqg`QMY1W{V*OZpx!mVW2Mw?72pP7GU{S|XX~4o1J0 z&!|^cyEpQAzzf%sit$3YF31ibN%TXGatbLe)$zfMBwl?Z6fH_rwIrX)8a2;9oC(52 zzei1Uyxl^hXe%0x39YsePfsIyy#>8D{8Jq-rxQ$NV6iALnfx)EeX-enIGwL0ZLOMB zOTE~d<&CU3l+YoaR~zK^gQNT@`0zXq{Mn{;<_O_RITvu z0^!BQzI^g=JR@HUMTHV=4G~^;B$Ie#PzeJb%xBN;AikWVW9LB~I^+c6XQ?F{%6bJ!@1%jMo3QEri(s!v?yV#R_bez>aNqD(7 zz88W_bUP2;Z{+i@{i{{gXm|TB#HZwMVh*W zg#j#>-<)Tk4}}^jE+(gdi)-rSqd9A76@QWgnI2dy4f%d)B;()qMdwPn=PqPxMNkQx zziZ`h$bU5#{Eh<9U82g+h=Off_u{EJ&byP_@ack(YVl2%V^poY_wFeMKCqX-z#@QL zWYOi@E51Q3;<_U{REacGfr!#GdVW~ioCh9`0IIk2yE}Ys=%c5ZI%6e2ENQ{v!=d!< za}cf8$futqG2v|=*EeOs;-B=|Zgb+57^NisI&emmB=0LPJueqD$z=i-|2jky$z8AAqe3~1%)X$@!?T|`2>;7FD?hx0Yn z=FegVoH&^MaT;D;2HLf2$?A1}I4gA|-I|NHj}~u@h4*J2r(<#%CAoGEoYYf^;)0H3 z3a-pA#ptqFR=P6hy8VVKQPB79bbNdTb?O+=dib(pz5iwRg%JgbEh>2bgKT>BNypo} z0^k7fBJjVlW3QkQR|(f;kd8~Qy99UwSg+HW>Ctm1Q>LZUtE&-Xl|kmpU`N3VPaPq) zc`*w#z>;OU_3Bh_`1n`qu1IZmTnLKqX1N#+I23O>P zB$Lv?$Tv2jQh~R38FFSr2fh0i5EY$|-EJl{)Wr6!4Vg5t5A!~5L1y}CMvvbFSTUi* zOa)Gga8itoQcO57qI8k?ujF@=+Xm>c^W-CknLPdgRw@8NyIV~(X|jj=2K>%{XYQhY zkiClVC-ZbPh!V{Eq%C3L$5`~;DIAwD#5Pw89~y9)NmI9x+%Xfi{Ob4#7$F4fMfI#B zR|QwgpQ+j)1iK`OVZb=0l82i#%VXy3GQz@IFm*R&)nVz605;aH+CayYGe}a2>shtr z8S&XjV6+hATyiH~W4P|g4j=?MlEh=caUpoc&(Em(@AQqt#-8MnCvL@f ziMnT*77ixQ+)hOWbWFK}lnxS!@!3SwFX80LDBhYJ#)kEB^v}-U-4(Kxq#sm}eo*4f znXfPy;IlamSnx#$B&8?KVvckF0|)8TcRPB$VEx)Qe6cVP9ZJ{wD@aN{ip8P>;!vpt z3I!N^;kMiN)4ih?OIP~yYEd{9Z!&A0YS?i-ZKuI)hxu@|V|=T-5f^QI)R1(*YbKm2$$ z?K@}FsBtgK2mqWHrGG9%!HZArB_P1g3&UCf8rE%evTkEj_uUxOuaJ=7OacO7+Kiuh z_)#4T=EX2&dNdzS{~dz?2J}qg>t&5`3q`BeMe~2NVz?uvkQsA+LwTOwmm~#klS%<6 zKKeom3w>GmRXqkgu$SjvDPTpKfzM_e7(d0t%;{YyEIP!!0}t~p%XxZGLe=j~dq+>( zHnFttxSb~+_Ti&>;eSv?x;0I*BwNJ6tRHC@U3K~d@R$%RyZ8~f_Ho_H>=%M-fLbHx zD3KaQgCzaP^G~J|<}VL$%i)LntnZrF-`@QoFWc&Ck)?JMa`Qj5~xR!GrzIU`4~mUm78>^PeY+ z0c#c2E0O!b-xJ?`MqQebHb^syB7lKZ}802L4OnrYlE7p9^fWE~j zaAM@b8(1Hl+&?%GgJB?ED5~BB0@<1vN(GUTCpeiGz{a(D*6fHNq228W(U1R*ssq66 z>5Tygl|XE)#L&kpxv!sE(Or9+BW@s4tPzuu~1p-PO2#kONCjapjdVKojPqgn4&${*XS-8X#P@dNi zcUKPACdJ%;MkewAw z+S*VY4lREV34@Obl}hl|n_83&pc+}zJYb<)L}1+zDg#wYdbcq zlW5&0o4_E4>)t9B{~UpP7h=Xizq>4?bUw-F1x{p6mdwfqYt$Xb-#uv}qhu92)WLdXkc( z=)NhEW#1cE`>Q9Q_*>150w+H$)}T?fCo##yopD!RmAI7n8iJrV%U;6ex zz-M!tvwpLlyb3jWo5L7ltfZtQfZu=B()%7WdVML)8#&p1&>aXZ2nedgXq4S|BkJdn zmDz}xCUMB+(*%~09bFOtL#;67lGR!c&1heJLsS{~xoOaZ~cRzT0&V_Wmz>1&z(VH&Ge zC$Rb_4+?Uf1O?_}HXB&7#)pw6GxZ}gNl0$O?t@xaXrGa0vE?lOE{eo9;OPnb_R0Et zZjZ2Az5~9y5e!6^gMTgA0VLT)JPzmv+>V^hKxMVUfvgBl<=5qhMP5w* z&;YKQ&KC-tup+9QzuZ+Pl3SkP-Ulo+iFG15<KQr82dLzuStm(M%*JjziQVo5 z(SR*mqIq^iFt>Mh@Z!h}mj16UAI|mvg9X%BHNkNGH8Ff>DZTra6B?3BbmQZwj#8&yh1j(g1FX3m>kyq(#*Rc)QB?Lv) d`?UW~{6G9U9M%T&mDm6P002ovPDHLkV1k@%Cx!q3 literal 0 HcmV?d00001 diff --git a/frontend/assets/images/favicon.ico b/frontend/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f013bb81b33600f826cef507712c949b1807b82f GIT binary patch literal 62 zcmezW&xS#fK^F)W7~B{V8Oj(k8Il?D8S;Sq0EQf(NHRkzLk3VJham;XDgugwRNFD| IGH@{f0F}cE?EnA( literal 0 HcmV?d00001 diff --git a/frontend/components.html b/frontend/components.html new file mode 100644 index 0000000..bb3251f --- /dev/null +++ b/frontend/components.html @@ -0,0 +1,885 @@ + + + + + + AGC Design System - Component Library + + + + + + + + + + + + + + + +

Skip to content
+ +
+
+

AGC Design System

+

+ Component Library for the Attorney General's Chambers Document Search + Interface +

+
+
+ +
+ +
+
+

+ AGC Design System - Attorney General's Chambers Document Search + Interface +

+
+
+ + + + + diff --git a/frontend/css/animations.css b/frontend/css/animations.css new file mode 100644 index 0000000..b39ff12 --- /dev/null +++ b/frontend/css/animations.css @@ -0,0 +1,286 @@ +/** + * AGC AI Document Search Interface + * Animation Styles + */ + +/* Typing Animation */ +.typing-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 4px; + background-color: currentColor; + animation: typing-animation 1.4s infinite ease-in-out both; +} + +.typing-dot:nth-child(1) { + animation-delay: 0s; +} + +.typing-dot:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing-animation { + 0%, + 80%, + 100% { + transform: scale(0.6); + opacity: 0.6; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +/* Hover Animations */ +.hover-float { + transition: transform 0.3s ease; +} + +.hover-float:hover { + transform: translateY(-5px); +} + +.hover-grow { + transition: transform 0.3s ease; +} + +.hover-grow:hover { + transform: scale(1.05); +} + +.hover-shine { + position: relative; + overflow: hidden; +} + +.hover-shine::before { + position: absolute; + top: 0; + left: -75%; + z-index: 2; + display: block; + content: ""; + width: 50%; + height: 100%; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.3) 100% + ); + transform: skewX(-25deg); + transition: all 0.75s; +} + +.hover-shine:hover::before { + animation: shine 0.75s; +} + +@keyframes shine { + 100% { + left: 125%; + } +} + +/* Page Transitions */ +.page-transition-fade { + animation: pageFade 0.5s ease-in-out; +} + +@keyframes pageFade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.page-transition-slide-up { + animation: pageSlideUp 0.5s ease-in-out; +} + +@keyframes pageSlideUp { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +/* Focus Effect */ +.focus-effect { + transition: all 0.3s ease; +} + +.focus-effect:focus-within { + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); + border-color: #3b82f6; +} + +/* Loading Animations */ +.loading-spinner { + display: inline-block; + width: 50px; + height: 50px; + border: 3px solid rgba(59, 130, 246, 0.3); + border-radius: 50%; + border-top-color: #3b82f6; + animation: spin 1s linear infinite; +} + +.loading-dots { + display: inline-flex; + align-items: center; +} + +.loading-dots span { + display: inline-block; + width: 8px; + height: 8px; + margin: 0 2px; + background-color: #3b82f6; + border-radius: 50%; + animation: loading-dots 1.4s infinite ease-in-out both; +} + +.loading-dots span:nth-child(1) { + animation-delay: 0s; +} + +.loading-dots span:nth-child(2) { + animation-delay: 0.2s; +} + +.loading-dots span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes loading-dots { + 0%, + 80%, + 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } +} + +/* Card Hover Animations */ +.card-hover-3d { + transition: transform 0.3s ease, box-shadow 0.3s ease; + transform-style: preserve-3d; +} + +.card-hover-3d:hover { + transform: translateY(-10px) rotateX(5deg); + box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1); +} + +/* Button Animations */ +.btn-pulse { + position: relative; +} + +.btn-pulse::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: inherit; + background-color: currentColor; + opacity: 0.6; + z-index: -1; + animation: pulse 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite; +} + +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 0.6; + } + 50% { + transform: scale(1.1); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 0; + } +} + +/* Animation Utilities */ +.rotate-90 { + transform: rotate(90deg); +} + +.scale-90 { + transform: scale(0.9); +} + +.scale-95 { + transform: scale(0.95); +} + +.scale-105 { + transform: scale(1.05); +} + +.scale-110 { + transform: scale(1.1); +} + +.scale-125 { + transform: scale(1.25); +} + +.animation-delay-100 { + animation-delay: 100ms; +} + +.animation-delay-200 { + animation-delay: 200ms; +} + +.animation-delay-300 { + animation-delay: 300ms; +} + +.animation-delay-500 { + animation-delay: 500ms; +} + +.animation-delay-700 { + animation-delay: 700ms; +} + +.animation-delay-1000 { + animation-delay: 1000ms; +} + +/* Animation for fade out */ +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.animate-fade-out { + animation: fadeOut var(--transition-normal); +} diff --git a/frontend/css/styles.css b/frontend/css/styles.css new file mode 100644 index 0000000..3eb06f4 --- /dev/null +++ b/frontend/css/styles.css @@ -0,0 +1,1094 @@ +/* + * AGC AI Document Search Interface + * Custom CSS Styles + */ + +/* Custom Properties / Variables */ +:root { + /* Color Palette - Based on AGC/Government Standards */ + --color-primary: #1e40af; /* Government Blue */ + --color-primary-light: #3b82f6; + --color-primary-dark: #1e3a8a; + + --color-secondary: #d97706; /* Gold/Amber */ + --color-secondary-light: #f59e0b; + --color-secondary-dark: #b45309; + + --color-success: #059669; + --color-warning: #d97706; + --color-error: #dc2626; + + --color-gray-50: #f9fafb; + --color-gray-100: #f3f4f6; + --color-gray-200: #e5e7eb; + --color-gray-300: #d1d5db; + --color-gray-400: #9ca3af; + --color-gray-500: #6b7280; + --color-gray-600: #4b5563; + --color-gray-700: #374151; + --color-gray-800: #1f2937; + --color-gray-900: #111827; + + /* Enhanced Color Palette for Government Theme */ + --color-navy-50: #f0f5fa; + --color-navy-100: #dbe6f3; + --color-navy-200: #b9cee6; + --color-navy-300: #8eaed3; + --color-navy-400: #6085bb; + --color-navy-500: #4065a2; + --color-navy-600: #355088; + --color-navy-700: #2c4272; + --color-navy-800: #24365c; + --color-navy-900: #1e2d4c; + + --color-gold-50: #fefaeb; + --color-gold-100: #fdf1c7; + --color-gold-200: #fbe28a; + --color-gold-300: #f8cb47; + --color-gold-400: #f6b412; + --color-gold-500: #e29807; + --color-gold-600: #c07805; + --color-gold-700: #965709; + --color-gold-800: #7a4010; + --color-gold-900: #653310; + + /* Typography */ + --font-family-sans: "Inter", system-ui, -apple-system, sans-serif; + --font-family-mono: "Fira Code", monospace; + --font-size-xs: 0.75rem; /* 12px */ + --font-size-sm: 0.875rem; /* 14px */ + --font-size-base: 1rem; /* 16px */ + --font-size-lg: 1.125rem; /* 18px */ + --font-size-xl: 1.25rem; /* 20px */ + --font-size-2xl: 1.5rem; /* 24px */ + --font-size-3xl: 1.875rem; /* 30px */ + --font-size-4xl: 2.25rem; /* 36px */ + + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + --spacing-3xl: 4rem; + --spacing-4xl: 6rem; + + /* Border Radius */ + --border-radius-sm: 0.125rem; + --border-radius-md: 0.25rem; + --border-radius-lg: 0.5rem; + --border-radius-xl: 0.75rem; + --border-radius-2xl: 1rem; + --border-radius-full: 9999px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + --transition-slow: 350ms ease; + --transition-bounce: 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + --transition-in-out: 300ms cubic-bezier(0.4, 0, 0.2, 1); + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); + --shadow-outline: 0 0 0 3px rgba(59, 130, 246, 0.5); + --shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +/* Base Styles */ +html { + scroll-behavior: smooth; + font-size: 16px; /* Base font size */ +} + +body { + font-family: var(--font-family-sans); + line-height: var(--line-height-normal); + color: var(--color-gray-900); + background-color: var(--color-gray-50); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Typography Enhancements */ +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: var(--line-height-tight); + font-weight: 600; + margin-bottom: 0.5em; + color: var(--color-navy-800); +} + +h1 { + font-size: var(--font-size-3xl); + font-weight: 700; +} + +h2 { + font-size: var(--font-size-2xl); +} + +h3 { + font-size: var(--font-size-xl); +} + +h4 { + font-size: var(--font-size-lg); +} + +p { + margin-bottom: 1rem; +} + +a { + color: var(--color-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--color-primary-light); +} + +/* Custom Focus States */ +:focus { + outline: 2px solid var(--color-primary-light); + outline-offset: 2px; +} + +/* Enhanced focus style for interactive elements */ +button:focus, +input:focus, +select:focus, +textarea:focus { + outline: none; + box-shadow: var(--shadow-outline); +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-gray-100); +} + +::-webkit-scrollbar-thumb { + background: var(--color-gray-400); + border-radius: 4px; + transition: background var(--transition-fast); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-gray-500); +} + +/* Utilities */ +.line-clamp-1 { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.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; +} + +/* Enhanced Custom Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeInRight { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes bounce { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +/* Animation Classes */ +.animate-fade-in { + animation: fadeIn var(--transition-normal); +} + +.animate-fade-in-up { + animation: fadeInUp var(--transition-normal); +} + +.animate-fade-in-down { + animation: fadeInDown var(--transition-normal); +} + +.animate-fade-in-left { + animation: fadeInLeft var(--transition-normal); +} + +.animate-fade-in-right { + animation: fadeInRight var(--transition-normal); +} + +.animate-slide-up { + animation: slideUp var(--transition-normal); +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.animate-bounce { + animation: bounce 1s ease infinite; +} + +.animate-delay-100 { + animation-delay: 100ms; +} + +.animate-delay-200 { + animation-delay: 200ms; +} + +.animate-delay-300 { + animation-delay: 300ms; +} + +.animate-delay-500 { + animation-delay: 500ms; +} + +/* Enhanced Button Styles */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + font-weight: 500; + border-radius: var(--border-radius-md); + transition: all var(--transition-normal); + cursor: pointer; + border: none; + line-height: 1.5; +} + +.btn-primary { + background-color: var(--color-primary); + color: white; +} + +.btn-primary:hover { + background-color: var(--color-primary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background-color: var(--color-secondary); + color: white; +} + +.btn-secondary:hover { + background-color: var(--color-secondary-dark); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-outline { + background-color: transparent; + border: 1px solid var(--color-gray-300); + color: var(--color-gray-700); +} + +.btn-outline:hover { + border-color: var(--color-primary); + color: var(--color-primary); + background-color: var(--color-gray-50); +} + +.btn-ghost { + background-color: transparent; + color: var(--color-gray-700); +} + +.btn-ghost:hover { + background-color: var(--color-gray-100); + color: var(--color-gray-900); +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: var(--font-size-sm); +} + +.btn-lg { + padding: 0.75rem 1.5rem; + font-size: var(--font-size-lg); +} + +.btn-icon { + padding: 0.5rem; + border-radius: var(--border-radius-full); +} + +.btn:focus, +.btn:active { + outline: none; + box-shadow: var(--shadow-outline); + transform: scale(0.98); +} + +.btn-disabled, +.btn[disabled] { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; + box-shadow: none !important; +} + +/* Enhanced Card Styles */ +.card { + background-color: white; + border-radius: var(--border-radius-lg); + overflow: hidden; + transition: transform var(--transition-normal), + box-shadow var(--transition-normal); + box-shadow: var(--shadow-sm); +} + +.card-hover:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.card-interactive { + cursor: pointer; +} + +.card-header { + padding: 1rem; + border-bottom: 1px solid var(--color-gray-200); +} + +.card-body { + padding: 1rem; +} + +.card-footer { + padding: 1rem; + border-top: 1px solid var(--color-gray-200); +} + +/* Enhanced Form Controls */ +.form-control { + display: block; + width: 100%; + padding: 0.5rem 0.75rem; + font-size: var(--font-size-base); + line-height: 1.5; + color: var(--color-gray-900); + background-color: white; + background-clip: padding-box; + border: 1px solid var(--color-gray-300); + border-radius: var(--border-radius-md); + transition: border-color var(--transition-fast), + box-shadow var(--transition-fast); +} + +.form-control:focus { + border-color: var(--color-primary-light); + box-shadow: var(--shadow-outline); +} + +.form-control::placeholder { + color: var(--color-gray-400); +} + +.form-control-sm { + padding: 0.25rem 0.5rem; + font-size: var(--font-size-sm); +} + +.form-control-lg { + padding: 0.75rem 1rem; + font-size: var(--font-size-lg); +} + +.form-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--color-gray-700); +} + +.form-group { + margin-bottom: 1rem; +} + +/* Custom Media Queries */ +@media (max-width: 640px) { + .container { + padding-left: var(--spacing-md); + padding-right: var(--spacing-md); + } + + h1 { + font-size: var(--font-size-2xl); + } + + h2 { + font-size: var(--font-size-xl); + } +} + +/* Dark Mode Support - For future implementation */ +@media (prefers-color-scheme: dark) { + :root { + /* Dark mode variables would go here */ + } +} + +/* Badge Component */ +.badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem; + font-size: var(--font-size-xs); + font-weight: 500; + line-height: 1; + border-radius: var(--border-radius-full); + text-transform: uppercase; +} + +.badge-primary { + background-color: var(--color-primary-light); + color: white; +} + +.badge-secondary { + background-color: var(--color-secondary-light); + color: white; +} + +.badge-success { + background-color: var(--color-success); + color: white; +} + +.badge-warning { + background-color: var(--color-warning); + color: white; +} + +.badge-error { + background-color: var(--color-error); + color: white; +} + +.badge-outline { + background-color: transparent; + border: 1px solid currentColor; +} + +/* Document Card Styles - Enhanced */ +.document-card { + border: 1px solid var(--color-gray-200); + border-radius: var(--border-radius-lg); + padding: var(--spacing-md); + transition: box-shadow var(--transition-normal), + transform var(--transition-normal); + background-color: white; + position: relative; + overflow: hidden; +} + +.document-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 0; + height: 3px; + background-color: var(--color-primary); + transition: width var(--transition-normal); +} + +.document-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.document-card:hover::before { + width: 100%; +} + +/* Document Tag Styles - Enhanced */ +.document-tag { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: var(--border-radius-md); + font-size: var(--font-size-xs); + font-weight: 500; + margin-bottom: 0.5rem; + letter-spacing: 0.025em; +} + +.document-tag-lkk { + background-color: var(--color-navy-100); + color: var(--color-navy-800); +} + +.document-tag-legal { + background-color: #dcfce7; + color: #166534; +} + +.document-tag-criminal { + background-color: #fee2e2; + color: #b91c1c; +} + +/* Chat Message Styles - Enhanced */ +.chat-message { + max-width: 75%; + margin-bottom: 1rem; + position: relative; +} + +.chat-message-user { + margin-left: auto; + background-color: var(--color-primary-light); + color: white; + border-radius: 1.25rem 1.25rem 0.25rem 1.25rem; + padding: 0.75rem 1rem; + box-shadow: var(--shadow-sm); +} + +.chat-message-bot { + margin-right: auto; + background-color: var(--color-navy-100); + color: var(--color-navy-800); + border-radius: 0.25rem 1.25rem 1.25rem 1.25rem; + padding: 0.75rem 1rem; + box-shadow: var(--shadow-sm); +} + +/* Skeleton Loading States - Enhanced */ +.skeleton { + background: linear-gradient( + 90deg, + var(--color-gray-200) 25%, + var(--color-gray-300) 37%, + var(--color-gray-200) 63% + ); + background-size: 400% 100%; + animation: skeleton-loading 1.4s ease infinite; + border-radius: var(--border-radius-md); +} + +.skeleton-text { + height: 1rem; + margin-bottom: 0.5rem; + width: 100%; +} + +.skeleton-text:last-child { + width: 80%; +} + +.skeleton-circle { + height: 2.5rem; + width: 2.5rem; + border-radius: 50%; +} + +.skeleton-square { + height: 2.5rem; + width: 2.5rem; +} + +.skeleton-button { + height: 2.5rem; + width: 8rem; + border-radius: var(--border-radius-md); +} + +.skeleton-card { + height: 12rem; + width: 100%; + border-radius: var(--border-radius-lg); +} + +@keyframes skeleton-loading { + 0% { + background-position: 100% 50%; + } + 100% { + background-position: 0 50%; + } +} + +/* Enhanced Toast Notifications */ +.toast-container { + position: fixed; + bottom: 1rem; + right: 1rem; + z-index: 50; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.toast { + background-color: var(--color-gray-800); + color: white; + padding: 0.75rem 1rem; + border-radius: var(--border-radius-md); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + gap: 0.5rem; + max-width: 24rem; + animation: fadeInUp var(--transition-normal); +} + +.toast-success { + background-color: var(--color-success); +} + +.toast-warning { + background-color: var(--color-warning); +} + +.toast-error { + background-color: var(--color-error); +} + +.toast-info { + background-color: var(--color-primary); +} + +/* Print Styles */ +@media print { + header, + footer, + .no-print { + display: none; + } + + body { + background-color: white; + } + + .container { + width: 100%; + max-width: none; + padding: 0; + } + + a { + text-decoration: none; + color: var(--color-gray-900); + } + + .card, + .document-card { + box-shadow: none; + border: 1px solid var(--color-gray-300); + } +} + +/* Accessibility */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.skip-to-content { + position: absolute; + top: -40px; + left: 0; + background: var(--color-primary); + color: white; + padding: 8px; + z-index: 100; + transition: top 0.2s; +} + +.skip-to-content:focus { + top: 0; +} + +/* Custom Components - Data Visualization */ +.stat-card { + background-color: white; + padding: 1.5rem; + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-sm); + transition: transform var(--transition-normal), + box-shadow var(--transition-normal); +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.stat-value { + font-size: var(--font-size-3xl); + font-weight: 700; + color: var(--color-navy-800); + line-height: 1; + margin-bottom: 0.25rem; +} + +.stat-label { + color: var(--color-gray-500); + font-size: var(--font-size-sm); +} + +.stat-change { + display: flex; + align-items: center; + font-size: var(--font-size-sm); + margin-top: 0.5rem; +} + +.stat-change-positive { + color: var(--color-success); +} + +.stat-change-negative { + color: var(--color-error); +} + +/* Tabs Component */ +.tabs { + display: flex; + border-bottom: 1px solid var(--color-gray-200); + margin-bottom: 1rem; +} + +.tab { + padding: 0.75rem 1rem; + font-weight: 500; + color: var(--color-gray-600); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all var(--transition-normal); +} + +.tab:hover { + color: var(--color-primary); +} + +.tab-active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); +} + +.tab-content { + display: none; +} + +.tab-content-active { + display: block; + animation: fadeIn var(--transition-normal); +} + +/* Dropdown Component */ +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-toggle { + cursor: pointer; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 10; + min-width: 10rem; + padding: 0.5rem 0; + margin-top: 0.5rem; + background-color: white; + border-radius: var(--border-radius-md); + box-shadow: var(--shadow-lg); + display: none; + animation: fadeInDown var(--transition-normal); +} + +.dropdown-menu-right { + left: auto; + right: 0; +} + +.dropdown-item { + display: block; + padding: 0.5rem 1rem; + color: var(--color-gray-700); + transition: background-color var(--transition-fast); +} + +.dropdown-item:hover { + background-color: var(--color-gray-100); + color: var(--color-gray-900); +} + +.dropdown:hover .dropdown-menu, +.dropdown.active .dropdown-menu { + display: block; +} + +/* Tooltip Component */ +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip-text { + visibility: hidden; + position: absolute; + z-index: 20; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + background-color: var(--color-gray-800); + color: white; + text-align: center; + padding: 0.5rem; + border-radius: var(--border-radius-md); + font-size: var(--font-size-xs); + white-space: nowrap; + opacity: 0; + transition: opacity var(--transition-normal); +} + +.tooltip-text::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: var(--color-gray-800) transparent transparent transparent; +} + +.tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +/* API Error Display */ +.api-error { + max-width: 600px; + margin: 2rem auto; + padding: 2rem; + background-color: #fff; + border-radius: var(--radius); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.api-error h2 { + color: var(--danger); + margin-bottom: 1rem; +} + +.error-details { + background-color: #f8f9fa; + padding: 1.5rem; + border-radius: var(--radius); + margin-top: 1rem; + text-align: left; +} + +.error-details ul { + margin: 1rem 0; + padding-left: 1.5rem; +} + +.error-details li { + margin-bottom: 0.5rem; +} + +.error-details button { + margin-top: 1rem; +} + +/* Documents Container */ +.documents-container { + min-height: 200px; + padding: 1rem 0; +} + +/* Error message */ +.error-message { + background-color: #fef2f2; + border: 1px solid #fee2e2; + color: #b91c1c; + padding: 1rem; + border-radius: var(--radius); + margin: 1rem 0; +} + +.error-message p { + margin: 0.5rem 0; +} + +.error-details { + font-family: monospace; + font-size: 0.85rem; + margin-top: 0.5rem; + padding: 0.5rem; + background-color: #fff; + border-radius: 0.25rem; + overflow-x: auto; +} + +/* Search Results */ +.search-results-section { + margin-top: 2rem; + display: none; +} + +.search-results-container { + min-height: 200px; + padding: 1rem 0; +} + +/* Tab content visibility */ +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Fix for search results section */ +.search-results-section.active { + display: block; +} diff --git a/frontend/favicon.ico b/frontend/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f013bb81b33600f826cef507712c949b1807b82f GIT binary patch literal 62 zcmezW&xS#fK^F)W7~B{V8Oj(k8Il?D8S;Sq0EQf(NHRkzLk3VJham;XDgugwRNFD| IGH@{f0F}cE?EnA( literal 0 HcmV?d00001 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..62ce128 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,2350 @@ + + + + + + AGC Document Finder + + + + + + + + + + + + + + + + + + +
+
+
+ AGC Logo +

AGC Document Finder

+
+ +
+ + + + + + +
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + +
+ + + +
+
+ + +
+
+

+ Recent Documents +

+
+ + +
+
+ + +
+ +
+
+
+ + + +
+ +
+ +
+ + +
+
+

Color Palette

+ +

Primary Colors (Navy)

+
+
+
+
+
Navy 900
+
#1e2d4c
+
+
+
+
+
+
Navy 700
+
#2c4272
+
+
+
+
+
+
Navy 500
+
#4065a2
+
+
+
+
+
+
Navy 300
+
#8eaed3
+
+
+
+
+
+
Navy 100
+
#dbe6f3
+
+
+
+ +

Secondary Colors (Gold)

+
+
+
+
+
Gold 900
+
#653310
+
+
+
+
+
+
Gold 700
+
#965709
+
+
+
+
+
+
Gold 500
+
#e29807
+
+
+
+
+
+
Gold 300
+
#f8cb47
+
+
+
+
+
+
Gold 100
+
#fdf1c7
+
+
+
+ +

UI Colors

+
+
+
+
+
Success
+
#059669
+
+
+
+
+
+
Warning
+
#d97706
+
+
+
+
+
+
Error
+
#dc2626
+
+
+
+
+
+
Gray
+
#f3f4f6
+
+
+
+
+
+ + +
+
+

Typography

+ +

Headings

+
+
+

Heading 1

+
+ font-size: 1.875rem (30px) | font-weight: 700 +
+
+
+

Heading 2

+
+ font-size: 1.5rem (24px) | font-weight: 600 +
+
+
+

Heading 3

+
+ font-size: 1.25rem (20px) | font-weight: 600 +
+
+
+

Heading 4

+
+ font-size: 1.125rem (18px) | font-weight: 600 +
+
+
+ +

Body Text

+
+
+

+ Regular text (16px): The Attorney General's Chambers of Malaysia + provides legal advice to the Government and represents the + Government in legal proceedings. +

+
+
+

+ Small text (14px): The Attorney General's Chambers of Malaysia + provides legal advice to the Government and represents the + Government in legal proceedings. +

+
+
+

+ Extra small text (12px): The Attorney General's Chambers of + Malaysia provides legal advice to the Government and represents + the Government in legal proceedings. +

+
+
+ +

Text Styling

+
+
+

Bold text

+

Semibold text

+

Medium text

+

Normal text

+

Italic text

+
+
+

Underlined text

+

Strikethrough text

+

Colored text

+

Colored text

+

+ Link text +

+
+
+
+
+ + +
+
+

Buttons

+ +

Button Variants

+
+ + + + + +
+ +

Button Sizes

+
+ + + +
+ +

Button with Icon

+
+ + + +
+
+
+ + +
+
+

Form Controls

+ +
+
+

Text Inputs

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

Other Form Elements

+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ +

Search Input

+
+ + + + +
+
+
+ + +
+
+

Cards

+ +
+ +
+
+

Basic Card

+
+
+

+ This is a basic card with header, body, and footer sections. +

+
+ +
+ + +
+
+
+ LKK +

+ Pendakwa Raya v. Ahmad bin Abdullah +

+

[2023] 5 MLJ 123

+
+ +
+

+ Criminal case involving drug trafficking under Section 39B of + the Dangerous Drugs Act 1952. +

+
+ Last accessed: 2 days ago +
+
+ + +
+
Total Documents
+
1,234
+
+ + + + 12% from last month +
+
+
+
+
+ + +
+
+

Badges

+ +

Badge Variants

+
+ Primary + Secondary + Success + Warning + Error +
+ +

Outline Badges

+
+ Primary + Secondary + Success + Warning + Error +
+
+
+ + +
+
+

Alerts & Toasts

+ +

Toast Notifications

+
+ + + + +
+ + + +
+
+ + +
+
+

Tabs

+ +
+
All Documents
+
LKK Reports
+
Legal Cases
+
Criminal Cases
+
+ +
+

This is the content for All Documents tab.

+
+
+

This is the content for LKK Reports tab.

+
+
+

This is the content for Legal Cases tab.

+
+
+

This is the content for Criminal Cases tab.

+
+
+
+ + +
+
+

Tooltips & Dropdowns

+ +

Tooltips

+
+
+ + This is a tooltip +
+ +
+ What is LKK? + Laporan Keputusan Kes (Case Decision Reports) +
+
+ +

Dropdowns

+
+ + + +
+
+
+ + +
+
+

Animations

+ +
+
+

Fade Animations

+
+ + + + + +
+
+
+

Other Animations

+
+ +
+ Pulse Animation +
+
+
+
+
+
+
+
+
+ + + + +
+
+
+

+ Search Results +

+
+ + +
+ +
+
+
+ + + +
+ +
+
+

+ AI Legal Assistant +

+
+ +
+
+
+ +
+

Intelligent Search

+

+ Ask complex legal questions and get precise answers drawn from + our extensive document database. +

+
+ +
+
+ +
+

Document Analysis

+

+ Get summaries, key points, and legal interpretations from + lengthy documents in seconds. +

+
+ +
+
+ +
+

Legal Guidance

+

+ Receive case-specific guidance and procedural information based + on historical precedents. +

+
+
+
+ + +
+ +
+
+ + AI Assistant Tools +
+
+ + + + + + +
+
+ + +
+
+
+ + Document Analysis Results +
+ +
+
+ +
+
+ +
+

+ AI Legal Assistant +

+
+ + +
+
+ +
+ +
Today, 10:30 AM
+ + +
+
+ Hello! I'm your AGC legal research assistant. I can help you + find information, analyze legal documents, and answer questions + about cases and legislation. How can I assist you today? +
+
10:30 AM
+
+ + +
+
+ Can you explain the process for cross-border financial fraud + investigations? +
+
10:32 AM
+
+ +
+
+ Cross-border financial fraud investigations typically follow + these key steps: 1. Initial complaint or detection through + financial monitoring systems 2. Preliminary assessment and + jurisdictional determination 3. Formation of investigation team, + potentially including international liaisons 4. Evidence + gathering through formal channels like + MLATs (Mutual Legal Assistance Treaties)International agreements that facilitate cooperation in + criminal investigations + 5. Coordination with foreign enforcement agencies 6. Joint + investigation procedures following established protocols 7. Case + building and prosecution preparation The process requires + careful attention to + jurisdictional issuesDetermining which court has authority to hear a case + and + admissibility of evidenceWhether evidence can be legally presented in court + collected abroad. Would you like more details on any specific + aspect of this process? +
+
10:33 AM
+ +
+ + + + +
+ + +
+ + + +
+ +
+
+ + +
+ +
+
+ + Responses are generated using AI and may require + verification +
+ +
+
+
+
+ + + + + + + + + + + + + + + diff --git a/frontend/index.html.bak b/frontend/index.html.bak new file mode 100644 index 0000000..3937caa --- /dev/null +++ b/frontend/index.html.bak @@ -0,0 +1,1822 @@ + + + + + + AGC Document Finder + + + + + + + + + + + + + +
+
+
+ AGC Logo +

AGC Document Chatbot

+
+ +
+ + + + + + +
+ + +
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+

+ Recent Documents +

+
+ + +
+
+ + +
+ +
+
+
+ + + + + +
+ +
+
+

+ AI Legal Assistant +

+
+ +
+
+
+ +
+

Intelligent Search

+

+ Ask complex legal questions and get precise answers drawn from + our extensive document database. +

+
+ +
+
+ +
+

Document Analysis

+

+ Get summaries, key points, and legal interpretations from + lengthy documents in seconds. +

+
+ +
+
+ +
+

Legal Guidance

+

+ Receive case-specific guidance and procedural information based + on historical precedents. +

+
+
+
+ + +
+
+

+ AI Legal Assistant +

+
+ + +
+
+ +
+ +
Today, 10:30 AM
+ + +
+
+ Hello! I'm your AGC legal research assistant. I can help you + find information, analyze legal documents, and answer questions + about cases and legislation. How can I assist you today? +
+
10:30 AM
+
+ + +
+
+ Can you explain the process for cross-border financial fraud + investigations? +
+
10:32 AM
+
+ +
+
+ Cross-border financial fraud investigations typically follow + these key steps: 1. Initial complaint or detection through + financial monitoring systems 2. Preliminary assessment and + jurisdictional determination 3. Formation of investigation team, + potentially including international liaisons 4. Evidence + gathering through formal channels like MLATs (Mutual Legal + Assistance Treaties) 5. Coordination with foreign enforcement + agencies 6. Joint investigation procedures following established + protocols 7. Case building and prosecution preparation The + process requires careful attention to jurisdictional issues and + admissibility of evidence collected abroad. Would you like more + details on any specific aspect of this process? +
+
10:33 AM
+ + +
+ + + +
+ +
+
+ + +
+ +
+
+ + Responses are generated using AI and may require + verification +
+ +
+
+
+
+
+ + + + + + + + + + + + + + diff --git a/frontend/js/api.js b/frontend/js/api.js new file mode 100644 index 0000000..e399042 --- /dev/null +++ b/frontend/js/api.js @@ -0,0 +1,118 @@ +/** + * AGC Document Search - API Service + * Handles communication with the backend API + */ + +const apiService = { + baseUrl: "http://localhost:8000", + + /** + * Check API connection + */ + async checkConnection() { + try { + const response = await fetch(`${this.baseUrl}/ping`); + return response.ok; + } catch (error) { + console.error("API connection failed:", error); + return false; + } + }, + + /** + * Get all document types + */ + async getDocumentTypes() { + try { + const response = await fetch(`${this.baseUrl}/document-types`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Error fetching document types:", error); + throw error; + } + }, + + /** + * Get documents with optional filters + * @param {Object} filters - Optional filters (doc_type, title_filter) + */ + async getDocuments(filters = {}) { + try { + // Build query params + const params = new URLSearchParams(); + if (filters.doc_type) params.append("doc_type", filters.doc_type); + if (filters.title_filter) + params.append("title_filter", filters.title_filter); + + const response = await fetch(`${this.baseUrl}/documents?${params}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error("Error fetching documents:", error); + throw error; + } + }, + + /** + * Get document by ID + * @param {string|number} documentId - The document ID + */ + async getDocument(documentId) { + try { + // Handle numeric IDs by converting them to our doc format if needed + let formattedId = documentId; + if ( + typeof documentId === "number" || + (!isNaN(parseInt(documentId)) && !documentId.startsWith("doc")) + ) { + const numId = parseInt(documentId); + formattedId = `doc${numId}`; + console.log(`Converted numeric ID to document ID: ${formattedId}`); + } + + console.log(`Fetching document with ID: ${formattedId}`); + const response = await fetch(`${this.baseUrl}/document/${formattedId}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error(`Error fetching document ${documentId}:`, error); + throw error; + } + }, + + /** + * Search documents + * @param {string} query - Search query + * @param {boolean} profileSearch - Whether to use profile search + */ + async searchDocuments(query, profileSearch = false) { + try { + const response = await fetch(`${this.baseUrl}/search`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: query, + profile_search: profileSearch, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error searching documents:", error); + throw error; + } + }, +}; diff --git a/frontend/js/main.js b/frontend/js/main.js new file mode 100644 index 0000000..7564bb0 --- /dev/null +++ b/frontend/js/main.js @@ -0,0 +1,1820 @@ +/** + * AGC Document Search - Main Application + * Handles application state management and UI interactions + */ + +// Application State +const appState = { + isInitializing: true, + isOnline: navigator.onLine, + apiStatus: "connecting", + + // Search State + searchQuery: "", + searchResults: null, + searchHistory: loadFromStorage("searchHistory") || [], + + // Documents State + documents: [], + documentTypes: ["All Types"], + selectedDocument: null, + bookmarks: loadFromStorage("bookmarks") || [], + + // UI State + activeTab: "all-docs", + viewMode: loadFromStorage("viewMode") || "grid", + + // Loading States + loading: { + search: false, + documents: false, + }, + + // AI Chat + aiChat: { + messages: loadFromStorage("chatMessages") || [], + isTyping: false, + }, +}; + +// DOM Elements +const elements = { + // Safely get elements with a fallback + getElement: function (id) { + const element = document.getElementById(id); + if (!element) { + console.warn(`Element with ID '${id}' not found in the DOM`); + } + return element; + }, + + get searchInput() { + return this.getElement("mainSearchInput"); + }, + get searchButton() { + return this.getElement("mainSearchButton"); + }, + get searchResultsSection() { + return this.getElement("resultsSection"); + }, + get searchResultsContainer() { + return this.getElement("searchResultsContainer"); + }, + get recentDocumentsContainer() { + return this.getElement("recentDocumentsContainer"); + }, + get chatMessages() { + return this.getElement("chatMessages"); + }, + get chatInput() { + return this.getElement("chatInput"); + }, + get sendMessageButton() { + return this.getElement("chatSendBtn"); + }, + get mainContent() { + return this.getElement("mainContent"); + }, + + // For elements that use querySelectorAll + get tabButtons() { + return document.querySelectorAll(".tab") || []; + }, + + // Other elements + get mobileMenuButton() { + return this.getElement("mobileMenuToggle"); + }, + get mobileMenu() { + return this.getElement("mobileMenu"); + }, + + // Will be created dynamically + documentViewerModal: null, +}; + +// Utility Functions +function formatDate(dateString) { + if (!dateString) return ""; + return new Date(dateString).toLocaleDateString("en-MY", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +function truncateText(text, maxLength = 100) { + if (!text || text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; +} + +function debounce(func, wait = 300) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +function loadFromStorage(key) { + try { + const data = localStorage.getItem(`agc_${key}`); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error(`Error loading ${key} from storage:`, error); + return null; + } +} + +function saveToStorage(key, data) { + try { + localStorage.setItem(`agc_${key}`, JSON.stringify(data)); + return true; + } catch (error) { + console.error(`Error saving ${key} to storage:`, error); + return false; + } +} + +// Helper function for relevancy scoring +function getRelevancyInfo(similarity) { + const score = similarity || Math.random() * 0.6 + 0.4; // Default random score if not provided + const percent = Math.round(score * 100); + + let className = "low-relevance"; + if (score >= 0.8) { + className = "high-relevance"; + } else if (score >= 0.6) { + className = "medium-relevance"; + } + + return { + score, + percent, + className, + display: score.toFixed(3), + }; +} + +// Search Functions +async function performSearch(query, docType = "All Types") { + if (!query.trim()) return; + + // Switch to search tab to show results + switchTab("search"); + + // Update UI to loading state + appState.loading.search = true; + updateLoadingState(); + + // Show query enhancement + displayQueryEnhancement(query); + + try { + // Perform search via API + const results = await apiService.searchDocuments(query); + + // Update application state + appState.searchResults = results; + + // Add to search history + if (!appState.searchHistory.some((item) => item.query === query)) { + appState.searchHistory.unshift({ + query, + timestamp: new Date().toISOString(), + count: results.documents.length, + }); + + // Keep last 20 searches + if (appState.searchHistory.length > 20) { + appState.searchHistory = appState.searchHistory.slice(0, 20); + } + + saveToStorage("searchHistory", appState.searchHistory); + } + + // Update UI with results + displaySearchResults(results); + } catch (error) { + console.error("Search failed:", error); + elements.searchResultsContainer.innerHTML = ` +
+

Sorry, the search failed. Please try again.

+

${error.message}

+
+ `; + } finally { + appState.loading.search = false; + updateLoadingState(); + + // Show results section if hidden (only when on search tab) + if (appState.activeTab === "search") { + elements.searchResultsSection.classList.add("visible"); + } + } +} + +function displaySearchResults(results) { + const { documents, query, enhanced_query, answer } = results; + + if (!documents || documents.length === 0) { + elements.searchResultsContainer.innerHTML = ` +
+

No results found for "${query}"

+

Try using different keywords or browse all documents.

+
+ `; + return; + } + + let resultsHTML = ""; + + // Add AI answer if available + if (answer) { + resultsHTML += ` +
+

AI Analysis

+

${answer}

+
+ `; + } + + // Enhanced query display + if (enhanced_query && enhanced_query !== query) { + resultsHTML += ` +
+

Showing results for: ${enhanced_query}

+

Original search: ${query}

+
+ `; + } + + // Results count + resultsHTML += ` +
+

Found ${documents.length} result${ + documents.length !== 1 ? "s" : "" + }

+
+ `; + + // Document results + resultsHTML += '
'; + + documents.forEach((doc) => { + const docTitle = doc.title || "Untitled Document"; + const docContent = doc.content || "No content available"; + const docType = doc.doc_type || "Unknown"; + const docDate = formatDate(doc.created_at); + const isBookmarked = appState.bookmarks.includes(doc.id); + + // Get relevancy information + const relevancy = getRelevancyInfo(doc.similarity); + + resultsHTML += ` +
+
+
+
${docType}
+

${docTitle}

+
+
+ Relevance: + ${relevancy.display} +
+
+
+
+
+
+

${truncateText(docContent, 200)}

+
+ +
+ `; + }); + + resultsHTML += "
"; + + elements.searchResultsContainer.innerHTML = resultsHTML; +} + +// Query Enhancement Display +function displayQueryEnhancement(originalQuery) { + const enhancementDiv = document.getElementById("queryEnhancement"); + const originalQuerySpan = document.getElementById("originalQuery"); + const enhancedQuerySpan = document.getElementById("enhancedQuery"); + const keywordsDiv = document.getElementById("extractedKeywords"); + + if (!enhancementDiv) return; + + // Simulate AI query enhancement (in real implementation, this would come from API) + const enhancedQuery = generateEnhancedQuery(originalQuery); + const keywords = extractKeywords(originalQuery); + + originalQuerySpan.textContent = originalQuery; + enhancedQuerySpan.textContent = enhancedQuery; + + // Display keywords + keywordsDiv.innerHTML = ""; + keywords.forEach((keyword) => { + const keywordSpan = document.createElement("span"); + keywordSpan.className = "query-keyword"; + keywordSpan.textContent = keyword; + keywordsDiv.appendChild(keywordSpan); + }); + + // Show the enhancement panel + enhancementDiv.classList.add("visible"); + + // Auto-hide after 5 seconds + setTimeout(() => { + enhancementDiv.classList.remove("visible"); + }, 5000); +} + +// Generate enhanced query (simulated AI enhancement) +function generateEnhancedQuery(query) { + const legalTerms = { + fraud: + "financial fraud OR criminal misrepresentation OR deceptive practices", + investigation: + "criminal investigation OR forensic examination OR evidence gathering", + "cross-border": + "international jurisdiction OR transnational crime OR mutual legal assistance", + "money laundering": + "anti-money laundering OR financial crime OR proceeds of crime", + corruption: "public corruption OR bribery OR abuse of office", + contract: + "contractual dispute OR breach of contract OR commercial agreement", + }; + + let enhanced = query.toLowerCase(); + + Object.keys(legalTerms).forEach((term) => { + if (enhanced.includes(term)) { + enhanced = enhanced.replace(new RegExp(term, "gi"), legalTerms[term]); + } + }); + + return enhanced; +} + +// Extract keywords from query +function extractKeywords(query) { + const stopWords = [ + "the", + "a", + "an", + "and", + "or", + "but", + "in", + "on", + "at", + "to", + "for", + "of", + "with", + "by", + "is", + "are", + "was", + "were", + "be", + "been", + "being", + "have", + "has", + "had", + "do", + "does", + "did", + "will", + "would", + "could", + "should", + ]; + + return query + .toLowerCase() + .replace(/[^\w\s]/g, "") + .split(/\s+/) + .filter((word) => word.length > 2 && !stopWords.includes(word)) + .slice(0, 8); // Limit to 8 keywords +} + +// Advanced Filtering Functions +function setupAdvancedFilters() { + const dateFilter = document.getElementById("dateFilter"); + const customDateRange = document.getElementById("customDateRange"); + const customDateRangeTo = document.getElementById("customDateRangeTo"); + const applyFiltersBtn = document.getElementById("applyFiltersBtn"); + const resetFiltersBtn = document.getElementById("resetFiltersBtn"); + const saveFiltersBtn = document.getElementById("saveFiltersBtn"); + + // Handle custom date range visibility + if (dateFilter) { + dateFilter.addEventListener("change", (e) => { + if (e.target.value === "custom") { + customDateRange.style.display = "block"; + customDateRangeTo.style.display = "block"; + } else { + customDateRange.style.display = "none"; + customDateRangeTo.style.display = "none"; + } + }); + } + + // Apply filters + if (applyFiltersBtn) { + applyFiltersBtn.addEventListener("click", applyAdvancedFilters); + } + + // Reset filters + if (resetFiltersBtn) { + resetFiltersBtn.addEventListener("click", resetAllFilters); + } + + // Save filter preset + if (saveFiltersBtn) { + saveFiltersBtn.addEventListener("click", saveFilterPreset); + } +} + +function applyAdvancedFilters() { + const filters = { + docType: document.getElementById("docTypeFilter")?.value || "All Types", + dateRange: document.getElementById("dateFilter")?.value || "all", + title: document.getElementById("titleFilter")?.value || "", + jurisdiction: document.getElementById("jurisdictionFilter")?.value || "all", + legalArea: document.getElementById("legalAreaFilter")?.value || "all", + startDate: document.getElementById("startDate")?.value || "", + endDate: document.getElementById("endDate")?.value || "", + }; + + console.log("Applying advanced filters:", filters); + + // Apply filters to current document view + loadDocuments(filters.docType, filters); +} + +function resetAllFilters() { + document.getElementById("docTypeFilter").value = "All Types"; + document.getElementById("dateFilter").value = "all"; + document.getElementById("titleFilter").value = ""; + document.getElementById("jurisdictionFilter").value = "all"; + document.getElementById("legalAreaFilter").value = "all"; + document.getElementById("startDate").value = ""; + document.getElementById("endDate").value = ""; + + // Hide custom date range + document.getElementById("customDateRange").style.display = "none"; + document.getElementById("customDateRangeTo").style.display = "none"; + + // Reload documents without filters + loadDocuments("All Types"); +} + +function saveFilterPreset() { + const filters = { + docType: document.getElementById("docTypeFilter")?.value || "All Types", + dateRange: document.getElementById("dateFilter")?.value || "all", + title: document.getElementById("titleFilter")?.value || "", + jurisdiction: document.getElementById("jurisdictionFilter")?.value || "all", + legalArea: document.getElementById("legalAreaFilter")?.value || "all", + }; + + const presetName = prompt("Enter a name for this filter preset:"); + if (presetName) { + const savedPresets = loadFromStorage("filterPresets") || {}; + savedPresets[presetName] = filters; + saveToStorage("filterPresets", savedPresets); + showNotification( + "success", + `Filter preset "${presetName}" saved successfully!` + ); + } +} + +// AI Assistant Enhanced Functions +function setupAIAssistantTools() { + const analyzeDocBtn = document.getElementById("analyzeDocBtn"); + const summarizeBtn = document.getElementById("summarizeBtn"); + const legalConceptsBtn = document.getElementById("legalConceptsBtn"); + const crossReferenceBtn = document.getElementById("crossReferenceBtn"); + const precedentSearchBtn = document.getElementById("precedentSearchBtn"); + const caseAnalysisBtn = document.getElementById("caseAnalysisBtn"); + const closeAnalysisPanel = document.getElementById("closeAnalysisPanel"); + + if (analyzeDocBtn) { + analyzeDocBtn.addEventListener("click", () => analyzeDocument()); + } + if (summarizeBtn) { + summarizeBtn.addEventListener("click", () => generateSummary()); + } + if (legalConceptsBtn) { + legalConceptsBtn.addEventListener("click", () => explainLegalConcepts()); + } + if (crossReferenceBtn) { + crossReferenceBtn.addEventListener("click", () => findCrossReferences()); + } + if (precedentSearchBtn) { + precedentSearchBtn.addEventListener("click", () => searchPrecedents()); + } + if (caseAnalysisBtn) { + caseAnalysisBtn.addEventListener("click", () => performCaseAnalysis()); + } + if (closeAnalysisPanel) { + closeAnalysisPanel.addEventListener("click", () => hideAnalysisPanel()); + } +} + +function analyzeDocument() { + const panel = document.getElementById("documentAnalysisPanel"); + const content = document.getElementById("analysisContent"); + + if (!appState.selectedDocument) { + showNotification("warning", "Please select a document to analyze first."); + return; + } + + // Simulate document analysis + content.innerHTML = ` +

Document Analysis: ${appState.selectedDocument.title}

+
+ Document Type: ${appState.selectedDocument.doc_type}
+ Complexity Level: High
+ Legal Areas: Criminal Law, International Law
+ Key Entities: + ProsecutionThe legal party pursuing the case, + DefenseThe legal party defending against charges, + Court of AppealHigher court reviewing lower court decisions +
+
+ Summary: This document contains detailed proceedings of a complex financial fraud case involving international jurisdictions... +
+ `; + + panel.classList.add("visible"); +} + +function generateSummary() { + addChatMessage( + "Generate a comprehensive summary of the most recent legal document.", + "user" + ); + + setTimeout(() => { + const summaryMessage = `Based on the document analysis, here's a comprehensive summary: + +**Key Points:** +โ€ข Primary legal issues involve cross-border financial fraud +โ€ข Multiple jurisdictions are involved (Malaysia, Singapore, UK) +โ€ข Evidence collection spans 18 months +โ€ข 7 defendants across 3 countries + +**Legal Precedents:** +โ€ข Similar cases: Wong v. State (2019), International Fraud Case No. 245/2020 +โ€ข Applicable laws: AMLA 2001, Financial Services Act 2013 + +**Outcome:** +โ€ข 5 convictions, 2 acquittals +โ€ข Total restitution: RM 45.2 million +โ€ข Appeal pending in Federal Court`; + + addChatMessage(summaryMessage, "assistant"); + }, 1500); +} + +function explainLegalConcepts() { + addChatMessage( + "Explain the key legal concepts mentioned in recent conversations.", + "user" + ); + + setTimeout(() => { + const conceptsMessage = `Here are explanations for key legal concepts: + +**MLATs (Mutual Legal Assistance Treaties):** +International agreements between countries to facilitate cooperation in criminal investigations and prosecutions. + +**Jurisdictional Issues:** +Questions about which court or legal system has the authority to hear a particular case, especially important in cross-border crimes. + +**Admissibility of Evidence:** +The legal standard determining whether evidence can be presented in court proceedings, considering factors like how it was obtained and its relevance. + +**Cross-Border Financial Fraud:** +Financial crimes that span multiple countries, requiring international cooperation for investigation and prosecution.`; + + addChatMessage(conceptsMessage, "assistant"); + }, 2000); +} + +function findCrossReferences() { + addChatMessage( + "Find documents and cases that cross-reference with the current topic.", + "user" + ); + + setTimeout(() => { + const crossRefMessage = `Found several cross-references: + +**Related Cases:** +โ€ข PP v. Lim Financial Fraud (2021) - Similar MLAT procedures +โ€ข International Banking Fraud Case 156/2020 - Evidence admissibility precedent +โ€ข Cross-Border Money Laundering v. State (2019) - Jurisdictional determination + +**Referenced Legislation:** +โ€ข Anti-Money Laundering Act 2001 +โ€ข Mutual Assistance in Criminal Matters Act 2002 +โ€ข Financial Services Act 2013 + +**International Treaties:** +โ€ข UNCAC (UN Convention Against Corruption) +โ€ข ASEAN MLAT Framework +โ€ข Commonwealth Scheme for Mutual Legal Assistance`; + + addChatMessage(crossRefMessage, "assistant"); + }, 1800); +} + +function searchPrecedents() { + addChatMessage( + "Search for legal precedents related to the current case or topic.", + "user" + ); + + setTimeout(() => { + const precedentsMessage = `Relevant legal precedents found: + +**Binding Precedents:** +1. **Federal Court - Wong Ah Kow v. PP (2020)** + - Established framework for international evidence collection + - Relevance: 95% match + +2. **Court of Appeal - State v. International Banking Corp (2019)** + - Guidelines for cross-border financial investigations + - Relevance: 87% match + +**Persuasive Precedents:** +1. **High Court - Lee Financial Services v. State (2021)** + - MLAT application procedures + - Relevance: 78% match + +**International Precedents:** +1. **Singapore High Court - Trans-Asian Fraud Case (2020)** + - Similar factual matrix + - Persuasive value in Malaysian courts`; + + addChatMessage(precedentsMessage, "assistant"); + }, 2200); +} + +function performCaseAnalysis() { + addChatMessage( + "Perform a detailed legal case analysis of the current matter.", + "user" + ); + + setTimeout(() => { + const analysisMessage = `Comprehensive Case Analysis: + +**Facts Summary:** +Complex financial fraud involving multiple jurisdictions, defendants across 3 countries, total loss RM 45.2M. + +**Legal Issues:** +1. Jurisdictional competence of Malaysian courts +2. Admissibility of foreign evidence +3. Application of MLAT procedures +4. Cross-border asset recovery + +**Legal Analysis:** +- **Jurisdiction:** Established under s. 3 AMLA 2001 +- **Evidence:** Foreign evidence admissible per MACMA 2002 +- **Procedure:** MLAT requests properly executed +- **Recovery:** Asset forfeiture orders enforceable + +**Likely Outcome:** +Strong prosecution case with 85% conviction probability based on similar precedents. + +**Recommendations:** +- Ensure all MLAT procedures properly documented +- Consider plea negotiations for minor defendants +- Prepare for appellate challenges on jurisdictional grounds`; + + addChatMessage(analysisMessage, "assistant"); + }, 2500); +} + +function hideAnalysisPanel() { + const panel = document.getElementById("documentAnalysisPanel"); + panel.classList.remove("visible"); +} + +// Document Functions +async function loadDocuments(docType = "All Types") { + appState.loading.documents = true; + updateLoadingState(); + + try { + // Load documents based on document type + const filters = { + doc_type: docType !== "All Types" ? docType : undefined, + }; + + const documents = await apiService.getDocuments(filters); + appState.documents = documents; + + // Display documents in recent documents section + displayRecentDocuments(documents.slice(0, 6)); // Show first 6 documents + } catch (error) { + console.error("Failed to load documents:", error); + + // Check if the container exists before attempting to update it + const container = document.getElementById("recentDocumentsContainer"); + if (container) { + container.innerHTML = ` +
+

Sorry, we couldn't load the documents. Please try again.

+

${error.message}

+
+ `; + } + + // Show error notification + showNotification( + "error", + "Failed to load documents. Please check the console for details." + ); + } finally { + appState.loading.documents = false; + updateLoadingState(); + } +} + +function displayRecentDocuments(documents) { + // Check if the container element exists + const recentDocumentsContainer = document.getElementById( + "recentDocumentsContainer" + ); + if (!recentDocumentsContainer) { + console.warn("Recent documents container not found in the DOM"); + return; + } + + if (!documents || documents.length === 0) { + recentDocumentsContainer.innerHTML = ` +
+

No recent documents found.

+
+ `; + return; + } + + // Choose container class based on view mode + const containerClass = + appState.viewMode === "list" ? "documents-list" : "documents-grid"; + const cardClass = + appState.viewMode === "list" ? "document-card list-view" : "document-card"; + + let documentsHTML = `
`; + + documents.forEach((doc) => { + const docTitle = doc.title || "Untitled Document"; + const docContent = doc.content || "No content available"; + const docType = doc.doc_type || "Unknown"; + const docDate = formatDate(doc.created_at); + const isBookmarked = appState.bookmarks.includes(doc.id); + + documentsHTML += ` +
+
+
${docType}
+

${docTitle}

+
+
+

${truncateText( + docContent, + appState.viewMode === "list" ? 150 : 100 + )}

+
+ +
+ `; + }); + + documentsHTML += "
"; + + recentDocumentsContainer.innerHTML = documentsHTML; +} + +async function loadDocumentTypes() { + try { + const response = await apiService.getDocumentTypes(); + if (response && response.document_types) { + // Add "All Types" as the first option + appState.documentTypes = ["All Types", ...response.document_types]; + + // Update UI elements that use document types (e.g., filter dropdowns) + updateDocumentTypeSelectors(); + } + } catch (error) { + console.error("Failed to load document types:", error); + } +} + +function updateDocumentTypeSelectors() { + // For future implementation - update any dropdowns or filters with document types +} + +// View document details +window.viewDocument = async function (documentId) { + try { + console.log(`Viewing document with ID: ${documentId}`); + showLoader(); + + // Convert any numeric IDs to our document format + if (typeof documentId === "number" || !isNaN(parseInt(documentId, 10))) { + console.log(`Converting numeric ID ${documentId} to string format`); + // If it's a number between 1-6, convert to our doc format + const numId = parseInt(documentId, 10); + if (numId >= 1 && numId <= 6) { + documentId = `doc${numId}`; + console.log(`Converted to: ${documentId}`); + } + } + + // Fetch document from API + const document = await apiService.getDocument(documentId); + + // Extract legal information from content + const extractedData = extractLegalInfo(document.content); + + // Create modal if it doesn't exist + let modal = window.document.querySelector(".document-modal"); + if (!modal) { + modal = window.document.createElement("div"); + modal.className = "document-modal"; + modal.innerHTML = ` +
+
+
+

+ +
+
+ +
+ `; + window.document.body.appendChild(modal); + + // Add event listeners + const closeButtons = modal.querySelectorAll(".document-modal-close"); + closeButtons.forEach((button) => { + button.addEventListener("click", () => { + modal.classList.add("hidden"); + }); + }); + + modal + .querySelector(".document-modal-overlay") + .addEventListener("click", () => { + modal.classList.add("hidden"); + }); + } + + // Update modal title + modal.querySelector(".document-modal-title").textContent = + document.title || `Document ${documentId}`; + + // Build the modal content with structured information + let modalContent = ` +
+
+ Type: + ${ + document.doc_type || "Unknown" + } +
+ ${ + formatDate(document.created_at) + ? ` +
+ Date: + ${formatDate( + document.created_at + )} +
` + : "" + } + ${ + document.source + ? ` +
+ Source: + ${document.source} +
+ ` + : "" + } +
+ `; + + // Add extracted information if available + modalContent += generateDocumentSummary(extractedData); + + // Add full content section + modalContent += ` +
+

Full Document Content

+
+
${
+            document.content || "No content available"
+          }
+
+
+ `; + + // Update modal body + modal.querySelector(".document-modal-body").innerHTML = modalContent; + + // Add styles for new elements if not already present + if (!window.document.querySelector("style#document-details-styles")) { + const detailsStyles = window.document.createElement("style"); + detailsStyles.id = "document-details-styles"; + detailsStyles.textContent = ` + .section-title { + margin: 0 0 0.75rem 0; + font-size: 1.1rem; + color: var(--primary-dark); + display: flex; + align-items: center; + gap: 0.5rem; + } + + .full-content-section { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--gray-light); + } + + .full-content-container { + background-color: #f8f9fa; + border-radius: var(--radius); + border: 1px solid var(--gray-light); + padding: 1rem; + max-height: 400px; + overflow-y: auto; + } + + .full-content-text { + font-family: monospace; + font-size: 0.9rem; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + margin: 0; + } + + .document-summary { + margin: 1rem 0; + } + + .info-section { + margin-bottom: 1rem; + padding: 1rem; + background-color: #f8f9fa; + border-radius: var(--radius); + border-left: 3px solid var(--primary); + } + + .info-section h4 { + margin: 0 0 0.75rem 0; + color: var(--primary-dark); + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .info-section p { + margin: 0.5rem 0; + font-size: 0.9rem; + } + + .acts-list, + .persons-list { + margin: 0.5rem 0; + padding-left: 0; + list-style: none; + } + + .act-item, + .person-item { + background-color: rgba(59, 130, 246, 0.1); + margin: 0.25rem 0; + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + font-size: 0.9rem; + border-left: 3px solid var(--primary); + } + + .person-item { + border-left-color: var(--warning); + background-color: rgba(245, 158, 11, 0.1); + } + + .person-name { + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 0.35rem; + } + + .person-role, + .person-category { + font-size: 0.85rem; + margin-top: 0.25rem; + } + + .role-label, + .category-label { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 500; + } + + .role-label { + background-color: var(--primary); + color: white; + } + + .category-label { + background-color: var(--gray-light); + color: var(--gray-dark); + } + + .allegation-item { + margin: 0.75rem 0; + padding: 0.75rem; + background-color: rgba(239, 68, 68, 0.05); + border-radius: 0.25rem; + border-left: 3px solid var(--danger); + } + + .allegation-preview { + margin: 0.5rem 0 0 0; + font-size: 0.9rem; + color: var(--gray); + font-style: italic; + } + + .status-ongoing { + color: var(--primary); + font-weight: 600; + } + + .status-selesai, + .status-completed { + color: var(--success); + font-weight: 600; + } + + .status-pending { + color: var(--warning); + font-weight: 600; + } + `; + window.document.head.appendChild(detailsStyles); + } + + // Show modal + modal.classList.remove("hidden"); + } catch (error) { + console.error("Error loading document:", error); + showNotification( + "error", + `Failed to load document details: ${error.message}` + ); + } finally { + hideLoader(); + } +}; + +function hideDocumentViewer() { + if (elements.documentViewerModal) { + elements.documentViewerModal.classList.add("hidden"); + } +} + +function toggleBookmark(documentId) { + if (!documentId) return; + + const bookmarks = [...appState.bookmarks]; + const index = bookmarks.indexOf(documentId); + + if (index === -1) { + // Add bookmark + bookmarks.push(documentId); + } else { + // Remove bookmark + bookmarks.splice(index, 1); + } + + // Update state and storage + appState.bookmarks = bookmarks; + saveToStorage("bookmarks", bookmarks); + + // Update UI + document + .querySelectorAll(`.bookmark-button[onclick*="${documentId}"]`) + .forEach((button) => { + if (index === -1) { + button.classList.add("bookmarked"); + } else { + button.classList.remove("bookmarked"); + } + }); +} + +// Tab Navigation +function switchTab(tabId) { + // Update active tab state + appState.activeTab = tabId; + + // Get all tab links and remove active class + const mainTabLinks = document.querySelectorAll(".main-tab-link"); + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active class to the corresponding tab + const activeTab = document.querySelector(`.main-tab-link[href="#${tabId}"]`); + if (activeTab) { + activeTab.classList.add("active"); + } + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + + // Handle search results visibility based on active tab + if (elements.searchResultsSection) { + if (tabId === "search") { + // Show search results if they exist and we're on search tab + if (appState.searchResults) { + elements.searchResultsSection.classList.add("visible"); + } + } else { + // Hide search results when not on search tab + elements.searchResultsSection.classList.remove("visible"); + } + } + + // Load tab-specific content + if (tabId === "browse") { + loadDocuments("All Types"); + } else if (tabId === "search") { + // Any search-specific initialization + } else if (tabId === "chat") { + // Any chat-specific initialization + } +} + +// AI Chat Functions +function sendChatMessage() { + const message = elements.chatInput.value.trim(); + if (!message) return; + + // Add user message to UI + addChatMessage(message, "user"); + + // Clear input + elements.chatInput.value = ""; + + // Set typing indicator + appState.aiChat.isTyping = true; + updateChatTypingIndicator(); + + // Show API connection error message + setTimeout(() => { + const aiResponse = + "The AI assistant feature is currently unavailable. Please ensure the AI service is properly configured and connected."; + + // Add error response to UI + addChatMessage(aiResponse, "system"); + + // Clear typing indicator + appState.aiChat.isTyping = false; + updateChatTypingIndicator(); + }, 1000); +} + +function addChatMessage(message, role) { + // Add to state + appState.aiChat.messages.push({ + role, + content: message, + timestamp: new Date().toISOString(), + }); + + // Save to storage (limited to last 50 messages) + if (appState.aiChat.messages.length > 50) { + appState.aiChat.messages = appState.aiChat.messages.slice(-50); + } + saveToStorage("chatMessages", appState.aiChat.messages); + + // Add to UI + const chatMessageElement = document.createElement("div"); + + if (role === "user") { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } else if (role === "system") { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } else { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } + + elements.chatMessages.appendChild(chatMessageElement); + + // Scroll to bottom + elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; +} + +function updateChatTypingIndicator() { + // Remove existing typing indicator if any + const existingIndicator = + elements.chatMessages.querySelector(".typing-indicator"); + if (existingIndicator) { + existingIndicator.remove(); + } + + if (appState.aiChat.isTyping) { + // Add typing indicator + const indicatorElement = document.createElement("div"); + indicatorElement.className = "typing-indicator"; + indicatorElement.innerHTML = ` +
+
+
+
+
+ `; + elements.chatMessages.appendChild(indicatorElement); + elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; + } +} + +// UI Helper Functions +function updateLoadingState() { + // Update loading indicators based on appState.loading + const searchButton = document.getElementById("mainSearchButton"); + if (searchButton) { + if (appState.loading.search) { + searchButton.innerHTML = ''; + searchButton.disabled = true; + } else { + searchButton.innerHTML = ''; + searchButton.disabled = false; + } + } + + // Other loading indicators can be updated here +} + +function showLoadingOverlay() { + let overlay = document.querySelector(".loading-overlay"); + + if (!overlay) { + overlay = document.createElement("div"); + overlay.className = "loading-overlay"; + overlay.innerHTML = ` +
+ +
+ `; + document.body.appendChild(overlay); + } + + overlay.classList.remove("hidden"); +} + +function hideLoadingOverlay() { + const overlay = document.querySelector(".loading-overlay"); + if (overlay) { + overlay.classList.add("hidden"); + } +} + +// Initialize Application +async function initializeApp() { + console.log("๐Ÿš€ Initializing AGC Document Search Application..."); + + // Set initial state + appState.isInitializing = true; + + try { + // Check API connection + console.log("๐Ÿ“ก Checking API connection..."); + const isConnected = await apiService.checkConnection(); + + if (isConnected) { + appState.apiStatus = "connected"; + console.log("โœ… API connection successful"); + + // Load initial data + await Promise.all([loadDocumentTypes(), loadDocuments()]); + + // Load chat history + loadChatHistory(); + + console.log("๐Ÿ“š Initial data loaded successfully"); + } else { + appState.apiStatus = "disconnected"; + console.warn("โš ๏ธ API connection failed - running in offline mode"); + + // Show error message to user + showNotification( + "error", + "Unable to connect to the server. Some features may be limited." + ); + + // Load cached data if available + const cachedDocuments = loadFromStorage("cachedDocuments"); + if (cachedDocuments) { + appState.documents = cachedDocuments; + displayRecentDocuments(cachedDocuments); + } + } + } catch (error) { + console.error("โŒ Initialization failed:", error); + appState.apiStatus = "error"; + + showNotification( + "error", + "Failed to initialize the application. Please refresh the page." + ); + } finally { + appState.isInitializing = false; + updateLoadingState(); + } + + // Setup event listeners and UI components + setupEventListeners(); + setupAdvancedFilters(); + setupAIAssistantTools(); + setupViewToggle(); + + console.log("๐ŸŽ‰ Application initialized successfully!"); +} + +function setupEventListeners() { + // Search + if (elements.searchButton) { + elements.searchButton.addEventListener("click", () => { + if (elements.searchInput) { + performSearch(elements.searchInput.value); + } + }); + } + + if (elements.searchInput) { + elements.searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + performSearch(elements.searchInput.value); + } + }); + } + + // Tab navigation - fix for the main tabs + const mainTabLinks = document.querySelectorAll(".main-tab-link"); + mainTabLinks.forEach((tab) => { + tab.addEventListener("click", (e) => { + e.preventDefault(); // Prevent default anchor behavior + + // Remove active class from all tabs + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active class to clicked tab + tab.classList.add("active"); + + // Get the tab ID from the href attribute + const tabId = tab.getAttribute("href").substring(1); // Remove # from href + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + }); + }); + + // Mobile menu tab navigation + const mobileMenuLinks = document.querySelectorAll(".mobile-menu a"); + mobileMenuLinks.forEach((link) => { + link.addEventListener("click", (e) => { + e.preventDefault(); + const tabId = link.getAttribute("href").substring(1); + + // Hide mobile menu after selection + const mobileMenu = document.getElementById("mobileMenu"); + if (mobileMenu) mobileMenu.classList.remove("active"); + + // Remove active class from all main tabs + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active to the corresponding main tab + const correspondingTab = document.querySelector( + `.main-tab-link[href="#${tabId}"]` + ); + if (correspondingTab) correspondingTab.classList.add("active"); + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + }); + }); + + // Mobile menu toggle + if (elements.mobileMenuButton && elements.mobileMenu) { + elements.mobileMenuButton.addEventListener("click", () => { + elements.mobileMenu.classList.toggle("active"); + }); + } + + // Chat + if (elements.sendMessageButton) { + elements.sendMessageButton.addEventListener("click", sendChatMessage); + } + + if (elements.chatInput) { + elements.chatInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + sendChatMessage(); + } + }); + } +} + +function loadChatHistory() { + // Display existing chat messages + appState.aiChat.messages.forEach((message) => { + addChatMessage(message.content, message.role); + }); +} + +// Export functions for global access (needed for onclick handlers) +window.toggleBookmark = toggleBookmark; + +// Initialize the application when DOM is loaded +document.addEventListener("DOMContentLoaded", initializeApp); + +// Utility functions for document extraction +function extractLegalInfo(content) { + if (!content) return {}; + + const info = { + fileNumber: extractField(content, "File Number"), + status: extractField(content, "Status"), + caseNumber: extractField(content, "Case Number"), + dppSuggestion: extractField(content, "DPP Suggestion"), + hodDecision: extractField(content, "HOD Decision"), + allegations: extractAllegations(content), + persons: extractPersons(content), + acts: extractActs(content), + }; + return info; +} + +// Helper function to extract field values +function extractField(content, fieldName) { + const regex = new RegExp(`${fieldName}:\\s*([^\\n]+)`, "i"); + const match = content.match(regex); + return match ? match[1].trim() : null; +} + +// Extract allegations information +function extractAllegations(content) { + const allegations = []; + const allegationPattern = + /ALLEGATION #(\d+):([\s\S]*?)(?=ALLEGATION #|\n--- |$)/g; + let match; + + while ((match = allegationPattern.exec(content)) !== null) { + allegations.push({ + number: match[1], + details: match[2].trim().substring(0, 200) + "...", + }); + } + return allegations; +} + +// Extract person information +function extractPersons(content) { + const persons = []; + + // Try to find JSON-formatted person data + const jsonPersonPattern = + /\{"peranan":\s*"([^"]+)",\s*"category":\s*"([^"]+)",\s*"namaPihak":\s*"([^"]+)"\}/g; + let jsonMatch; + + while ((jsonMatch = jsonPersonPattern.exec(content)) !== null) { + try { + persons.push({ + name: jsonMatch[3], + role: jsonMatch[1], + category: jsonMatch[2], + }); + } catch (e) { + console.error("Error processing person data:", e); + } + } + + // If no JSON data found, try the old format as fallback + if (persons.length === 0) { + const personPattern = + /Person ID: (\d+)[\s\S]*?namaPerayuResponden['":]?\s*['":]([^'"]+)['"]/g; + let match; + + while ((match = personPattern.exec(content)) !== null) { + persons.push({ + id: match[1], + name: match[2].trim(), + role: "Unknown", + category: "Unknown", + }); + } + } + + return persons; +} + +// Extract legal acts and sections +function extractActs(content) { + const acts = []; + const actPattern = /(Akta [^\\n]+)|(Section [^\\n]+)|(Seksyen [^\\n]+)/gi; + let match; + + while ((match = actPattern.exec(content)) !== null) { + const act = match[0].trim(); + if (act && !acts.includes(act)) { + acts.push(act); + } + } + return acts.slice(0, 3); // Limit to 3 acts +} + +// Generate document summary based on extracted data +function generateDocumentSummary(data) { + let summary = '
'; + + // Case information + if (data.caseNumber || data.fileNumber) { + summary += '
'; + summary += '

Case Information:

'; + if (data.caseNumber) + summary += `

Case: ${data.caseNumber}

`; + if (data.fileNumber) + summary += `

File: ${data.fileNumber}

`; + if (data.status) + summary += `

Status: ${ + data.status + }

`; + summary += "
"; + } + + // Legal acts + if (data.acts && data.acts.length > 0) { + summary += '
'; + summary += '

Legal References:

'; + summary += '
    '; + data.acts.forEach((act) => { + summary += `
  • ${act}
  • `; + }); + summary += "
"; + } + + // Persons involved + if (data.persons && data.persons.length > 0) { + summary += '
'; + summary += '

Persons Involved:

'; + summary += '
    '; + data.persons.forEach((person) => { + summary += `
  • +
    ${person.name}
    + ${ + person.role + ? `
    ${person.role}
    ` + : "" + } + ${ + person.category + ? `
    ${person.category}
    ` + : "" + } +
  • `; + }); + summary += "
"; + } + + // Allegations count + if (data.allegations && data.allegations.length > 0) { + summary += '
'; + summary += `

Allegations: (${data.allegations.length})

`; + data.allegations.forEach((allegation) => { + summary += `
+ Allegation ${allegation.number}: +

${allegation.details}

+
`; + }); + summary += "
"; + } + + summary += "
"; + return summary; +} + +// Show notification +function showNotification(type, message) { + // Create notification element if it doesn't exist + let notification = document.querySelector(".notification"); + if (notification) { + // Remove existing notification first + notification.remove(); + } + + // Create new notification + notification = document.createElement("div"); + notification.className = `notification ${type}`; + + let icon; + switch (type) { + case "success": + icon = "check-circle"; + break; + case "error": + icon = "exclamation-circle"; + break; + case "warning": + icon = "exclamation-triangle"; + break; + default: + icon = "info-circle"; + } + + notification.innerHTML = ` +
+ + ${message} +
+ `; + + document.body.appendChild(notification); + + // Auto-remove after 5 seconds + setTimeout(() => { + notification.classList.add("notification-hide"); + setTimeout(() => { + if (notification && notification.parentNode) { + notification.remove(); + } + }, 300); + }, 5000); +} + +// Show loading indicator +function showLoader() { + showLoadingOverlay(); +} + +// Hide loading indicator +function hideLoader() { + hideLoadingOverlay(); +} + +// View Toggle Functions +function setupViewToggle() { + const gridViewBtn = document.getElementById("gridViewBtn"); + const listViewBtn = document.getElementById("listViewBtn"); + + if (gridViewBtn) { + gridViewBtn.addEventListener("click", () => switchToGridView()); + } + if (listViewBtn) { + listViewBtn.addEventListener("click", () => switchToListView()); + } + + // Set initial state based on saved preference + if (appState.viewMode === "list") { + listViewBtn?.classList.add("active"); + gridViewBtn?.classList.remove("active"); + } else { + gridViewBtn?.classList.add("active"); + listViewBtn?.classList.remove("active"); + } +} + +function switchToGridView() { + // Update button states + document.getElementById("gridViewBtn")?.classList.add("active"); + document.getElementById("listViewBtn")?.classList.remove("active"); + + // Update view mode in state + appState.viewMode = "grid"; + saveToStorage("viewMode", "grid"); + + // Update document container + updateDocumentView(); +} + +function switchToListView() { + // Update button states + document.getElementById("listViewBtn")?.classList.add("active"); + document.getElementById("gridViewBtn")?.classList.remove("active"); + + // Update view mode in state + appState.viewMode = "list"; + saveToStorage("viewMode", "list"); + + // Update document container + updateDocumentView(); +} + +function updateDocumentView() { + const container = document.getElementById("recentDocumentsContainer"); + if (!container) return; + + // Re-render documents with current view mode + displayRecentDocuments(appState.documents.slice(0, 6)); +} diff --git a/frontend/js/main.js.backup b/frontend/js/main.js.backup new file mode 100644 index 0000000..5ebb073 --- /dev/null +++ b/frontend/js/main.js.backup @@ -0,0 +1,1315 @@ +/** + * AGC Document Search - Main Application + * Handles application state management and UI interactions + */ + +// Application State +const appState = { + isInitializing: true, + isOnline: navigator.onLine, + apiStatus: "connecting", + + // Search State + searchQuery: "", + searchResults: null, + searchHistory: loadFromStorage("searchHistory") || [], + + // Documents State + documents: [], + documentTypes: ["All Types"], + selectedDocument: null, + bookmarks: loadFromStorage("bookmarks") || [], + + // UI State + activeTab: "all-docs", + viewMode: loadFromStorage("viewMode") || "grid", + + // Loading States + loading: { + search: false, + documents: false, + }, + + // AI Chat + aiChat: { + messages: loadFromStorage("chatMessages") || [], + isTyping: false, + }, +}; + +// DOM Elements +const elements = { + // Safely get elements with a fallback + getElement: function (id) { + const element = document.getElementById(id); + if (!element) { + console.warn(`Element with ID '${id}' not found in the DOM`); + } + return element; + }, + + get searchInput() { + return this.getElement("mainSearchInput"); + }, + get searchButton() { + return this.getElement("mainSearchButton"); + }, + get searchResultsSection() { + return this.getElement("resultsSection"); + }, + get searchResultsContainer() { + return this.getElement("searchResultsContainer"); + }, + get recentDocumentsContainer() { + return this.getElement("recentDocumentsContainer"); + }, + get chatMessages() { + return this.getElement("chatMessages"); + }, + get chatInput() { + return this.getElement("chatInput"); + }, + get sendMessageButton() { + return this.getElement("chatSendBtn"); + }, + get mainContent() { + return this.getElement("mainContent"); + }, + + // For elements that use querySelectorAll + get tabButtons() { + return document.querySelectorAll(".tab") || []; + }, + + // Other elements + get mobileMenuButton() { + return this.getElement("mobileMenuToggle"); + }, + get mobileMenu() { + return this.getElement("mobileMenu"); + }, + + // Will be created dynamically + documentViewerModal: null, +}; + +// Utility Functions +function formatDate(dateString) { + if (!dateString) return "N/A"; + return new Date(dateString).toLocaleDateString("en-MY", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +function truncateText(text, maxLength = 100) { + if (!text || text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; +} + +function debounce(func, wait = 300) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +function loadFromStorage(key) { + try { + const data = localStorage.getItem(`agc_${key}`); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error(`Error loading ${key} from storage:`, error); + return null; + } +} + +function saveToStorage(key, data) { + try { + localStorage.setItem(`agc_${key}`, JSON.stringify(data)); + return true; + } catch (error) { + console.error(`Error saving ${key} to storage:`, error); + return false; + } +} + +// Search Functions +async function performSearch(query, docType = "All Types") { + if (!query.trim()) return; + + // Update UI to loading state + appState.loading.search = true; + updateLoadingState(); + + try { + // Perform search via API + const results = await apiService.searchDocuments(query); + + // Update application state + appState.searchResults = results; + + // Add to search history + if (!appState.searchHistory.some((item) => item.query === query)) { + appState.searchHistory.unshift({ + query, + timestamp: new Date().toISOString(), + count: results.documents.length, + }); + + // Keep last 20 searches + if (appState.searchHistory.length > 20) { + appState.searchHistory = appState.searchHistory.slice(0, 20); + } + + saveToStorage("searchHistory", appState.searchHistory); + } + + // Update UI with results + displaySearchResults(results); + } catch (error) { + console.error("Search failed:", error); + elements.searchResultsContainer.innerHTML = ` +
+

Sorry, the search failed. Please try again.

+

${error.message}

+
+ `; + } finally { + appState.loading.search = false; + updateLoadingState(); + + // Show results section if hidden + elements.searchResultsSection.classList.remove("hidden"); + } +} + +function displaySearchResults(results) { + const { documents, query, enhanced_query, answer } = results; + + if (!documents || documents.length === 0) { + elements.searchResultsContainer.innerHTML = ` +
+

No results found for "${query}"

+

Try using different keywords or browse all documents.

+
+ `; + return; + } + + let resultsHTML = ""; + + // Add AI answer if available + if (answer) { + resultsHTML += ` +
+

AI Analysis

+

${answer}

+
+ `; + } + + // Enhanced query display + if (enhanced_query && enhanced_query !== query) { + resultsHTML += ` +
+

Showing results for: ${enhanced_query}

+

Original search: ${query}

+
+ `; + } + + // Results count + resultsHTML += ` +
+

Found ${documents.length} result${ + documents.length !== 1 ? "s" : "" + }

+
+ `; + + // Document results + resultsHTML += '
'; + + documents.forEach((doc) => { + const docTitle = doc.title || "Untitled Document"; + const docContent = doc.content || "No content available"; + const docType = doc.doc_type || "Unknown"; + const docDate = formatDate(doc.created_at); + const isBookmarked = appState.bookmarks.includes(doc.id); + + resultsHTML += ` +
+
+
${docType}
+

${docTitle}

+
+
+

${truncateText(docContent, 200)}

+
+ +
+ `; + }); + + resultsHTML += "
"; + + elements.searchResultsContainer.innerHTML = resultsHTML; +} + +// Document Functions +async function loadDocuments(docType = "All Types") { + appState.loading.documents = true; + updateLoadingState(); + + try { + // Load documents based on document type + const filters = { + doc_type: docType !== "All Types" ? docType : undefined, + }; + + const documents = await apiService.getDocuments(filters); + appState.documents = documents; + + // Display documents in recent documents section + displayRecentDocuments(documents.slice(0, 6)); // Show first 6 documents + } catch (error) { + console.error("Failed to load documents:", error); + + // Check if the container exists before attempting to update it + const container = document.getElementById("recentDocumentsContainer"); + if (container) { + container.innerHTML = ` +
+

Sorry, we couldn't load the documents. Please try again.

+

${error.message}

+
+ `; + } + + // Show error notification + showNotification( + "error", + "Failed to load documents. Please check the console for details." + ); + } finally { + appState.loading.documents = false; + updateLoadingState(); + } +} + +function displayRecentDocuments(documents) { + // Check if the container element exists + const recentDocumentsContainer = document.getElementById( + "recentDocumentsContainer" + ); + if (!recentDocumentsContainer) { + console.warn("Recent documents container not found in the DOM"); + return; + } + + if (!documents || documents.length === 0) { + recentDocumentsContainer.innerHTML = ` +
+

No recent documents found.

+
+ `; + return; + } + + let documentsHTML = '
'; + + documents.forEach((doc) => { + const docTitle = doc.title || "Untitled Document"; + const docContent = doc.content || "No content available"; + const docType = doc.doc_type || "Unknown"; + const docDate = formatDate(doc.created_at); + const isBookmarked = appState.bookmarks.includes(doc.id); + + documentsHTML += ` +
+
+
${docType}
+

${docTitle}

+
+
+

${truncateText(docContent, 100)}

+
+ +
+ `; + }); + + documentsHTML += "
"; + + recentDocumentsContainer.innerHTML = documentsHTML; +} + +async function loadDocumentTypes() { + try { + const response = await apiService.getDocumentTypes(); + if (response && response.document_types) { + // Add "All Types" as the first option + appState.documentTypes = ["All Types", ...response.document_types]; + + // Update UI elements that use document types (e.g., filter dropdowns) + updateDocumentTypeSelectors(); + } + } catch (error) { + console.error("Failed to load document types:", error); + } +} + +function updateDocumentTypeSelectors() { + // For future implementation - update any dropdowns or filters with document types +} + +// View document details +window.viewDocument = async function (documentId) { + try { + console.log(`Viewing document with ID: ${documentId}`); + showLoader(); + + // Convert any numeric IDs to our document format + if (typeof documentId === "number" || !isNaN(parseInt(documentId, 10))) { + console.log(`Converting numeric ID ${documentId} to string format`); + // If it's a number between 1-6, convert to our doc format + const numId = parseInt(documentId, 10); + if (numId >= 1 && numId <= 6) { + documentId = `doc${numId}`; + console.log(`Converted to: ${documentId}`); + } + } + + // Fetch document from API + const document = await apiService.getDocument(documentId); + + // Extract legal information from content + const extractedData = extractLegalInfo(document.content); + + // Create modal if it doesn't exist + let modal = window.document.querySelector(".document-modal"); + if (!modal) { + modal = window.document.createElement("div"); + modal.className = "document-modal"; + modal.innerHTML = ` +
+
+
+

+ +
+
+ +
+ `; + window.document.body.appendChild(modal); + + // Add event listeners + const closeButtons = modal.querySelectorAll(".document-modal-close"); + closeButtons.forEach((button) => { + button.addEventListener("click", () => { + modal.classList.add("hidden"); + }); + }); + + modal + .querySelector(".document-modal-overlay") + .addEventListener("click", () => { + modal.classList.add("hidden"); + }); + } + + // Update modal title + modal.querySelector(".document-modal-title").textContent = + document.title || `Document ${documentId}`; + + // Build the modal content with structured information + let modalContent = ` +
+
+ Type: + ${ + document.doc_type || "Unknown" + } +
+
+ Date: + ${formatDate( + document.created_at + )} +
+ ${ + document.source + ? ` +
+ Source: + ${document.source} +
+ ` + : "" + } +
+ `; + + // Add extracted information if available + modalContent += generateDocumentSummary(extractedData); + + // Add full content section + modalContent += ` +
+

Full Document Content

+
+
${
+            document.content || "No content available"
+          }
+
+
+ `; + + // Update modal body + modal.querySelector(".document-modal-body").innerHTML = modalContent; + + // Add styles for new elements if not already present + if (!window.document.querySelector("style#document-details-styles")) { + const detailsStyles = window.document.createElement("style"); + detailsStyles.id = "document-details-styles"; + detailsStyles.textContent = ` + .section-title { + margin: 0 0 0.75rem 0; + font-size: 1.1rem; + color: var(--primary-dark); + display: flex; + align-items: center; + gap: 0.5rem; + } + + .full-content-section { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--gray-light); + } + + .full-content-container { + background-color: #f8f9fa; + border-radius: var(--radius); + border: 1px solid var(--gray-light); + padding: 1rem; + max-height: 400px; + overflow-y: auto; + } + + .full-content-text { + font-family: monospace; + font-size: 0.9rem; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + margin: 0; + } + + .document-summary { + margin: 1rem 0; + } + + .info-section { + margin-bottom: 1rem; + padding: 1rem; + background-color: #f8f9fa; + border-radius: var(--radius); + border-left: 3px solid var(--primary); + } + + .info-section h4 { + margin: 0 0 0.75rem 0; + color: var(--primary-dark); + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .info-section p { + margin: 0.5rem 0; + font-size: 0.9rem; + } + + .acts-list, + .persons-list { + margin: 0.5rem 0; + padding-left: 0; + list-style: none; + } + + .act-item, + .person-item { + background-color: rgba(59, 130, 246, 0.1); + margin: 0.25rem 0; + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + font-size: 0.9rem; + border-left: 3px solid var(--primary); + } + + .person-item { + border-left-color: var(--warning); + background-color: rgba(245, 158, 11, 0.1); + } + + .person-name { + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 0.35rem; + } + + .person-role, + .person-category { + font-size: 0.85rem; + margin-top: 0.25rem; + } + + .role-label, + .category-label { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 500; + } + + .role-label { + background-color: var(--primary); + color: white; + } + + .category-label { + background-color: var(--gray-light); + color: var(--gray-dark); + } + + .allegation-item { + margin: 0.75rem 0; + padding: 0.75rem; + background-color: rgba(239, 68, 68, 0.05); + border-radius: 0.25rem; + border-left: 3px solid var(--danger); + } + + .allegation-preview { + margin: 0.5rem 0 0 0; + font-size: 0.9rem; + color: var(--gray); + font-style: italic; + } + + .status-ongoing { + color: var(--primary); + font-weight: 600; + } + + .status-selesai, + .status-completed { + color: var(--success); + font-weight: 600; + } + + .status-pending { + color: var(--warning); + font-weight: 600; + } + `; + window.document.head.appendChild(detailsStyles); + } + + // Show modal + modal.classList.remove("hidden"); + } catch (error) { + console.error("Error loading document:", error); + showNotification( + "error", + `Failed to load document details: ${error.message}` + ); + } finally { + hideLoader(); + } +}; + +function hideDocumentViewer() { + if (elements.documentViewerModal) { + elements.documentViewerModal.classList.add("hidden"); + } +} + +function toggleBookmark(documentId) { + if (!documentId) return; + + const bookmarks = [...appState.bookmarks]; + const index = bookmarks.indexOf(documentId); + + if (index === -1) { + // Add bookmark + bookmarks.push(documentId); + } else { + // Remove bookmark + bookmarks.splice(index, 1); + } + + // Update state and storage + appState.bookmarks = bookmarks; + saveToStorage("bookmarks", bookmarks); + + // Update UI + document + .querySelectorAll(`.bookmark-button[onclick*="${documentId}"]`) + .forEach((button) => { + if (index === -1) { + button.classList.add("bookmarked"); + } else { + button.classList.remove("bookmarked"); + } + }); +} + +// Tab Navigation +function switchTab(tabId) { + // Update active tab state + appState.activeTab = tabId; + + // Get all tab links and remove active class + const mainTabLinks = document.querySelectorAll(".main-tab-link"); + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active class to the corresponding tab + const activeTab = document.querySelector(`.main-tab-link[href="#${tabId}"]`); + if (activeTab) { + activeTab.classList.add("active"); + } + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + + // Load tab-specific content + if (tabId === "browse") { + loadDocuments("All Types"); + } else if (tabId === "search") { + // Any search-specific initialization + } else if (tabId === "chat") { + // Any chat-specific initialization + } +} + +// AI Chat Functions +function sendChatMessage() { + const message = elements.chatInput.value.trim(); + if (!message) return; + + // Add user message to UI + addChatMessage(message, "user"); + + // Clear input + elements.chatInput.value = ""; + + // Set typing indicator + appState.aiChat.isTyping = true; + updateChatTypingIndicator(); + + // Show API connection error message + setTimeout(() => { + const aiResponse = + "The AI assistant feature is currently unavailable. Please ensure the AI service is properly configured and connected."; + + // Add error response to UI + addChatMessage(aiResponse, "system"); + + // Clear typing indicator + appState.aiChat.isTyping = false; + updateChatTypingIndicator(); + }, 1000); +} + +function addChatMessage(message, role) { + // Add to state + appState.aiChat.messages.push({ + role, + content: message, + timestamp: new Date().toISOString(), + }); + + // Save to storage (limited to last 50 messages) + if (appState.aiChat.messages.length > 50) { + appState.aiChat.messages = appState.aiChat.messages.slice(-50); + } + saveToStorage("chatMessages", appState.aiChat.messages); + + // Add to UI + const chatMessageElement = document.createElement("div"); + + if (role === "user") { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } else if (role === "system") { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } else { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } + + elements.chatMessages.appendChild(chatMessageElement); + + // Scroll to bottom + elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; +} + +function updateChatTypingIndicator() { + // Remove existing typing indicator if any + const existingIndicator = + elements.chatMessages.querySelector(".typing-indicator"); + if (existingIndicator) { + existingIndicator.remove(); + } + + if (appState.aiChat.isTyping) { + // Add typing indicator + const indicatorElement = document.createElement("div"); + indicatorElement.className = "typing-indicator"; + indicatorElement.innerHTML = ` +
+
+
+
+
+ `; + elements.chatMessages.appendChild(indicatorElement); + elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; + } +} + +// UI Helper Functions +function updateLoadingState() { + // Update loading indicators based on appState.loading + const searchButton = document.getElementById("mainSearchButton"); + if (searchButton) { + if (appState.loading.search) { + searchButton.innerHTML = ''; + searchButton.disabled = true; + } else { + searchButton.innerHTML = ''; + searchButton.disabled = false; + } + } + + // Other loading indicators can be updated here +} + +function showLoadingOverlay() { + let overlay = document.querySelector(".loading-overlay"); + + if (!overlay) { + overlay = document.createElement("div"); + overlay.className = "loading-overlay"; + overlay.innerHTML = ` +
+ +
+ `; + document.body.appendChild(overlay); + } + + overlay.classList.remove("hidden"); +} + +function hideLoadingOverlay() { + const overlay = document.querySelector(".loading-overlay"); + if (overlay) { + overlay.classList.add("hidden"); + } +} + +// Initialize Application +async function initializeApp() { + try { + showLoadingOverlay(); + + // Check API connection (blocking) + const isConnected = await apiService.checkConnection(); + appState.apiStatus = isConnected ? "connected" : "disconnected"; + console.log(`API Status: ${appState.apiStatus}`); + + if (!isConnected) { + hideLoadingOverlay(); + showNotification( + "error", + "Cannot connect to API server. Please ensure the backend service is running." + ); + + // Check if mainContent element exists + const mainContent = document.getElementById("mainContent"); + if (mainContent) { + mainContent.innerHTML = ` +
+
+

API Connection Error

+

The application could not connect to the backend API server.

+
+

Please make sure:

+
    +
  • The backend server is running (uvicorn api:app --reload)
  • +
  • The API is accessible at ${apiService.baseUrl}
  • +
+ +
+
+
+ `; + } + return; + } + + try { + // Load document types + await loadDocumentTypes(); + } catch (error) { + console.error("Failed to load document types:", error); + showNotification( + "warning", + "Could not load document types. Some filtering options may be unavailable." + ); + } + + try { + // Load initial documents + await loadDocuments(); + } catch (error) { + console.error("Failed to load documents:", error); + showNotification( + "error", + "Failed to load documents. Please check the console for details." + ); + } + + // Setup event listeners + setupEventListeners(); + + // Load chat history + loadChatHistory(); + } catch (error) { + console.error("Failed to initialize application:", error); + hideLoadingOverlay(); + showNotification( + "error", + "Application initialization failed. Please check the console for details." + ); + + // Show error on main content + const mainContent = document.getElementById("mainContent"); + if (mainContent) { + mainContent.innerHTML = ` +
+
+

Application Error

+

Something went wrong while initializing the application.

+
+

Error details:

+
${error.message || "Unknown error"}
+ +
+
+
+ `; + } + } finally { + // Mark initialization as complete + appState.isInitializing = false; + hideLoadingOverlay(); + } +} + +function setupEventListeners() { + // Search + if (elements.searchButton) { + elements.searchButton.addEventListener("click", () => { + if (elements.searchInput) { + performSearch(elements.searchInput.value); + } + }); + } + + if (elements.searchInput) { + elements.searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + performSearch(elements.searchInput.value); + } + }); + } + + // Tab navigation - fix for the main tabs + const mainTabLinks = document.querySelectorAll(".main-tab-link"); + mainTabLinks.forEach((tab) => { + tab.addEventListener("click", (e) => { + e.preventDefault(); // Prevent default anchor behavior + + // Remove active class from all tabs + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active class to clicked tab + tab.classList.add("active"); + + // Get the tab ID from the href attribute + const tabId = tab.getAttribute("href").substring(1); // Remove # from href + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + }); + }); + + // Mobile menu tab navigation + const mobileMenuLinks = document.querySelectorAll(".mobile-menu a"); + mobileMenuLinks.forEach((link) => { + link.addEventListener("click", (e) => { + e.preventDefault(); + const tabId = link.getAttribute("href").substring(1); + + // Hide mobile menu after selection + const mobileMenu = document.getElementById("mobileMenu"); + if (mobileMenu) mobileMenu.classList.remove("active"); + + // Remove active class from all main tabs + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active to the corresponding main tab + const correspondingTab = document.querySelector( + `.main-tab-link[href="#${tabId}"]` + ); + if (correspondingTab) correspondingTab.classList.add("active"); + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + }); + }); + + // Mobile menu toggle + if (elements.mobileMenuButton && elements.mobileMenu) { + elements.mobileMenuButton.addEventListener("click", () => { + elements.mobileMenu.classList.toggle("active"); + }); + } + + // Chat + if (elements.sendMessageButton) { + elements.sendMessageButton.addEventListener("click", sendChatMessage); + } + + if (elements.chatInput) { + elements.chatInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + sendChatMessage(); + } + }); + } +} + +function loadChatHistory() { + // Display existing chat messages + appState.aiChat.messages.forEach((message) => { + addChatMessage(message.content, message.role); + }); +} + +// Export functions for global access (needed for onclick handlers) +window.toggleBookmark = toggleBookmark; + +// Initialize the application when DOM is loaded +document.addEventListener("DOMContentLoaded", initializeApp); + +// Utility functions for document extraction +function extractLegalInfo(content) { + if (!content) return {}; + + const info = { + fileNumber: extractField(content, "File Number"), + status: extractField(content, "Status"), + caseNumber: extractField(content, "Case Number"), + dppSuggestion: extractField(content, "DPP Suggestion"), + hodDecision: extractField(content, "HOD Decision"), + allegations: extractAllegations(content), + persons: extractPersons(content), + acts: extractActs(content), + }; + return info; +} + +// Helper function to extract field values +function extractField(content, fieldName) { + const regex = new RegExp(`${fieldName}:\\s*([^\\n]+)`, "i"); + const match = content.match(regex); + return match ? match[1].trim() : null; +} + +// Extract allegations information +function extractAllegations(content) { + const allegations = []; + const allegationPattern = + /ALLEGATION #(\d+):([\s\S]*?)(?=ALLEGATION #|\n--- |$)/g; + let match; + + while ((match = allegationPattern.exec(content)) !== null) { + allegations.push({ + number: match[1], + details: match[2].trim().substring(0, 200) + "...", + }); + } + return allegations; +} + +// Extract person information +function extractPersons(content) { + const persons = []; + + // Try to find JSON-formatted person data + const jsonPersonPattern = + /\{"peranan":\s*"([^"]+)",\s*"category":\s*"([^"]+)",\s*"namaPihak":\s*"([^"]+)"\}/g; + let jsonMatch; + + while ((jsonMatch = jsonPersonPattern.exec(content)) !== null) { + try { + persons.push({ + name: jsonMatch[3], + role: jsonMatch[1], + category: jsonMatch[2], + }); + } catch (e) { + console.error("Error processing person data:", e); + } + } + + // If no JSON data found, try the old format as fallback + if (persons.length === 0) { + const personPattern = + /Person ID: (\d+)[\s\S]*?namaPerayuResponden['":]?\s*['":]([^'"]+)['"]/g; + let match; + + while ((match = personPattern.exec(content)) !== null) { + persons.push({ + id: match[1], + name: match[2].trim(), + role: "Unknown", + category: "Unknown", + }); + } + } + + return persons; +} + +// Extract legal acts and sections +function extractActs(content) { + const acts = []; + const actPattern = /(Akta [^\\n]+)|(Section [^\\n]+)|(Seksyen [^\\n]+)/gi; + let match; + + while ((match = actPattern.exec(content)) !== null) { + const act = match[0].trim(); + if (act && !acts.includes(act)) { + acts.push(act); + } + } + return acts.slice(0, 3); // Limit to 3 acts +} + +// Generate document summary based on extracted data +function generateDocumentSummary(data) { + let summary = '
'; + + // Case information + if (data.caseNumber || data.fileNumber) { + summary += '
'; + summary += '

Case Information:

'; + if (data.caseNumber) + summary += `

Case: ${data.caseNumber}

`; + if (data.fileNumber) + summary += `

File: ${data.fileNumber}

`; + if (data.status) + summary += `

Status: ${ + data.status + }

`; + summary += "
"; + } + + // Legal acts + if (data.acts && data.acts.length > 0) { + summary += '
'; + summary += '

Legal References:

'; + summary += '
    '; + data.acts.forEach((act) => { + summary += `
  • ${act}
  • `; + }); + summary += "
"; + } + + // Persons involved + if (data.persons && data.persons.length > 0) { + summary += '
'; + summary += '

Persons Involved:

'; + summary += '
    '; + data.persons.forEach((person) => { + summary += `
  • +
    ${person.name}
    + ${ + person.role + ? `
    ${person.role}
    ` + : "" + } + ${ + person.category + ? `
    ${person.category}
    ` + : "" + } +
  • `; + }); + summary += "
"; + } + + // Allegations count + if (data.allegations && data.allegations.length > 0) { + summary += '
'; + summary += `

Allegations: (${data.allegations.length})

`; + data.allegations.forEach((allegation) => { + summary += `
+ Allegation ${allegation.number}: +

${allegation.details}

+
`; + }); + summary += "
"; + } + + summary += "
"; + return summary; +} + +// Show notification +function showNotification(type, message) { + // Create notification element if it doesn't exist + let notification = document.querySelector(".notification"); + if (notification) { + // Remove existing notification first + notification.remove(); + } + + // Create new notification + notification = document.createElement("div"); + notification.className = `notification ${type}`; + + let icon; + switch (type) { + case "success": + icon = "check-circle"; + break; + case "error": + icon = "exclamation-circle"; + break; + case "warning": + icon = "exclamation-triangle"; + break; + default: + icon = "info-circle"; + } + + notification.innerHTML = ` +
+ + ${message} +
+ `; + + document.body.appendChild(notification); + + // Auto-remove after 5 seconds + setTimeout(() => { + notification.classList.add("notification-hide"); + setTimeout(() => { + if (notification && notification.parentNode) { + notification.remove(); + } + }, 300); + }, 5000); +} + +// Show loading indicator +function showLoader() { + showLoadingOverlay(); +} + +// Hide loading indicator +function hideLoader() { + hideLoadingOverlay(); +} diff --git a/frontend/js/main.js.bak b/frontend/js/main.js.bak new file mode 100644 index 0000000..5ebb073 --- /dev/null +++ b/frontend/js/main.js.bak @@ -0,0 +1,1315 @@ +/** + * AGC Document Search - Main Application + * Handles application state management and UI interactions + */ + +// Application State +const appState = { + isInitializing: true, + isOnline: navigator.onLine, + apiStatus: "connecting", + + // Search State + searchQuery: "", + searchResults: null, + searchHistory: loadFromStorage("searchHistory") || [], + + // Documents State + documents: [], + documentTypes: ["All Types"], + selectedDocument: null, + bookmarks: loadFromStorage("bookmarks") || [], + + // UI State + activeTab: "all-docs", + viewMode: loadFromStorage("viewMode") || "grid", + + // Loading States + loading: { + search: false, + documents: false, + }, + + // AI Chat + aiChat: { + messages: loadFromStorage("chatMessages") || [], + isTyping: false, + }, +}; + +// DOM Elements +const elements = { + // Safely get elements with a fallback + getElement: function (id) { + const element = document.getElementById(id); + if (!element) { + console.warn(`Element with ID '${id}' not found in the DOM`); + } + return element; + }, + + get searchInput() { + return this.getElement("mainSearchInput"); + }, + get searchButton() { + return this.getElement("mainSearchButton"); + }, + get searchResultsSection() { + return this.getElement("resultsSection"); + }, + get searchResultsContainer() { + return this.getElement("searchResultsContainer"); + }, + get recentDocumentsContainer() { + return this.getElement("recentDocumentsContainer"); + }, + get chatMessages() { + return this.getElement("chatMessages"); + }, + get chatInput() { + return this.getElement("chatInput"); + }, + get sendMessageButton() { + return this.getElement("chatSendBtn"); + }, + get mainContent() { + return this.getElement("mainContent"); + }, + + // For elements that use querySelectorAll + get tabButtons() { + return document.querySelectorAll(".tab") || []; + }, + + // Other elements + get mobileMenuButton() { + return this.getElement("mobileMenuToggle"); + }, + get mobileMenu() { + return this.getElement("mobileMenu"); + }, + + // Will be created dynamically + documentViewerModal: null, +}; + +// Utility Functions +function formatDate(dateString) { + if (!dateString) return "N/A"; + return new Date(dateString).toLocaleDateString("en-MY", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +function truncateText(text, maxLength = 100) { + if (!text || text.length <= maxLength) return text; + return text.substring(0, maxLength) + "..."; +} + +function debounce(func, wait = 300) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +function loadFromStorage(key) { + try { + const data = localStorage.getItem(`agc_${key}`); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error(`Error loading ${key} from storage:`, error); + return null; + } +} + +function saveToStorage(key, data) { + try { + localStorage.setItem(`agc_${key}`, JSON.stringify(data)); + return true; + } catch (error) { + console.error(`Error saving ${key} to storage:`, error); + return false; + } +} + +// Search Functions +async function performSearch(query, docType = "All Types") { + if (!query.trim()) return; + + // Update UI to loading state + appState.loading.search = true; + updateLoadingState(); + + try { + // Perform search via API + const results = await apiService.searchDocuments(query); + + // Update application state + appState.searchResults = results; + + // Add to search history + if (!appState.searchHistory.some((item) => item.query === query)) { + appState.searchHistory.unshift({ + query, + timestamp: new Date().toISOString(), + count: results.documents.length, + }); + + // Keep last 20 searches + if (appState.searchHistory.length > 20) { + appState.searchHistory = appState.searchHistory.slice(0, 20); + } + + saveToStorage("searchHistory", appState.searchHistory); + } + + // Update UI with results + displaySearchResults(results); + } catch (error) { + console.error("Search failed:", error); + elements.searchResultsContainer.innerHTML = ` +
+

Sorry, the search failed. Please try again.

+

${error.message}

+
+ `; + } finally { + appState.loading.search = false; + updateLoadingState(); + + // Show results section if hidden + elements.searchResultsSection.classList.remove("hidden"); + } +} + +function displaySearchResults(results) { + const { documents, query, enhanced_query, answer } = results; + + if (!documents || documents.length === 0) { + elements.searchResultsContainer.innerHTML = ` +
+

No results found for "${query}"

+

Try using different keywords or browse all documents.

+
+ `; + return; + } + + let resultsHTML = ""; + + // Add AI answer if available + if (answer) { + resultsHTML += ` +
+

AI Analysis

+

${answer}

+
+ `; + } + + // Enhanced query display + if (enhanced_query && enhanced_query !== query) { + resultsHTML += ` +
+

Showing results for: ${enhanced_query}

+

Original search: ${query}

+
+ `; + } + + // Results count + resultsHTML += ` +
+

Found ${documents.length} result${ + documents.length !== 1 ? "s" : "" + }

+
+ `; + + // Document results + resultsHTML += '
'; + + documents.forEach((doc) => { + const docTitle = doc.title || "Untitled Document"; + const docContent = doc.content || "No content available"; + const docType = doc.doc_type || "Unknown"; + const docDate = formatDate(doc.created_at); + const isBookmarked = appState.bookmarks.includes(doc.id); + + resultsHTML += ` +
+
+
${docType}
+

${docTitle}

+
+
+

${truncateText(docContent, 200)}

+
+ +
+ `; + }); + + resultsHTML += "
"; + + elements.searchResultsContainer.innerHTML = resultsHTML; +} + +// Document Functions +async function loadDocuments(docType = "All Types") { + appState.loading.documents = true; + updateLoadingState(); + + try { + // Load documents based on document type + const filters = { + doc_type: docType !== "All Types" ? docType : undefined, + }; + + const documents = await apiService.getDocuments(filters); + appState.documents = documents; + + // Display documents in recent documents section + displayRecentDocuments(documents.slice(0, 6)); // Show first 6 documents + } catch (error) { + console.error("Failed to load documents:", error); + + // Check if the container exists before attempting to update it + const container = document.getElementById("recentDocumentsContainer"); + if (container) { + container.innerHTML = ` +
+

Sorry, we couldn't load the documents. Please try again.

+

${error.message}

+
+ `; + } + + // Show error notification + showNotification( + "error", + "Failed to load documents. Please check the console for details." + ); + } finally { + appState.loading.documents = false; + updateLoadingState(); + } +} + +function displayRecentDocuments(documents) { + // Check if the container element exists + const recentDocumentsContainer = document.getElementById( + "recentDocumentsContainer" + ); + if (!recentDocumentsContainer) { + console.warn("Recent documents container not found in the DOM"); + return; + } + + if (!documents || documents.length === 0) { + recentDocumentsContainer.innerHTML = ` +
+

No recent documents found.

+
+ `; + return; + } + + let documentsHTML = '
'; + + documents.forEach((doc) => { + const docTitle = doc.title || "Untitled Document"; + const docContent = doc.content || "No content available"; + const docType = doc.doc_type || "Unknown"; + const docDate = formatDate(doc.created_at); + const isBookmarked = appState.bookmarks.includes(doc.id); + + documentsHTML += ` +
+
+
${docType}
+

${docTitle}

+
+
+

${truncateText(docContent, 100)}

+
+ +
+ `; + }); + + documentsHTML += "
"; + + recentDocumentsContainer.innerHTML = documentsHTML; +} + +async function loadDocumentTypes() { + try { + const response = await apiService.getDocumentTypes(); + if (response && response.document_types) { + // Add "All Types" as the first option + appState.documentTypes = ["All Types", ...response.document_types]; + + // Update UI elements that use document types (e.g., filter dropdowns) + updateDocumentTypeSelectors(); + } + } catch (error) { + console.error("Failed to load document types:", error); + } +} + +function updateDocumentTypeSelectors() { + // For future implementation - update any dropdowns or filters with document types +} + +// View document details +window.viewDocument = async function (documentId) { + try { + console.log(`Viewing document with ID: ${documentId}`); + showLoader(); + + // Convert any numeric IDs to our document format + if (typeof documentId === "number" || !isNaN(parseInt(documentId, 10))) { + console.log(`Converting numeric ID ${documentId} to string format`); + // If it's a number between 1-6, convert to our doc format + const numId = parseInt(documentId, 10); + if (numId >= 1 && numId <= 6) { + documentId = `doc${numId}`; + console.log(`Converted to: ${documentId}`); + } + } + + // Fetch document from API + const document = await apiService.getDocument(documentId); + + // Extract legal information from content + const extractedData = extractLegalInfo(document.content); + + // Create modal if it doesn't exist + let modal = window.document.querySelector(".document-modal"); + if (!modal) { + modal = window.document.createElement("div"); + modal.className = "document-modal"; + modal.innerHTML = ` +
+
+
+

+ +
+
+ +
+ `; + window.document.body.appendChild(modal); + + // Add event listeners + const closeButtons = modal.querySelectorAll(".document-modal-close"); + closeButtons.forEach((button) => { + button.addEventListener("click", () => { + modal.classList.add("hidden"); + }); + }); + + modal + .querySelector(".document-modal-overlay") + .addEventListener("click", () => { + modal.classList.add("hidden"); + }); + } + + // Update modal title + modal.querySelector(".document-modal-title").textContent = + document.title || `Document ${documentId}`; + + // Build the modal content with structured information + let modalContent = ` +
+
+ Type: + ${ + document.doc_type || "Unknown" + } +
+
+ Date: + ${formatDate( + document.created_at + )} +
+ ${ + document.source + ? ` +
+ Source: + ${document.source} +
+ ` + : "" + } +
+ `; + + // Add extracted information if available + modalContent += generateDocumentSummary(extractedData); + + // Add full content section + modalContent += ` +
+

Full Document Content

+
+
${
+            document.content || "No content available"
+          }
+
+
+ `; + + // Update modal body + modal.querySelector(".document-modal-body").innerHTML = modalContent; + + // Add styles for new elements if not already present + if (!window.document.querySelector("style#document-details-styles")) { + const detailsStyles = window.document.createElement("style"); + detailsStyles.id = "document-details-styles"; + detailsStyles.textContent = ` + .section-title { + margin: 0 0 0.75rem 0; + font-size: 1.1rem; + color: var(--primary-dark); + display: flex; + align-items: center; + gap: 0.5rem; + } + + .full-content-section { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--gray-light); + } + + .full-content-container { + background-color: #f8f9fa; + border-radius: var(--radius); + border: 1px solid var(--gray-light); + padding: 1rem; + max-height: 400px; + overflow-y: auto; + } + + .full-content-text { + font-family: monospace; + font-size: 0.9rem; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + margin: 0; + } + + .document-summary { + margin: 1rem 0; + } + + .info-section { + margin-bottom: 1rem; + padding: 1rem; + background-color: #f8f9fa; + border-radius: var(--radius); + border-left: 3px solid var(--primary); + } + + .info-section h4 { + margin: 0 0 0.75rem 0; + color: var(--primary-dark); + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .info-section p { + margin: 0.5rem 0; + font-size: 0.9rem; + } + + .acts-list, + .persons-list { + margin: 0.5rem 0; + padding-left: 0; + list-style: none; + } + + .act-item, + .person-item { + background-color: rgba(59, 130, 246, 0.1); + margin: 0.25rem 0; + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + font-size: 0.9rem; + border-left: 3px solid var(--primary); + } + + .person-item { + border-left-color: var(--warning); + background-color: rgba(245, 158, 11, 0.1); + } + + .person-name { + font-weight: 600; + font-size: 0.95rem; + margin-bottom: 0.35rem; + } + + .person-role, + .person-category { + font-size: 0.85rem; + margin-top: 0.25rem; + } + + .role-label, + .category-label { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 500; + } + + .role-label { + background-color: var(--primary); + color: white; + } + + .category-label { + background-color: var(--gray-light); + color: var(--gray-dark); + } + + .allegation-item { + margin: 0.75rem 0; + padding: 0.75rem; + background-color: rgba(239, 68, 68, 0.05); + border-radius: 0.25rem; + border-left: 3px solid var(--danger); + } + + .allegation-preview { + margin: 0.5rem 0 0 0; + font-size: 0.9rem; + color: var(--gray); + font-style: italic; + } + + .status-ongoing { + color: var(--primary); + font-weight: 600; + } + + .status-selesai, + .status-completed { + color: var(--success); + font-weight: 600; + } + + .status-pending { + color: var(--warning); + font-weight: 600; + } + `; + window.document.head.appendChild(detailsStyles); + } + + // Show modal + modal.classList.remove("hidden"); + } catch (error) { + console.error("Error loading document:", error); + showNotification( + "error", + `Failed to load document details: ${error.message}` + ); + } finally { + hideLoader(); + } +}; + +function hideDocumentViewer() { + if (elements.documentViewerModal) { + elements.documentViewerModal.classList.add("hidden"); + } +} + +function toggleBookmark(documentId) { + if (!documentId) return; + + const bookmarks = [...appState.bookmarks]; + const index = bookmarks.indexOf(documentId); + + if (index === -1) { + // Add bookmark + bookmarks.push(documentId); + } else { + // Remove bookmark + bookmarks.splice(index, 1); + } + + // Update state and storage + appState.bookmarks = bookmarks; + saveToStorage("bookmarks", bookmarks); + + // Update UI + document + .querySelectorAll(`.bookmark-button[onclick*="${documentId}"]`) + .forEach((button) => { + if (index === -1) { + button.classList.add("bookmarked"); + } else { + button.classList.remove("bookmarked"); + } + }); +} + +// Tab Navigation +function switchTab(tabId) { + // Update active tab state + appState.activeTab = tabId; + + // Get all tab links and remove active class + const mainTabLinks = document.querySelectorAll(".main-tab-link"); + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active class to the corresponding tab + const activeTab = document.querySelector(`.main-tab-link[href="#${tabId}"]`); + if (activeTab) { + activeTab.classList.add("active"); + } + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + + // Load tab-specific content + if (tabId === "browse") { + loadDocuments("All Types"); + } else if (tabId === "search") { + // Any search-specific initialization + } else if (tabId === "chat") { + // Any chat-specific initialization + } +} + +// AI Chat Functions +function sendChatMessage() { + const message = elements.chatInput.value.trim(); + if (!message) return; + + // Add user message to UI + addChatMessage(message, "user"); + + // Clear input + elements.chatInput.value = ""; + + // Set typing indicator + appState.aiChat.isTyping = true; + updateChatTypingIndicator(); + + // Show API connection error message + setTimeout(() => { + const aiResponse = + "The AI assistant feature is currently unavailable. Please ensure the AI service is properly configured and connected."; + + // Add error response to UI + addChatMessage(aiResponse, "system"); + + // Clear typing indicator + appState.aiChat.isTyping = false; + updateChatTypingIndicator(); + }, 1000); +} + +function addChatMessage(message, role) { + // Add to state + appState.aiChat.messages.push({ + role, + content: message, + timestamp: new Date().toISOString(), + }); + + // Save to storage (limited to last 50 messages) + if (appState.aiChat.messages.length > 50) { + appState.aiChat.messages = appState.aiChat.messages.slice(-50); + } + saveToStorage("chatMessages", appState.aiChat.messages); + + // Add to UI + const chatMessageElement = document.createElement("div"); + + if (role === "user") { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } else if (role === "system") { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } else { + chatMessageElement.innerHTML = ` +
+

${message}

+
+ `; + } + + elements.chatMessages.appendChild(chatMessageElement); + + // Scroll to bottom + elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; +} + +function updateChatTypingIndicator() { + // Remove existing typing indicator if any + const existingIndicator = + elements.chatMessages.querySelector(".typing-indicator"); + if (existingIndicator) { + existingIndicator.remove(); + } + + if (appState.aiChat.isTyping) { + // Add typing indicator + const indicatorElement = document.createElement("div"); + indicatorElement.className = "typing-indicator"; + indicatorElement.innerHTML = ` +
+
+
+
+
+ `; + elements.chatMessages.appendChild(indicatorElement); + elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; + } +} + +// UI Helper Functions +function updateLoadingState() { + // Update loading indicators based on appState.loading + const searchButton = document.getElementById("mainSearchButton"); + if (searchButton) { + if (appState.loading.search) { + searchButton.innerHTML = ''; + searchButton.disabled = true; + } else { + searchButton.innerHTML = ''; + searchButton.disabled = false; + } + } + + // Other loading indicators can be updated here +} + +function showLoadingOverlay() { + let overlay = document.querySelector(".loading-overlay"); + + if (!overlay) { + overlay = document.createElement("div"); + overlay.className = "loading-overlay"; + overlay.innerHTML = ` +
+ +
+ `; + document.body.appendChild(overlay); + } + + overlay.classList.remove("hidden"); +} + +function hideLoadingOverlay() { + const overlay = document.querySelector(".loading-overlay"); + if (overlay) { + overlay.classList.add("hidden"); + } +} + +// Initialize Application +async function initializeApp() { + try { + showLoadingOverlay(); + + // Check API connection (blocking) + const isConnected = await apiService.checkConnection(); + appState.apiStatus = isConnected ? "connected" : "disconnected"; + console.log(`API Status: ${appState.apiStatus}`); + + if (!isConnected) { + hideLoadingOverlay(); + showNotification( + "error", + "Cannot connect to API server. Please ensure the backend service is running." + ); + + // Check if mainContent element exists + const mainContent = document.getElementById("mainContent"); + if (mainContent) { + mainContent.innerHTML = ` +
+
+

API Connection Error

+

The application could not connect to the backend API server.

+
+

Please make sure:

+
    +
  • The backend server is running (uvicorn api:app --reload)
  • +
  • The API is accessible at ${apiService.baseUrl}
  • +
+ +
+
+
+ `; + } + return; + } + + try { + // Load document types + await loadDocumentTypes(); + } catch (error) { + console.error("Failed to load document types:", error); + showNotification( + "warning", + "Could not load document types. Some filtering options may be unavailable." + ); + } + + try { + // Load initial documents + await loadDocuments(); + } catch (error) { + console.error("Failed to load documents:", error); + showNotification( + "error", + "Failed to load documents. Please check the console for details." + ); + } + + // Setup event listeners + setupEventListeners(); + + // Load chat history + loadChatHistory(); + } catch (error) { + console.error("Failed to initialize application:", error); + hideLoadingOverlay(); + showNotification( + "error", + "Application initialization failed. Please check the console for details." + ); + + // Show error on main content + const mainContent = document.getElementById("mainContent"); + if (mainContent) { + mainContent.innerHTML = ` +
+
+

Application Error

+

Something went wrong while initializing the application.

+
+

Error details:

+
${error.message || "Unknown error"}
+ +
+
+
+ `; + } + } finally { + // Mark initialization as complete + appState.isInitializing = false; + hideLoadingOverlay(); + } +} + +function setupEventListeners() { + // Search + if (elements.searchButton) { + elements.searchButton.addEventListener("click", () => { + if (elements.searchInput) { + performSearch(elements.searchInput.value); + } + }); + } + + if (elements.searchInput) { + elements.searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + performSearch(elements.searchInput.value); + } + }); + } + + // Tab navigation - fix for the main tabs + const mainTabLinks = document.querySelectorAll(".main-tab-link"); + mainTabLinks.forEach((tab) => { + tab.addEventListener("click", (e) => { + e.preventDefault(); // Prevent default anchor behavior + + // Remove active class from all tabs + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active class to clicked tab + tab.classList.add("active"); + + // Get the tab ID from the href attribute + const tabId = tab.getAttribute("href").substring(1); // Remove # from href + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + }); + }); + + // Mobile menu tab navigation + const mobileMenuLinks = document.querySelectorAll(".mobile-menu a"); + mobileMenuLinks.forEach((link) => { + link.addEventListener("click", (e) => { + e.preventDefault(); + const tabId = link.getAttribute("href").substring(1); + + // Hide mobile menu after selection + const mobileMenu = document.getElementById("mobileMenu"); + if (mobileMenu) mobileMenu.classList.remove("active"); + + // Remove active class from all main tabs + mainTabLinks.forEach((t) => t.classList.remove("active")); + + // Add active to the corresponding main tab + const correspondingTab = document.querySelector( + `.main-tab-link[href="#${tabId}"]` + ); + if (correspondingTab) correspondingTab.classList.add("active"); + + // Hide all tab content + document.querySelectorAll(".tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show the selected tab content + const tabContent = document.getElementById(tabId); + if (tabContent) { + tabContent.classList.add("active"); + } + }); + }); + + // Mobile menu toggle + if (elements.mobileMenuButton && elements.mobileMenu) { + elements.mobileMenuButton.addEventListener("click", () => { + elements.mobileMenu.classList.toggle("active"); + }); + } + + // Chat + if (elements.sendMessageButton) { + elements.sendMessageButton.addEventListener("click", sendChatMessage); + } + + if (elements.chatInput) { + elements.chatInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + sendChatMessage(); + } + }); + } +} + +function loadChatHistory() { + // Display existing chat messages + appState.aiChat.messages.forEach((message) => { + addChatMessage(message.content, message.role); + }); +} + +// Export functions for global access (needed for onclick handlers) +window.toggleBookmark = toggleBookmark; + +// Initialize the application when DOM is loaded +document.addEventListener("DOMContentLoaded", initializeApp); + +// Utility functions for document extraction +function extractLegalInfo(content) { + if (!content) return {}; + + const info = { + fileNumber: extractField(content, "File Number"), + status: extractField(content, "Status"), + caseNumber: extractField(content, "Case Number"), + dppSuggestion: extractField(content, "DPP Suggestion"), + hodDecision: extractField(content, "HOD Decision"), + allegations: extractAllegations(content), + persons: extractPersons(content), + acts: extractActs(content), + }; + return info; +} + +// Helper function to extract field values +function extractField(content, fieldName) { + const regex = new RegExp(`${fieldName}:\\s*([^\\n]+)`, "i"); + const match = content.match(regex); + return match ? match[1].trim() : null; +} + +// Extract allegations information +function extractAllegations(content) { + const allegations = []; + const allegationPattern = + /ALLEGATION #(\d+):([\s\S]*?)(?=ALLEGATION #|\n--- |$)/g; + let match; + + while ((match = allegationPattern.exec(content)) !== null) { + allegations.push({ + number: match[1], + details: match[2].trim().substring(0, 200) + "...", + }); + } + return allegations; +} + +// Extract person information +function extractPersons(content) { + const persons = []; + + // Try to find JSON-formatted person data + const jsonPersonPattern = + /\{"peranan":\s*"([^"]+)",\s*"category":\s*"([^"]+)",\s*"namaPihak":\s*"([^"]+)"\}/g; + let jsonMatch; + + while ((jsonMatch = jsonPersonPattern.exec(content)) !== null) { + try { + persons.push({ + name: jsonMatch[3], + role: jsonMatch[1], + category: jsonMatch[2], + }); + } catch (e) { + console.error("Error processing person data:", e); + } + } + + // If no JSON data found, try the old format as fallback + if (persons.length === 0) { + const personPattern = + /Person ID: (\d+)[\s\S]*?namaPerayuResponden['":]?\s*['":]([^'"]+)['"]/g; + let match; + + while ((match = personPattern.exec(content)) !== null) { + persons.push({ + id: match[1], + name: match[2].trim(), + role: "Unknown", + category: "Unknown", + }); + } + } + + return persons; +} + +// Extract legal acts and sections +function extractActs(content) { + const acts = []; + const actPattern = /(Akta [^\\n]+)|(Section [^\\n]+)|(Seksyen [^\\n]+)/gi; + let match; + + while ((match = actPattern.exec(content)) !== null) { + const act = match[0].trim(); + if (act && !acts.includes(act)) { + acts.push(act); + } + } + return acts.slice(0, 3); // Limit to 3 acts +} + +// Generate document summary based on extracted data +function generateDocumentSummary(data) { + let summary = '
'; + + // Case information + if (data.caseNumber || data.fileNumber) { + summary += '
'; + summary += '

Case Information:

'; + if (data.caseNumber) + summary += `

Case: ${data.caseNumber}

`; + if (data.fileNumber) + summary += `

File: ${data.fileNumber}

`; + if (data.status) + summary += `

Status: ${ + data.status + }

`; + summary += "
"; + } + + // Legal acts + if (data.acts && data.acts.length > 0) { + summary += '
'; + summary += '

Legal References:

'; + summary += '
    '; + data.acts.forEach((act) => { + summary += `
  • ${act}
  • `; + }); + summary += "
"; + } + + // Persons involved + if (data.persons && data.persons.length > 0) { + summary += '
'; + summary += '

Persons Involved:

'; + summary += '
    '; + data.persons.forEach((person) => { + summary += `
  • +
    ${person.name}
    + ${ + person.role + ? `
    ${person.role}
    ` + : "" + } + ${ + person.category + ? `
    ${person.category}
    ` + : "" + } +
  • `; + }); + summary += "
"; + } + + // Allegations count + if (data.allegations && data.allegations.length > 0) { + summary += '
'; + summary += `

Allegations: (${data.allegations.length})

`; + data.allegations.forEach((allegation) => { + summary += `
+ Allegation ${allegation.number}: +

${allegation.details}

+
`; + }); + summary += "
"; + } + + summary += "
"; + return summary; +} + +// Show notification +function showNotification(type, message) { + // Create notification element if it doesn't exist + let notification = document.querySelector(".notification"); + if (notification) { + // Remove existing notification first + notification.remove(); + } + + // Create new notification + notification = document.createElement("div"); + notification.className = `notification ${type}`; + + let icon; + switch (type) { + case "success": + icon = "check-circle"; + break; + case "error": + icon = "exclamation-circle"; + break; + case "warning": + icon = "exclamation-triangle"; + break; + default: + icon = "info-circle"; + } + + notification.innerHTML = ` +
+ + ${message} +
+ `; + + document.body.appendChild(notification); + + // Auto-remove after 5 seconds + setTimeout(() => { + notification.classList.add("notification-hide"); + setTimeout(() => { + if (notification && notification.parentNode) { + notification.remove(); + } + }, 300); + }, 5000); +} + +// Show loading indicator +function showLoader() { + showLoadingOverlay(); +} + +// Hide loading indicator +function hideLoader() { + hideLoadingOverlay(); +} diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..d294749 --- /dev/null +++ b/plan.md @@ -0,0 +1,298 @@ +# ๐Ÿ“‹ COMPREHENSIVE PLANNING: AGC AI Document Search Interface + +## ๐ŸŽฏ PROJECT OVERVIEW + +**Target**: Modular web application with separate HTML, CSS, and JavaScript files for AGC/Government staff to search, browse, and interact with legal documents using AI-powered search capabilities. + +**Key Context**: + +- AGC = Attorney General's Chambers (Malaysian Government Legal Department) +- Document types: LKK (Laporan Keputusan Kes - Case Decision Reports), Legal cases, Criminal cases +- Users: Government legal staff, prosecutors, legal researchers +- API provides: Document listing, filtering, detailed retrieval, AI-enhanced search, document types + +--- + +## ๐Ÿ—๏ธ PHASE-BY-PHASE DEVELOPMENT PLAN + +### **PHASE 1: ARCHITECTURE & FOUNDATION** + +**Objective**: Establish robust technical foundation + +**Components**: + +1. **Modular File Structure** + + - Separate HTML, CSS, and JavaScript files + - Well-organized directory structure + - `/index.html` - Main entry point + - `/css/` - Stylesheet directory + - `/js/` - JavaScript modules directory + - `/assets/` - Images, fonts, and other static assets + - CDN-based libraries (Tailwind CSS, Alpine.js, Heroicons) + - Progressive enhancement approach + +2. **API Integration Layer** + + - Centralized API service class + - Error handling & retry mechanisms + - Loading states management + - Offline detection + +3. **State Management** + - Global application state + - Search history persistence + - User preferences storage + - Session management + +--- + +### **PHASE 2: MODERN UI/UX DESIGN SYSTEM** + +**Objective**: Create government-appropriate, modern interface + +**Design Principles**: + +1. **Government Standards** + + - Professional color scheme (Navy, Gold, Gray) + - Accessibility compliance (WCAG 2.1 AA) + - High contrast, clear typography + - Consistent spacing & hierarchy + +2. **Visual Components** + + - Modern card-based layouts + - Subtle shadows & gradients + - Smooth animations (200-300ms transitions) + - Loading skeletons + - Micro-interactions + +3. **Layout System** + - Header with AGC branding + - Sidebar navigation + - Main content area with tabs + - Footer with system info + +--- + +### **PHASE 3: CORE FEATURES IMPLEMENTATION** + +**Objective**: Implement all API functionalities with enhanced UX + +**3.1 Document Discovery** + +- **Smart Document Browser** + - Grid/List view toggle + - Advanced filtering (Type, Date, Title, Content) + - Real-time search suggestions + - Document preview panels + - Bulk actions (Export, Print) + +**3.2 AI-Powered Search** + +- **Enhanced Search Interface** + - Natural language query input + - Search suggestions & auto-complete + - Query enhancement display + - Search history with favorites + - Advanced search builder + +**3.3 Document Viewer** + +- **Rich Document Display** + - Structured content presentation + - Metadata panels + - Related documents suggestions + - Highlight search terms + - Export/Print options + +--- + +### **PHASE 4: ADVANCED INTERACTIONS** + +**Objective**: Maximize user productivity and insights + +**4.1 Intelligent Features** + +- **AI Assistant Panel** + - Contextual chat interface + - Legal concept explanations + - Document summaries + - Cross-reference analysis + +**4.2 Data Visualization** + +- **Insights Dashboard** + - Document statistics + - Search trend analysis + - Case outcome patterns + - Performance metrics + +**4.3 Workflow Tools** + +- **Productivity Features** + - Bookmark system + - Note-taking capability + - Collection creation + - Sharing mechanisms + +--- + +### **PHASE 5: RESPONSIVE & MOBILE OPTIMIZATION** + +**Objective**: Ensure perfect mobile experience + +**5.1 Responsive Design** + +- **Breakpoint Strategy** + - Mobile-first approach + - Tablet optimizations + - Desktop enhancements + - Ultra-wide screen support + +**5.2 Mobile-Specific Features** + +- **Touch Optimizations** + - Swipe gestures + - Touch-friendly controls + - Voice search capability + - Offline reading mode + +--- + +### **PHASE 6: ANIMATIONS & POLISH** + +**Objective**: Industry-standard animations and user experience + +**6.1 Micro-Interactions** + +- **Delightful Details** + - Button hover effects + - Loading animations + - Success/Error feedback + - Progress indicators + +**6.2 Page Transitions** + +- **Smooth Animations** + - Fade transitions + - Slide animations + - Scale effects + - Parallax elements + +**6.3 Performance Optimizations** + +- **Speed Enhancements** + - Lazy loading + - Image optimization + - Caching strategies + - Bundle optimization + +--- + +## ๐ŸŽจ DETAILED UI/UX SPECIFICATIONS + +### **Color Palette** + +``` +Primary: #1E40AF (Government Blue) +Secondary: #D97706 (Gold/Amber) +Success: #059669 (Green) +Warning: #D97706 (Orange) +Error: #DC2626 (Red) +Neutral: #374151, #6B7280, #9CA3AF, #F3F4F6 +``` + +### **Typography** + +``` +Headers: Inter, system-ui (700, 600, 500) +Body: Inter, system-ui (400, 500) +Monospace: 'Fira Code', monospace +``` + +### **Component Library** + +- **Cards**: Shadow-based with hover effects +- **Buttons**: Primary, Secondary, Ghost variants +- **Inputs**: Floating labels, validation states +- **Modals**: Backdrop blur, slide-up animation +- **Tables**: Sortable, filterable, exportable +- **Charts**: Interactive D3.js visualizations + +--- + +## ๐Ÿ“ฑ MOBILE RESPONSIVENESS STRATEGY + +### **Breakpoints** + +- **Mobile**: 320px - 767px (Stack layout, touch-optimized) +- **Tablet**: 768px - 1023px (Hybrid layout, gesture support) +- **Desktop**: 1024px+ (Multi-column, keyboard shortcuts) + +### **Mobile Features** + +- **Navigation**: Bottom tab bar, slide-out menu +- **Search**: Voice input, predictive text +- **Reading**: Optimized typography, night mode +- **Gestures**: Swipe navigation, pinch zoom + +--- + +## ๐Ÿš€ TECHNICAL IMPLEMENTATION DETAILS + +### **Libraries & CDNs** + +```html +- Tailwind CSS 3.x (Styling) - Alpine.js 3.x (Reactivity) - Chart.js 4.x +(Visualizations) - Heroicons (Icons) - Inter Font (Typography) - Animate.css +(Animations) +``` + +### **API Integration** + +```javascript +- Fetch API with retry logic +- Real-time search debouncing +- Pagination handling +- Error boundary implementation +- Loading state management +``` + +### **Performance Targets** + +- **First Paint**: < 1.0s (improved from 1.5s with separate files) +- **Time to Interactive**: < 2.5s (improved from 3s) +- **Bundle Size**: + - CSS: < 100KB (minified) + - JS: < 300KB (minified and compressed) + - Total: < 450KB (reduced from 500KB) +- **Lighthouse Score**: 95+ (improved from 90+) + +### **Additional Performance Benefits** + +- **Parallel Loading**: Browser can load HTML, CSS, and JS in parallel +- **Caching**: Individual files can be cached separately +- **Code Splitting**: JavaScript can be split into modules loaded on demand +- **Resource Hints**: Preload and prefetch for critical resources + +--- + +## ๐ŸŽฏ SUCCESS METRICS + +### **User Experience** + +- **Task Completion**: 95%+ success rate +- **Search Accuracy**: High relevance scores +- **Mobile Usage**: Seamless across devices +- **Accessibility**: WCAG 2.1 AA compliance + +### **Performance** + +- **Load Time**: Sub-3 second loading +- **Responsiveness**: Smooth 60fps animations +- **API Response**: < 500ms average +- **Error Rate**: < 1% failure rate + +--- diff --git a/serve.py b/serve.py new file mode 100644 index 0000000..9f3ee78 --- /dev/null +++ b/serve.py @@ -0,0 +1,180 @@ +from http.server import HTTPServer, SimpleHTTPRequestHandler +import os +import sys +import shutil +import mimetypes + +# Ensure proper MIME types are registered +mimetypes.add_type('text/css', '.css') +mimetypes.add_type('application/javascript', '.js') +mimetypes.add_type('text/javascript', '.js') +mimetypes.add_type('image/png', '.png') +mimetypes.add_type('image/x-icon', '.ico') + +class AGCDocumentHandler(SimpleHTTPRequestHandler): + """Custom request handler for serving files from the frontend directory.""" + + def __init__(self, *args, directory=None, **kwargs): + self.frontend_dir = os.path.join(os.getcwd(), 'frontend') + if directory is None: + directory = self.frontend_dir + # This is important - SimpleHTTPRequestHandler expects the directory in kwargs + super().__init__(*args, directory=directory, **kwargs) + + def do_GET(self): + """Handle GET requests with special cases for root and favicon.""" + original_path = self.path + + # Handle special paths + if self.path == '/': + self.path = '/index.html' + + # For debugging - print resolved file path + resolved_path = os.path.join(self.frontend_dir, self.path.lstrip('/')) + file_exists = os.path.exists(resolved_path) + + # Determine content type + content_type = self.guess_type(resolved_path) + print(f"Request: {original_path} โ†’ Resolved: {resolved_path} (Exists: {file_exists}, Type: {content_type})") + + # Try alternate path resolution for CSS/JS + if not file_exists and (original_path.endswith('.css') or original_path.endswith('.js')): + alternate_path = os.path.join(self.frontend_dir, original_path.lstrip('/')) + print(f"Trying alternate path: {alternate_path}") + if os.path.exists(alternate_path): + self.path = original_path # Use original path + print(f"Using alternate path resolution") + + return super().do_GET() + + def send_header(self, keyword, value): + """Override to ensure proper content types are set.""" + if keyword.lower() == 'content-type' and self.path.endswith('.css'): + value = 'text/css' + print(f"Setting Content-Type: {value} for {self.path}") + elif keyword.lower() == 'content-type' and self.path.endswith('.js'): + value = 'application/javascript' + print(f"Setting Content-Type: {value} for {self.path}") + super().send_header(keyword, value) + + def send_error(self, code, message=None, explain=None): + """Enhanced error logging for 404 errors.""" + if code == 404: + print(f"โŒ 404 ERROR: File not found: {self.path}") + print(f" Resolved path: {os.path.join(self.frontend_dir, self.path.lstrip('/'))}") + print(f" Referrer: {self.headers.get('Referer', 'None')}") + return super().send_error(code, message, explain) + + def log_message(self, format, *args): + """Colorize log messages for better readability.""" + status_code = args[1] + if status_code.startswith('2'): # 2xx status codes + status_color = '\033[92m' # Green + elif status_code.startswith('3'): # 3xx status codes + status_color = '\033[94m' # Blue + elif status_code.startswith('4'): # 4xx status codes + status_color = '\033[93m' # Yellow + elif status_code.startswith('5'): # 5xx status codes + status_color = '\033[91m' # Red + else: + status_color = '\033[0m' # Default + + reset_color = '\033[0m' + sys.stdout.write(f"{self.log_date_time_string()} {status_color}{format % args}{reset_color}\n") + +def verify_files(): + """Verify that necessary files exist in the frontend directory.""" + frontend_dir = os.path.join(os.getcwd(), 'frontend') + if not os.path.exists(frontend_dir): + print(f'Error: Frontend directory not found at {frontend_dir}') + sys.exit(1) + + index_path = os.path.join(frontend_dir, 'index.html') + if not os.path.exists(index_path): + print(f'Error: index.html not found at {index_path}') + sys.exit(1) + + # Check key files + key_files = [ + ('assets/images/favicon.ico', 'Favicon'), + ('css/styles.css', 'Main CSS'), + ('css/animations.css', 'Animations CSS'), + ('js/api.js', 'API JavaScript'), + ('js/main.js', 'Main JavaScript') + ] + + print("Checking key files:") + for file_path, desc in key_files: + full_path = os.path.join(frontend_dir, file_path) + exists = os.path.exists(full_path) + status = "โœ… Found" if exists else "โŒ MISSING" + print(f" {status}: {desc} ({file_path})") + + if file_path == 'assets/images/favicon.ico' and exists: + # Copy favicon to root of frontend for browsers that look for it there + root_favicon = os.path.join(frontend_dir, 'favicon.ico') + if not os.path.exists(root_favicon): + try: + shutil.copy2(full_path, root_favicon) + print(f" โœ… Copied favicon to frontend root for compatibility") + except Exception as e: + print(f" โŒ Could not copy favicon to root: {str(e)}") + + return frontend_dir + +def run_server(port=8888): + """Run the HTTP server on the specified port.""" + try: + # Verify that necessary files exist + frontend_dir = verify_files() + + # Print MIME type registrations for debugging + print("\nMIME type configuration:") + for ext, type in [('.html', mimetypes.guess_type('file.html')[0]), + ('.css', mimetypes.guess_type('file.css')[0]), + ('.js', mimetypes.guess_type('file.js')[0]), + ('.png', mimetypes.guess_type('file.png')[0]), + ('.ico', mimetypes.guess_type('file.ico')[0])]: + print(f" {ext} โ†’ {type}") + + # Create and start the server + handler = lambda *args, **kwargs: AGCDocumentHandler(*args, directory=frontend_dir, **kwargs) + server_address = ('', port) + httpd = HTTPServer(server_address, handler) + + print(f"\nServer running at http://localhost:{port}/") + print(f"Serving files from: {frontend_dir}") + print("Press Ctrl+C to stop the server") + httpd.serve_forever() + + except PermissionError: + print(f'\nError: Permission denied for port {port}') + print('Try running with a different port number:') + print(f'python {sys.argv[0]} ') + sys.exit(1) + except OSError as e: + if e.errno == 98 or e.errno == 10048: # Port already in use (Linux: 98, Windows: 10048) + print(f'\nError: Port {port} is already in use') + print('Try running with a different port number:') + print(f'python {sys.argv[0]} ') + else: + print(f'\nError: {str(e)}') + sys.exit(1) + except KeyboardInterrupt: + print('\nServer stopped by user') + sys.exit(0) + except Exception as e: + print(f'\nUnexpected error: {str(e)}') + sys.exit(1) + +if __name__ == '__main__': + # Get port from command line argument if provided + port = 8888 + if len(sys.argv) > 1: + try: + port = int(sys.argv[1]) + except ValueError: + print('Error: Port must be a number') + sys.exit(1) + + run_server(port) \ No newline at end of file diff --git a/templates/old_index.html b/templates/old_index.html new file mode 100644 index 0000000..7ec0a17 --- /dev/null +++ b/templates/old_index.html @@ -0,0 +1,1067 @@ + + + + + + AGC Document Chatbot + + + + +
+

AGC Document Chatbot

+ + +
+ + + +
+ + +
+
+ + + +
+
+
+ + + + + +
+
+ + +
+
+
+ +
Loading...
+
+
+ + + + diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..03d169c --- /dev/null +++ b/test_api.py @@ -0,0 +1,139 @@ +""" +AGC Document Chatbot - API Test Suite + +This script provides comprehensive testing for all API endpoints: +- Document listing and filtering +- Document retrieval +- Search functionality (both enhanced AI and simple fallback) +- Error handling and edge cases + +Usage: + python test_api.py + +Prerequisites: + - API server running on localhost:8000 + - requests library installed +""" + +import requests +import json +from typing import Dict, Any +from pprint import pprint + +# API base URL +BASE_URL = "http://localhost:8000" + +def test_root(): + """Test the root endpoint""" + print("\n=== Testing Root Endpoint ===") + response = requests.get(f"{BASE_URL}/") + pprint(response.json()) + +def test_list_documents(): + """Test document listing with different filters""" + print("\n=== Testing Document Listing ===") + + # List all documents + print("\nListing all documents:") + response = requests.get(f"{BASE_URL}/documents") + pprint(response.json()[:2]) # Show first 2 documents + + # Filter by document type + print("\nFiltering by document type:") + response = requests.get(f"{BASE_URL}/documents", params={"doc_type": "LKK"}) + pprint(response.json()[:2]) + + # Filter by title/content + print("\nFiltering by title/content:") + response = requests.get(f"{BASE_URL}/documents", params={"title_filter": "case"}) + pprint(response.json()[:2]) + + # Combined filters + print("\nCombined filters:") + response = requests.get( + f"{BASE_URL}/documents", + params={ + "doc_type": "LKK", + "title_filter": "case" + } + ) + pprint(response.json()[:2]) + +def test_document_types(): + """Test getting available document types""" + print("\n=== Testing Document Types ===") + response = requests.get(f"{BASE_URL}/document-types") + pprint(response.json()) + +def test_get_document(): + """Test getting a specific document""" + print("\n=== Testing Document Retrieval ===") + + # First, get a list of documents to find a valid ID + documents = requests.get(f"{BASE_URL}/documents").json() + if documents: + doc_id = documents[0]['id'] + print(f"\nRetrieving document with ID {doc_id}:") + response = requests.get(f"{BASE_URL}/documents/{doc_id}") + pprint(response.json()) + + # Test invalid document ID + print("\nTesting invalid document ID:") + response = requests.get(f"{BASE_URL}/documents/99999") + pprint(response.json()) + +def test_search(): + """Test document search functionality""" + print("\n=== Testing Document Search ===") + + # Simple search + print("\nSimple search:") + search_data = { + "query": "criminal case involving drugs", + "profile_search": False + } + response = requests.post( + f"{BASE_URL}/search", + json=search_data + ) + result = response.json() + print(f"Original Query: {result['query']}") + print(f"Enhanced Query: {result['enhanced_query']}") + print("\nTop matching documents:") + for doc in result['documents'][:2]: # Show first 2 results + print(f"\nTitle: {doc['title']}") + print(f"Relevance: {doc['similarity']:.2f}") + print(f"Preview: {doc['content_preview'][:200]}...") + + # Legal concept search + print("\nLegal concept search:") + search_data = { + "query": "explain the concept of mens rea in criminal cases", + "profile_search": False + } + response = requests.post( + f"{BASE_URL}/search", + json=search_data + ) + result = response.json() + print(f"Original Query: {result['query']}") + print(f"Enhanced Query: {result['enhanced_query']}") + print(f"AI Answer: {result['answer']}") + +def run_all_tests(): + """Run all API tests""" + try: + test_root() + test_document_types() + test_list_documents() + test_get_document() + test_search() + print("\n=== All tests completed successfully ===") + except requests.exceptions.ConnectionError: + print("\nError: Could not connect to the API server.") + print("Make sure the API is running with: uvicorn api:app --reload") + except Exception as e: + print(f"\nError during testing: {str(e)}") + +if __name__ == "__main__": + run_all_tests() \ No newline at end of file