add ui interface utilizing fast api
This commit is contained in:
parent
881dcc4986
commit
b84bdf17f0
383
README.md
383
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**
|
||||
|
3570
agc-document-search.html
Normal file
3570
agc-document-search.html
Normal file
File diff suppressed because it is too large
Load Diff
166
api.py
Normal file
166
api.py
Normal file
@ -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)
|
@ -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)
|
||||
try:
|
||||
# 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)
|
||||
# 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['content']
|
||||
content_preview = content[:300] + "..." if len(content) > 300 else content
|
||||
# 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['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
|
||||
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]
|
||||
# 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."
|
||||
}
|
@ -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
|
121
embedding/simple_search_service.py
Normal file
121
embedding/simple_search_service.py
Normal file
@ -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."
|
||||
}
|
BIN
frontend/assets/images/agc-logo.png
Normal file
BIN
frontend/assets/images/agc-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
frontend/assets/images/favicon.ico
Normal file
BIN
frontend/assets/images/favicon.ico
Normal file
Binary file not shown.
885
frontend/components.html
Normal file
885
frontend/components.html
Normal file
@ -0,0 +1,885 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AGC Design System - Component Library</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Design system for the Attorney General's Chambers Document Search Interface"
|
||||
/>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- External CSS Libraries -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link href="css/styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900">
|
||||
<div class="skip-to-content" tabindex="0">Skip to content</div>
|
||||
|
||||
<header class="bg-white shadow-sm">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<h1 class="text-2xl font-bold text-blue-900">AGC Design System</h1>
|
||||
<p class="text-gray-600">
|
||||
Component Library for the Attorney General's Chambers Document Search
|
||||
Interface
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container mx-auto px-4 py-8">
|
||||
<!-- Table of Contents -->
|
||||
<section class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Table of Contents</h2>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href="#colors"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Color Palette</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#typography"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Typography</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#buttons"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Buttons</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#forms"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Form Controls</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#cards"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Cards</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#badges"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Badges</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#alerts"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Alerts & Toasts</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#tabs"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Tabs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#tooltips"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Tooltips & Dropdowns</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#animations"
|
||||
class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Animations</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Color Palette -->
|
||||
<section id="colors" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Color Palette</h2>
|
||||
|
||||
<h3 class="text-lg font-medium mt-6 mb-3">Primary Colors (Navy)</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<div class="h-20 bg-navy-900 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Navy 900</div>
|
||||
<div class="text-gray-600">#1e2d4c</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-navy-700 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Navy 700</div>
|
||||
<div class="text-gray-600">#2c4272</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-navy-500 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Navy 500</div>
|
||||
<div class="text-gray-600">#4065a2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-navy-300 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Navy 300</div>
|
||||
<div class="text-gray-600">#8eaed3</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-navy-100 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Navy 100</div>
|
||||
<div class="text-gray-600">#dbe6f3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mt-6 mb-3">Secondary Colors (Gold)</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div>
|
||||
<div class="h-20 bg-gold-900 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Gold 900</div>
|
||||
<div class="text-gray-600">#653310</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-gold-700 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Gold 700</div>
|
||||
<div class="text-gray-600">#965709</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-gold-500 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Gold 500</div>
|
||||
<div class="text-gray-600">#e29807</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-gold-300 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Gold 300</div>
|
||||
<div class="text-gray-600">#f8cb47</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-gold-100 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Gold 100</div>
|
||||
<div class="text-gray-600">#fdf1c7</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mt-6 mb-3">UI Colors</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div class="h-20 bg-success rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Success</div>
|
||||
<div class="text-gray-600">#059669</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-warning rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Warning</div>
|
||||
<div class="text-gray-600">#d97706</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-error rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Error</div>
|
||||
<div class="text-gray-600">#dc2626</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="h-20 bg-gray-100 rounded-t-lg"></div>
|
||||
<div
|
||||
class="bg-white p-2 border border-gray-200 rounded-b-lg text-xs"
|
||||
>
|
||||
<div class="font-semibold">Gray</div>
|
||||
<div class="text-gray-600">#f3f4f6</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Typography -->
|
||||
<section id="typography" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Typography</h2>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Headings</h3>
|
||||
<div class="space-y-4 border-b border-gray-200 pb-6 mb-6">
|
||||
<div>
|
||||
<h1>Heading 1</h1>
|
||||
<div class="text-sm text-gray-500">
|
||||
font-size: 1.875rem (30px) | font-weight: 700
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Heading 2</h2>
|
||||
<div class="text-sm text-gray-500">
|
||||
font-size: 1.5rem (24px) | font-weight: 600
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Heading 3</h3>
|
||||
<div class="text-sm text-gray-500">
|
||||
font-size: 1.25rem (20px) | font-weight: 600
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Heading 4</h4>
|
||||
<div class="text-sm text-gray-500">
|
||||
font-size: 1.125rem (18px) | font-weight: 600
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Body Text</h3>
|
||||
<div class="space-y-4 border-b border-gray-200 pb-6 mb-6">
|
||||
<div>
|
||||
<p class="text-base">
|
||||
Regular text (16px): The Attorney General's Chambers of Malaysia
|
||||
provides legal advice to the Government and represents the
|
||||
Government in legal proceedings.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm">
|
||||
Small text (14px): The Attorney General's Chambers of Malaysia
|
||||
provides legal advice to the Government and represents the
|
||||
Government in legal proceedings.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs">
|
||||
Extra small text (12px): The Attorney General's Chambers of
|
||||
Malaysia provides legal advice to the Government and represents
|
||||
the Government in legal proceedings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Text Styling</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p class="font-bold mb-2">Bold text</p>
|
||||
<p class="font-semibold mb-2">Semibold text</p>
|
||||
<p class="font-medium mb-2">Medium text</p>
|
||||
<p class="font-normal mb-2">Normal text</p>
|
||||
<p class="italic mb-2">Italic text</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="underline mb-2">Underlined text</p>
|
||||
<p class="line-through mb-2">Strikethrough text</p>
|
||||
<p class="text-navy-700 mb-2">Colored text</p>
|
||||
<p class="text-gold-600 mb-2">Colored text</p>
|
||||
<p>
|
||||
<a href="#" class="text-blue-600 hover:text-blue-800 transition"
|
||||
>Link text</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Buttons -->
|
||||
<section id="buttons" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Buttons</h2>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Button Variants</h3>
|
||||
<div class="flex flex-wrap gap-4 border-b border-gray-200 pb-6 mb-6">
|
||||
<button class="btn btn-primary">Primary Button</button>
|
||||
<button class="btn btn-secondary">Secondary Button</button>
|
||||
<button class="btn btn-outline">Outline Button</button>
|
||||
<button class="btn btn-ghost">Ghost Button</button>
|
||||
<button class="btn btn-primary" disabled>Disabled Button</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Button Sizes</h3>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-4 border-b border-gray-200 pb-6 mb-6"
|
||||
>
|
||||
<button class="btn btn-primary btn-sm">Small Button</button>
|
||||
<button class="btn btn-primary">Regular Button</button>
|
||||
<button class="btn btn-primary btn-lg">Large Button</button>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Button with Icon</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<button class="btn btn-primary flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Add New
|
||||
</button>
|
||||
<button class="btn btn-outline flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
<button class="btn btn-icon btn-ghost">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Form Controls -->
|
||||
<section id="forms" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Form Controls</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Text Inputs</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="form-group">
|
||||
<label for="default-input" class="form-label"
|
||||
>Default Input</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="default-input"
|
||||
class="form-control"
|
||||
placeholder="Enter text"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="focused-input" class="form-label"
|
||||
>Focused Input</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="focused-input"
|
||||
class="form-control focus"
|
||||
placeholder="This input has focus"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="disabled-input" class="form-label"
|
||||
>Disabled Input</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="disabled-input"
|
||||
class="form-control"
|
||||
placeholder="Disabled input"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Other Form Elements</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="form-group">
|
||||
<label for="select-input" class="form-label"
|
||||
>Select Input</label
|
||||
>
|
||||
<select id="select-input" class="form-control">
|
||||
<option value="">Select an option</option>
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="textarea-input" class="form-label"
|
||||
>Textarea</label
|
||||
>
|
||||
<textarea
|
||||
id="textarea-input"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Enter multiple lines of text"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Checkbox</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="checkbox1"
|
||||
class="h-4 w-4 text-blue-600"
|
||||
/>
|
||||
<label for="checkbox1">Checkbox option</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mt-6 mb-3">Search Input</h3>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control pl-10 pr-4"
|
||||
placeholder="Search for legal documents..."
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cards -->
|
||||
<section id="cards" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Cards</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Basic Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="text-lg font-medium">Basic Card</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
This is a basic card with header, body, and footer sections.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-sm btn-primary">Action</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document Card -->
|
||||
<div class="document-card">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<span class="document-tag document-tag-lkk">LKK</span>
|
||||
<h3 class="font-medium">
|
||||
Pendakwa Raya v. Ahmad bin Abdullah
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">[2023] 5 MLJ 123</p>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-400 hover:text-amber-600 transition duration-200"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-gray-700 mt-3 line-clamp-2">
|
||||
Criminal case involving drug trafficking under Section 39B of
|
||||
the Dangerous Drugs Act 1952.
|
||||
</p>
|
||||
<div class="mt-3 text-xs text-gray-500">
|
||||
Last accessed: 2 days ago
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stat Card -->
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Documents</div>
|
||||
<div class="stat-value">1,234</div>
|
||||
<div class="stat-change stat-change-positive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-1"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
12% from last month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Badges -->
|
||||
<section id="badges" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Badges</h2>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Badge Variants</h3>
|
||||
<div class="flex flex-wrap gap-2 border-b border-gray-200 pb-6 mb-6">
|
||||
<span class="badge badge-primary">Primary</span>
|
||||
<span class="badge badge-secondary">Secondary</span>
|
||||
<span class="badge badge-success">Success</span>
|
||||
<span class="badge badge-warning">Warning</span>
|
||||
<span class="badge badge-error">Error</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Outline Badges</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="badge badge-outline text-primary">Primary</span>
|
||||
<span class="badge badge-outline text-secondary">Secondary</span>
|
||||
<span class="badge badge-outline text-success">Success</span>
|
||||
<span class="badge badge-outline text-warning">Warning</span>
|
||||
<span class="badge badge-outline text-error">Error</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Alerts & Toasts -->
|
||||
<section id="alerts" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Alerts & Toasts</h2>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Toast Notifications</h3>
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<button class="btn btn-outline" onclick="showToast('default')">
|
||||
Default Toast
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline text-success"
|
||||
onclick="showToast('success')"
|
||||
>
|
||||
Success Toast
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline text-warning"
|
||||
onclick="showToast('warning')"
|
||||
>
|
||||
Warning Toast
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline text-error"
|
||||
onclick="showToast('error')"
|
||||
>
|
||||
Error Toast
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container (for demonstration) -->
|
||||
<div class="toast-container hidden" id="toastDemo">
|
||||
<div class="toast">This is a default toast notification</div>
|
||||
<div class="toast toast-success">
|
||||
This is a success toast notification
|
||||
</div>
|
||||
<div class="toast toast-warning">
|
||||
This is a warning toast notification
|
||||
</div>
|
||||
<div class="toast toast-error">
|
||||
This is an error toast notification
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tabs -->
|
||||
<section id="tabs" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Tabs</h2>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab tab-active" data-tab="tab1">All Documents</div>
|
||||
<div class="tab" data-tab="tab2">LKK Reports</div>
|
||||
<div class="tab" data-tab="tab3">Legal Cases</div>
|
||||
<div class="tab" data-tab="tab4">Criminal Cases</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content tab-content-active" id="tab1">
|
||||
<p class="py-4">This is the content for All Documents tab.</p>
|
||||
</div>
|
||||
<div class="tab-content" id="tab2">
|
||||
<p class="py-4">This is the content for LKK Reports tab.</p>
|
||||
</div>
|
||||
<div class="tab-content" id="tab3">
|
||||
<p class="py-4">This is the content for Legal Cases tab.</p>
|
||||
</div>
|
||||
<div class="tab-content" id="tab4">
|
||||
<p class="py-4">This is the content for Criminal Cases tab.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tooltips & Dropdowns -->
|
||||
<section id="tooltips" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Tooltips & Dropdowns</h2>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Tooltips</h3>
|
||||
<div class="flex flex-wrap gap-4 border-b border-gray-200 pb-6 mb-6">
|
||||
<div class="tooltip">
|
||||
<button class="btn btn-outline">Hover Me</button>
|
||||
<span class="tooltip-text">This is a tooltip</span>
|
||||
</div>
|
||||
|
||||
<div class="tooltip">
|
||||
<span class="text-blue-600 underline cursor-help"
|
||||
>What is LKK?</span
|
||||
>
|
||||
<span class="tooltip-text"
|
||||
>Laporan Keputusan Kes (Case Decision Reports)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-medium mb-3">Dropdowns</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline dropdown-toggle">
|
||||
Click to Open
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="#" class="dropdown-item">Option 1</a>
|
||||
<a href="#" class="dropdown-item">Option 2</a>
|
||||
<a href="#" class="dropdown-item">Option 3</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown">
|
||||
<button
|
||||
class="btn btn-primary dropdown-toggle flex items-center gap-2"
|
||||
>
|
||||
<span>User Actions</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a href="#" class="dropdown-item">Profile</a>
|
||||
<a href="#" class="dropdown-item">Settings</a>
|
||||
<a href="#" class="dropdown-item">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Animations -->
|
||||
<section id="animations" class="mb-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Animations</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Fade Animations</h3>
|
||||
<div class="space-y-4">
|
||||
<button class="btn btn-primary animate-fade-in">Fade In</button>
|
||||
<button class="btn btn-primary animate-fade-in-up">
|
||||
Fade In Up
|
||||
</button>
|
||||
<button class="btn btn-primary animate-fade-in-down">
|
||||
Fade In Down
|
||||
</button>
|
||||
<button class="btn btn-primary animate-fade-in-left">
|
||||
Fade In Left
|
||||
</button>
|
||||
<button class="btn btn-primary animate-fade-in-right">
|
||||
Fade In Right
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Other Animations</h3>
|
||||
<div class="space-y-4">
|
||||
<button class="btn btn-primary animate-slide-up">
|
||||
Slide Up
|
||||
</button>
|
||||
<div
|
||||
class="animate-pulse bg-blue-200 text-blue-800 p-3 rounded"
|
||||
>
|
||||
Pulse Animation
|
||||
</div>
|
||||
<div
|
||||
class="animate-spin h-8 w-8 border-4 border-blue-600 border-t-transparent rounded-full"
|
||||
></div>
|
||||
<div
|
||||
class="animate-bounce h-8 w-8 bg-blue-600 rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="bg-blue-900 text-white py-8 mt-10">
|
||||
<div class="container mx-auto px-4">
|
||||
<p class="text-center text-sm text-blue-200">
|
||||
AGC Design System - Attorney General's Chambers Document Search
|
||||
Interface
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Demo script for the component library -->
|
||||
<script>
|
||||
// Tabs functionality
|
||||
document.querySelectorAll(".tab").forEach((tab) => {
|
||||
tab.addEventListener("click", function () {
|
||||
// Remove active class from all tabs
|
||||
document
|
||||
.querySelectorAll(".tab")
|
||||
.forEach((t) => t.classList.remove("tab-active"));
|
||||
|
||||
// Add active class to clicked tab
|
||||
this.classList.add("tab-active");
|
||||
|
||||
// Hide all tab content
|
||||
document.querySelectorAll(".tab-content").forEach((content) => {
|
||||
content.classList.remove("tab-content-active");
|
||||
});
|
||||
|
||||
// Show corresponding tab content
|
||||
const tabId = this.getAttribute("data-tab");
|
||||
document.getElementById(tabId).classList.add("tab-content-active");
|
||||
});
|
||||
});
|
||||
|
||||
// Toast demo functionality
|
||||
function showToast(type) {
|
||||
const toastDemo = document.getElementById("toastDemo");
|
||||
toastDemo.classList.remove("hidden");
|
||||
|
||||
setTimeout(() => {
|
||||
toastDemo.classList.add("hidden");
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Add style for demo color classes
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.bg-navy-900 { background-color: #1e2d4c; }
|
||||
.bg-navy-700 { background-color: #2c4272; }
|
||||
.bg-navy-500 { background-color: #4065a2; }
|
||||
.bg-navy-300 { background-color: #8eaed3; }
|
||||
.bg-navy-100 { background-color: #dbe6f3; }
|
||||
|
||||
.bg-gold-900 { background-color: #653310; }
|
||||
.bg-gold-700 { background-color: #965709; }
|
||||
.bg-gold-500 { background-color: #e29807; }
|
||||
.bg-gold-300 { background-color: #f8cb47; }
|
||||
.bg-gold-100 { background-color: #fdf1c7; }
|
||||
|
||||
.bg-success { background-color: #059669; }
|
||||
.bg-warning { background-color: #d97706; }
|
||||
.bg-error { background-color: #dc2626; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
286
frontend/css/animations.css
Normal file
286
frontend/css/animations.css
Normal file
@ -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);
|
||||
}
|
1094
frontend/css/styles.css
Normal file
1094
frontend/css/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
frontend/favicon.ico
Normal file
BIN
frontend/favicon.ico
Normal file
Binary file not shown.
2350
frontend/index.html
Normal file
2350
frontend/index.html
Normal file
File diff suppressed because it is too large
Load Diff
1822
frontend/index.html.bak
Normal file
1822
frontend/index.html.bak
Normal file
File diff suppressed because it is too large
Load Diff
118
frontend/js/api.js
Normal file
118
frontend/js/api.js
Normal file
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
1820
frontend/js/main.js
Normal file
1820
frontend/js/main.js
Normal file
File diff suppressed because it is too large
Load Diff
1315
frontend/js/main.js.backup
Normal file
1315
frontend/js/main.js.backup
Normal file
File diff suppressed because it is too large
Load Diff
1315
frontend/js/main.js.bak
Normal file
1315
frontend/js/main.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
298
plan.md
Normal file
298
plan.md
Normal file
@ -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
|
||||
|
||||
---
|
180
serve.py
Normal file
180
serve.py
Normal file
@ -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]} <port_number>')
|
||||
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]} <port_number>')
|
||||
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)
|
1067
templates/old_index.html
Normal file
1067
templates/old_index.html
Normal file
File diff suppressed because it is too large
Load Diff
139
test_api.py
Normal file
139
test_api.py
Normal file
@ -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()
|
Loading…
x
Reference in New Issue
Block a user