AldawsariNLP commited on
Commit
df31088
·
0 Parent(s):

Initial commit: Cleaned up project for HuggingFace Spaces deployment

Browse files
.dockerignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ __pycache__
3
+ *.pyc
4
+ .env
5
+ .git
6
+ .gitignore
7
+ README.md
8
+ .vscode
9
+ .idea
10
+ documents/*.pdf
11
+ documents/*.docx
12
+ documents/*.txt
13
+ vectorstore
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
.gitignore ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ .venv/
11
+ *.egg-info/
12
+ dist/
13
+ # build/
14
+
15
+ # uv
16
+ .venv/
17
+ # uv.lock - included for reproducible builds
18
+
19
+ # Environment variables
20
+ .env
21
+ .env.local
22
+
23
+ # Vectorstore - included in repository (as requested)
24
+ # vectorstore/
25
+ # *.pkl
26
+ # *.faiss
27
+
28
+ # Processed documents JSON - included in repository (as requested)
29
+ # processed_documents.json
30
+
31
+ # Node
32
+ node_modules/
33
+ npm-debug.log*
34
+ yarn-debug.log*
35
+ yarn-error.log*
36
+
37
+ # React build (keep for deployment)
38
+
39
+
40
+ # IDE
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+
47
+ # OS
48
+ .DS_Store
49
+ Thumbs.db
50
+
51
+ # Documents (optional - you may want to track these)
52
+ documents/
53
+ documents1/
54
+
55
+ # documents/*.docx
56
+ # documents/*.txt
57
+
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies and uv
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ && rm -rf /var/lib/apt/lists/* \
10
+ && pip install uv
11
+
12
+ # Copy pyproject.toml and uv.lock for dependency management
13
+ COPY pyproject.toml uv.lock ./
14
+
15
+ # Install Python dependencies using uv
16
+ RUN uv pip install --system .
17
+
18
+ # Copy backend
19
+ # Copy backend code
20
+ COPY backend/ ./backend/
21
+
22
+ # Copy processed documents and vector data
23
+ # COPY documents/ ./documents/
24
+ COPY processed_documents.json ./processed_documents.json
25
+
26
+ # Copy built frontend bundle
27
+ COPY frontend/build/ ./frontend/build/
28
+
29
+ # Copy main app entry point
30
+ COPY app.py .
31
+
32
+
33
+ # Expose port (Hugging Face Spaces uses 7860)
34
+ EXPOSE 7860
35
+
36
+ # Run the application
37
+ CMD ["python", "app.py"]
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
GITHUB_SETUP.md ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GitHub Setup Guide
2
+
3
+ This guide will help you set up GitHub synchronization for your project.
4
+
5
+ ## Prerequisites
6
+
7
+ ### 1. Install Git
8
+
9
+ Git is not currently installed on your system. Please install it:
10
+
11
+ **Windows:**
12
+ 1. Download Git from: https://git-scm.com/download/win
13
+ 2. Run the installer and follow the setup wizard
14
+ 3. Choose "Git from the command line and also from 3rd-party software" when prompted
15
+ 4. Restart your terminal/PowerShell after installation
16
+
17
+ **Verify Installation:**
18
+ ```powershell
19
+ git --version
20
+ ```
21
+
22
+ ### 2. Configure Git (First Time Only)
23
+
24
+ After installing Git, configure your name and email:
25
+
26
+ ```powershell
27
+ git config --global user.name "Your Name"
28
+ git config --global user.email "[email protected]"
29
+ ```
30
+
31
+ ## Setting Up GitHub Repository
32
+
33
+ ### Step 1: Create Repository on GitHub
34
+
35
+ 1. Go to https://github.com and sign in
36
+ 2. Click the "+" icon in the top right, then "New repository"
37
+ 3. Fill in:
38
+ - **Repository name**: `law-document-rag` (or your preferred name)
39
+ - **Description**: "Law Document RAG Chat Application"
40
+ - **Visibility**: Public or Private
41
+ - **DO NOT** initialize with README, .gitignore, or license (we already have these)
42
+ 4. Click "Create repository"
43
+
44
+ ### Step 2: Initialize Git Repository
45
+
46
+ Open PowerShell in your project directory and run:
47
+
48
+ ```powershell
49
+ cd "C:\Users\Dr. Mohammed Alrobia\Desktop\Python_Projects\law_project1"
50
+ git init
51
+ ```
52
+
53
+ ### Step 3: Stage and Commit Files
54
+
55
+ ```powershell
56
+ # Stage all files
57
+ git add .
58
+
59
+ # Create initial commit
60
+ git commit -m "Initial commit: Cleaned up project for HuggingFace Spaces deployment"
61
+ ```
62
+
63
+ ### Step 4: Add GitHub Remote
64
+
65
+ Replace `YOUR_USERNAME` and `YOUR_REPO_NAME` with your actual GitHub username and repository name:
66
+
67
+ ```powershell
68
+ git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO_NAME.git
69
+ ```
70
+
71
+ ### Step 5: Push to GitHub
72
+
73
+ ```powershell
74
+ # Push to main branch (or master if your default is master)
75
+ git branch -M main
76
+ git push -u origin main
77
+ ```
78
+
79
+ If you're using master branch:
80
+ ```powershell
81
+ git push -u origin master
82
+ ```
83
+
84
+ ## Future Workflow
85
+
86
+ ### Pushing Changes
87
+
88
+ After making changes to your project:
89
+
90
+ ```powershell
91
+ # Stage changes
92
+ git add .
93
+
94
+ # Commit with descriptive message
95
+ git commit -m "Description of your changes"
96
+
97
+ # Push to GitHub
98
+ git push
99
+ ```
100
+
101
+ ### Pulling Changes
102
+
103
+ To get the latest changes from GitHub:
104
+
105
+ ```powershell
106
+ git pull
107
+ ```
108
+
109
+ ### Checking Status
110
+
111
+ To see what files have changed:
112
+
113
+ ```powershell
114
+ git status
115
+ ```
116
+
117
+ ## Important Notes
118
+
119
+ - **Never commit `.env` file** - It contains your API keys and is already in `.gitignore`
120
+ - **`vectorstore/` and `processed_documents.json`** are included in the repository (as requested)
121
+ - **`uv.lock`** is included for reproducible builds
122
+ - If you get authentication errors, you may need to set up a Personal Access Token:
123
+ - Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
124
+ - Generate a new token with `repo` permissions
125
+ - Use the token as your password when pushing
126
+
127
+ ## Troubleshooting
128
+
129
+ ### Authentication Issues
130
+
131
+ If you get authentication errors, you can use a Personal Access Token:
132
+ 1. Create a token at: https://github.com/settings/tokens
133
+ 2. When prompted for password, use the token instead
134
+
135
+ ### Large Files
136
+
137
+ If you encounter issues with large files (like PDFs in documents/), you may need Git LFS:
138
+ ```powershell
139
+ git lfs install
140
+ git lfs track "*.pdf"
141
+ git add .gitattributes
142
+ git add .
143
+ git commit -m "Add Git LFS for PDF files"
144
+ ```
145
+
QUICKSTART.md ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Quick Start Guide
2
+
3
+ ## Prerequisites
4
+
5
+ - Python 3.10 or 3.11 (required for faiss-cpu compatibility)
6
+ - uv (fast Python package manager) - [Install uv](https://github.com/astral-sh/uv)
7
+ - Node.js 16+ and npm
8
+ - OpenAI API key
9
+
10
+ ## 5-Minute Setup
11
+
12
+ ### 1. Install uv (if not already installed)
13
+
14
+ **macOS/Linux:**
15
+ ```bash
16
+ curl -LsSf https://astral.sh/uv/install.sh | sh
17
+ ```
18
+
19
+ **Windows:**
20
+ ```powershell
21
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
22
+ ```
23
+
24
+ ### 2. Install Node.js (REQUIRED - if not already installed)
25
+
26
+ **Check if Node.js is installed:**
27
+ ```powershell
28
+ node --version
29
+ npm --version
30
+ ```
31
+
32
+ **If you get "node is not recognized" error:**
33
+
34
+ 1. **Download Node.js**:
35
+ - Visit: https://nodejs.org/
36
+ - Click the **green "LTS" button** (Long Term Support)
37
+ - Download and run the installer
38
+
39
+ 2. **During Installation**:
40
+ - Make sure "Add to PATH" is checked (usually automatic)
41
+ - Complete the installation
42
+
43
+ 3. **CRITICAL: Restart Your Terminal**:
44
+ - **Close PowerShell completely**
45
+ - **Open a new PowerShell window**
46
+ - This is required for PATH changes to take effect
47
+
48
+ 4. **Verify Installation**:
49
+ ```powershell
50
+ node --version
51
+ npm --version
52
+ ```
53
+ Both should show version numbers.
54
+
55
+ **For detailed Windows installation instructions, see: [INSTALL_NODEJS_WINDOWS.md](INSTALL_NODEJS_WINDOWS.md)**
56
+
57
+ ### 3. Install Dependencies
58
+
59
+ **Backend (using uv):**
60
+ ```bash
61
+ uv sync
62
+ ```
63
+
64
+ **Frontend:**
65
+ ```bash
66
+ cd frontend
67
+ npm install
68
+ cd ..
69
+ ```
70
+
71
+ ### 4. Configure API Key
72
+
73
+ Create `.env` in the project root:
74
+ ```
75
+ OPENAI_API_KEY=sk-your-actual-api-key-here
76
+ ```
77
+
78
+ ### 5. Add Documents
79
+
80
+ Place your PDF, TXT, DOCX, or DOC files in the `documents/` folder.
81
+
82
+ ### 6. Run the Application
83
+
84
+ **Terminal 1 - Backend:**
85
+ ```bash
86
+ # Using uv run (recommended)
87
+ uv run python backend/main.py
88
+
89
+ # Or activate the virtual environment
90
+ # macOS/Linux: source .venv/bin/activate && python backend/main.py
91
+ # Windows: .venv\Scripts\activate && python backend\main.py
92
+ ```
93
+
94
+ **Terminal 2 - Frontend:**
95
+ ```bash
96
+ cd frontend
97
+ npm start
98
+ ```
99
+
100
+ ### 7. Use the Application
101
+
102
+ 1. Open http://localhost:3000 in your browser
103
+ 2. Click "Index Documents" to index files in the `documents/` folder
104
+ 3. Ask questions about your documents!
105
+
106
+ ## Example Questions
107
+
108
+ - "What are the key provisions in the contract?"
109
+ - "What does the law say about [topic]?"
110
+ - "Summarize the main points of the document"
111
+
112
+ ## Troubleshooting
113
+
114
+ **"OpenAI API key is required"**
115
+ - Make sure you created `.env` in the project root with your API key
116
+
117
+ **"No documents found"**
118
+ - Check that files are in the `documents/` folder
119
+ - Supported formats: PDF, TXT, DOCX, DOC
120
+
121
+ **Frontend can't connect to backend**
122
+ - Ensure backend is running on port 8000
123
+ - Check that CORS is enabled (it is by default)
124
+
125
+ **"npm is not recognized" or "node is not recognized"**
126
+ - Node.js is not installed or not in your PATH
127
+ - Install Node.js from https://nodejs.org/
128
+ - Restart your terminal after installation
129
+ - Verify installation: `node --version` and `npm --version`
130
+
131
+ ## Next Steps
132
+
133
+ - See [README.md](README.md) for full documentation
134
+ - See [README_HF_SPACES.md](README_HF_SPACES.md) for deployment instructions
135
+
README.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Saudi Law AI Assistant
3
+ emoji: ⚖️
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # Law Document RAG Chat Application
11
+ ...
12
+
13
+ # Law Document RAG Chat Application
14
+
15
+ A web application that allows users to ask questions about indexed legal documents using Retrieval Augmented Generation (RAG) techniques.
16
+
17
+ ## Features
18
+
19
+ - 🤖 **RAG-powered Q&A**: Ask questions about your legal documents and get answers extracted directly from the context
20
+ - 📚 **Document Indexing**: Automatically index PDF, TXT, DOCX, and DOC files from a folder
21
+ - 🎨 **Modern React Frontend**: Beautiful, responsive chat interface
22
+ - ⚡ **FastAPI Backend**: High-performance API with LangChain and FAISS
23
+ - 🔍 **Exact Context Extraction**: Answers are extracted directly from documents, not generated
24
+ - 🚀 **Hugging Face Spaces Ready**: Configured for easy deployment
25
+
26
+ ## Tech Stack
27
+
28
+ - **Frontend**: React 18
29
+ - **Backend**: FastAPI
30
+ - **RAG**: LangChain + FAISS + OpenAI Embeddings
31
+ - **Vector Database**: FAISS
32
+ - **LLM**: OpenAI API (for embeddings)
33
+ - **Python**: 3.10 or 3.11 (required for faiss-cpu compatibility)
34
+
35
+ ## Project Structure
36
+
37
+ ```
38
+ law_project1/
39
+ ├── backend/
40
+ │ ├── main.py # FastAPI application
41
+ │ ├── rag_system.py # RAG implementation
42
+ │ ├── requirements.txt # Python dependencies
43
+ │ └── .env.example # Environment variables template
44
+ ├── frontend/
45
+ │ ├── src/
46
+ │ │ ├── App.js # Main React component
47
+ │ │ ├── App.css # Styles
48
+ │ │ ├── index.js # React entry point
49
+ │ │ └── index.css # Global styles
50
+ │ ├── public/
51
+ │ │ └── index.html # HTML template
52
+ │ └── package.json # Node dependencies
53
+ ├── documents/ # Place your documents here
54
+ ├── app.py # Hugging Face Spaces entry point
55
+ ├── Dockerfile # Docker configuration
56
+ ├── requirements.txt # Main Python dependencies
57
+ └── README.md # This file
58
+ ```
59
+
60
+ ## Setup Instructions
61
+
62
+ ### Local Development
63
+
64
+ 1. **Navigate to the project**:
65
+ ```bash
66
+ cd law_project1
67
+ ```
68
+
69
+ 2. **Set up the backend with uv**:
70
+ ```bash
71
+ # Install uv if you haven't already
72
+ # On macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh
73
+ # On Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
74
+
75
+ # Install dependencies
76
+ uv sync
77
+ ```
78
+
79
+ 3. **Set up environment variables**:
80
+ ```bash
81
+ # Create .env file in project root
82
+ echo "OPENAI_API_KEY=your_openai_api_key_here" > .env
83
+ ```
84
+ Or manually create `.env` file in the project root with:
85
+ ```
86
+ OPENAI_API_KEY=your_openai_api_key_here
87
+ ```
88
+
89
+ 4. **Install Node.js** (REQUIRED - if not already installed):
90
+ - **Windows**: Download from https://nodejs.org/ (click the green "LTS" button)
91
+ - Run the installer (make sure "Add to PATH" is checked)
92
+ - **CRITICAL**: Close and restart your terminal/PowerShell after installation
93
+ - Verify: `node --version` and `npm --version`
94
+ - **For detailed Windows instructions, see: [INSTALL_NODEJS_WINDOWS.md](INSTALL_NODEJS_WINDOWS.md)**
95
+
96
+ 5. **Set up the frontend**:
97
+ ```bash
98
+ cd frontend
99
+ npm install
100
+ cd ..
101
+ ```
102
+
103
+ 6. **Add documents**:
104
+ - Create a `documents` folder in the project root (if it doesn't exist)
105
+ - Add your PDF, TXT, DOCX, or DOC files
106
+
107
+ 7. **Run the backend**:
108
+ ```bash
109
+ # Using uv run (recommended)
110
+ uv run python backend/main.py
111
+
112
+ # Or activate the virtual environment first
113
+ # On macOS/Linux:
114
+ source .venv/bin/activate
115
+ python backend/main.py
116
+
117
+ # On Windows:
118
+ # .venv\Scripts\activate
119
+ # python backend\main.py
120
+ ```
121
+ The API will run on `http://localhost:8000`
122
+
123
+ 8. **Run the frontend** (in a new terminal):
124
+ ```bash
125
+ cd frontend
126
+ npm start
127
+ ```
128
+ The app will open at `http://localhost:3000`
129
+
130
+ ### Usage
131
+
132
+ 1. **Index Documents**:
133
+ - Click the "Index Documents" button in the UI, or
134
+ - Make a POST request to `http://localhost:8000/index` with:
135
+ ```json
136
+ {
137
+ "folder_path": "documents"
138
+ }
139
+ ```
140
+
141
+ 2. **Ask Questions**:
142
+ - Type your question in the chat input
143
+ - The system will retrieve relevant context and return exact text from the documents
144
+
145
+ ## Hugging Face Spaces Deployment
146
+
147
+ See [README_HF_SPACES.md](README_HF_SPACES.md) for detailed deployment instructions.
148
+
149
+ ### Quick Deployment Steps
150
+
151
+ 1. **Build the frontend**:
152
+ ```bash
153
+ cd frontend
154
+ npm install
155
+ npm run build
156
+ cd ..
157
+ ```
158
+
159
+ 2. **Create a Hugging Face Space** (Docker SDK)
160
+
161
+ 3. **Set environment variable**:
162
+ - In Space Settings → Repository secrets
163
+ - Add `OPENAI_API_KEY` with your API key
164
+
165
+ 4. **Push to Hugging Face**:
166
+ ```bash
167
+ git init
168
+ git add .
169
+ git commit -m "Initial commit"
170
+ git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
171
+ git push -u origin main
172
+ ```
173
+
174
+ ## API Endpoints
175
+
176
+ - `GET /api/` - Health check
177
+ - `GET /api/health` - Health status
178
+ - `POST /api/index` - Index documents from a folder
179
+ ```json
180
+ {
181
+ "folder_path": "documents"
182
+ }
183
+ ```
184
+ - `POST /api/ask` - Ask a question
185
+ ```json
186
+ {
187
+ "question": "What is the law about X?"
188
+ }
189
+ ```
190
+
191
+ ## Environment Variables
192
+
193
+ - `OPENAI_API_KEY`: Your OpenAI API key (required)
194
+
195
+ ## Notes
196
+
197
+ - The system extracts exact text from documents, not generated responses
198
+ - Supported document formats: PDF, TXT, DOCX, DOC
199
+ - The vectorstore is saved locally and persists between sessions
200
+ - Make sure to index documents before asking questions
201
+ - For Hugging Face Spaces, the frontend automatically uses `/api` as the API URL
202
+ - This project uses `uv` for Python package management - dependencies are defined in `pyproject.toml`
203
+ - The `.env` file should be in the project root (not in the backend folder)
204
+
205
+ ## Troubleshooting
206
+
207
+ ### Backend Issues
208
+
209
+ - **OpenAI API Key Error**: Make sure `OPENAI_API_KEY` is set in your environment or `.env` file
210
+ - **No documents found**: Ensure documents are in the `documents/` folder with supported extensions
211
+
212
+ ### Frontend Issues
213
+
214
+ - **API Connection Error**: Check that the backend is running on port 8000
215
+ - **CORS Errors**: The backend has CORS enabled for all origins in development
216
+
217
+ ### Deployment Issues
218
+
219
+ - **Build fails**: Ensure all dependencies are in `requirements.txt`
220
+ - **Frontend not loading**: Make sure `npm run build` was run successfully
221
+ - **API not working**: Verify `OPENAI_API_KEY` is set in Hugging Face Space secrets
222
+
223
+ ## License
224
+
225
+ MIT
README_HF_SPACES.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying to Hugging Face Spaces
2
+
3
+ This guide will help you deploy the Law Document RAG application to Hugging Face Spaces.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. A Hugging Face account (sign up at https://huggingface.co)
8
+ 2. An OpenAI API key
9
+ 3. Git installed on your machine
10
+
11
+ ## Step-by-Step Deployment
12
+
13
+ ### 1. Create a New Space
14
+
15
+ 1. Go to https://huggingface.co/spaces
16
+ 2. Click "Create new Space"
17
+ 3. Fill in the details:
18
+ - **Space name**: `law-document-rag` (or your preferred name)
19
+ - **SDK**: Select **Docker**
20
+ - **Visibility**: Public or Private
21
+ 4. Click "Create Space"
22
+
23
+ ### 2. Prepare Your Code
24
+
25
+ 1. **Build the React frontend**:
26
+ ```bash
27
+ cd frontend
28
+ npm install
29
+ npm run build
30
+ cd ..
31
+ ```
32
+
33
+ 2. **Ensure all files are ready**:
34
+ - `app.py` - Main entry point
35
+ - `requirements.txt` - Python dependencies
36
+ - `Dockerfile` - Docker configuration
37
+ - `backend/` - Backend code
38
+ - `frontend/build/` - Built React app
39
+
40
+ ### 3. Set Up Environment Variables
41
+
42
+ 1. In your Hugging Face Space, go to **Settings**
43
+ 2. Scroll to **Repository secrets**
44
+ 3. Add a new secret:
45
+ - **Name**: `OPENAI_API_KEY`
46
+ - **Value**: Your OpenAI API key
47
+
48
+ ### 4. Push to Hugging Face
49
+
50
+ 1. **Initialize git** (if not already done):
51
+ ```bash
52
+ git init
53
+ ```
54
+
55
+ 2. **Add Hugging Face remote**:
56
+ ```bash
57
+ git remote add origin https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
58
+ ```
59
+ Replace `YOUR_USERNAME` and `YOUR_SPACE_NAME` with your actual values.
60
+
61
+ 3. **Add and commit files**:
62
+ ```bash
63
+ git add .
64
+ git commit -m "Initial deployment"
65
+ ```
66
+
67
+ 4. **Push to Hugging Face**:
68
+ ```bash
69
+ git push origin main
70
+ ```
71
+
72
+ ### 5. Wait for Build
73
+
74
+ - Hugging Face will automatically build your Docker image
75
+ - This may take 5-10 minutes
76
+ - You can monitor the build logs in the Space's "Logs" tab
77
+
78
+ ### 6. Access Your Application
79
+
80
+ Once the build completes, your application will be available at:
81
+ ```
82
+ https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space
83
+ ```
84
+
85
+ ## Important Notes
86
+
87
+ 1. **API Endpoints**: The frontend is configured to use `/api` prefix for backend calls. This is handled by the `app.py` file.
88
+
89
+ 2. **Documents**: Users can upload documents through the UI or you can pre-populate the `documents/` folder in your repository.
90
+
91
+ 3. **Vectorstore**: The FAISS vectorstore will be created and stored in the Space's persistent storage.
92
+
93
+ 4. **Port**: Hugging Face Spaces uses port 7860 by default, which is configured in `app.py`.
94
+
95
+ ## Troubleshooting
96
+
97
+ ### Build Fails
98
+
99
+ - Check the build logs in the Space's "Logs" tab
100
+ - Ensure all dependencies are in `requirements.txt`
101
+ - Verify the Dockerfile is correct
102
+
103
+ ### API Errors
104
+
105
+ - Check that `OPENAI_API_KEY` is set correctly in Space secrets
106
+ - Verify the API key is valid and has credits
107
+
108
+ ### Frontend Not Loading
109
+
110
+ - Ensure `npm run build` was run successfully
111
+ - Check that `frontend/build/` directory exists and contains `index.html`
112
+
113
+ ## Updating Your Space
114
+
115
+ To update your deployed application:
116
+
117
+ ```bash
118
+ git add .
119
+ git commit -m "Update description"
120
+ git push origin main
121
+ ```
122
+
123
+ Hugging Face will automatically rebuild and redeploy.
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
app.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces entry point
3
+ This file serves both the FastAPI backend and React frontend
4
+ """
5
+ import os
6
+ from pathlib import Path
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi.responses import FileResponse
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from backend.main import app as backend_app
12
+
13
+ # Create a new FastAPI app that will serve both backend and frontend
14
+ app = FastAPI()
15
+
16
+ # CORS middleware
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=["*"],
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ # Mount the backend API
26
+ app.mount("/api", backend_app)
27
+
28
+ # Serve React frontend
29
+ frontend_path = Path(__file__).parent / "frontend" / "build"
30
+
31
+ if frontend_path.exists():
32
+ # Serve static files (JS, CSS, images, etc.)
33
+ static_path = frontend_path / "static"
34
+ if static_path.exists():
35
+ app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
36
+
37
+ # Serve index.html for root and all other routes (React Router)
38
+ @app.get("/")
39
+ async def serve_index():
40
+ index_file = frontend_path / "index.html"
41
+ if index_file.exists():
42
+ return FileResponse(str(index_file))
43
+ return {"message": "Frontend not built. Please run 'npm run build' in the frontend directory."}
44
+
45
+ @app.get("/{full_path:path}")
46
+ async def serve_react_app(full_path: str, request: Request):
47
+ # Don't interfere with API routes
48
+ if full_path.startswith("api"):
49
+ return
50
+ # Don't interfere with static files
51
+ if full_path.startswith("static"):
52
+ return
53
+
54
+ index_file = frontend_path / "index.html"
55
+ if index_file.exists():
56
+ return FileResponse(str(index_file))
57
+ return {"message": "Frontend not built."}
58
+ else:
59
+ @app.get("/")
60
+ async def root():
61
+ return {"message": "Frontend not built. Please run 'npm run build' in the frontend directory."}
62
+
63
+ if __name__ == "__main__":
64
+ import uvicorn
65
+ port = int(os.getenv("PORT", 7860))
66
+ uvicorn.run(app, host="0.0.0.0", port=port)
67
+
backend/chat_history.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Optional
2
+ from datetime import datetime
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ class ChatHistory:
8
+ """Manages multi-turn chat history"""
9
+
10
+ def __init__(self, max_history: int = 10):
11
+ self.max_history = max_history
12
+ self.history: List[Dict[str, str]] = []
13
+
14
+ def add_message(self, role: str, content: str):
15
+ """Add a message to chat history"""
16
+ self.history.append({
17
+ "role": role,
18
+ "content": content,
19
+ "timestamp": datetime.now().isoformat()
20
+ })
21
+
22
+ # Keep only last N messages
23
+ if len(self.history) > self.max_history * 2: # *2 because we have user + assistant pairs
24
+ self.history = self.history[-self.max_history * 2:]
25
+
26
+ def get_recent_history(self, n_turns: Optional[int] = None) -> List[Dict[str, str]]:
27
+ """Get recent chat history formatted for LLM"""
28
+ if n_turns is None:
29
+ n_turns = self.max_history
30
+
31
+ # Get last N turns (each turn = user + assistant)
32
+ recent = self.history[-n_turns * 2:] if len(self.history) > n_turns * 2 else self.history
33
+
34
+ # Format for OpenAI API (remove timestamp)
35
+ return [{"role": msg["role"], "content": msg["content"]} for msg in recent]
36
+
37
+ def get_last_turn(self) -> List[Dict[str, str]]:
38
+ """Get only the last chat turn (last user + assistant messages)"""
39
+ if len(self.history) < 2:
40
+ return []
41
+
42
+ # Get last 2 messages (user + assistant)
43
+ last_two = self.history[-2:]
44
+
45
+ # Format for OpenAI API (remove timestamp)
46
+ return [{"role": msg["role"], "content": msg["content"]} for msg in last_two]
47
+
48
+ def clear(self):
49
+ """Clear chat history"""
50
+ self.history = []
51
+
52
+
backend/document_processor.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+ from openai import OpenAI
6
+ import httpx
7
+
8
+
9
+ class NoProxyHTTPClient(httpx.Client):
10
+ def __init__(self, *args, **kwargs):
11
+ kwargs.pop("proxies", None)
12
+ super().__init__(*args, **kwargs)
13
+
14
+
15
+ class DocumentProcessor:
16
+ """Processes PDF documents using LLM to extract clean text and generate summaries"""
17
+
18
+ def __init__(self, api_key: Optional[str] = None, model: str = "gpt-5"):
19
+ api_key = api_key or os.getenv("OPENAI_API_KEY")
20
+ if not api_key:
21
+ raise ValueError("OpenAI API key is required")
22
+
23
+ os.environ.setdefault("OPENAI_API_KEY", api_key)
24
+ http_client = NoProxyHTTPClient(timeout=300.0)
25
+ self.client = OpenAI(http_client=http_client)
26
+ self.model = model
27
+
28
+
29
+ def process_pdf_with_llm(self, pdf_path: str) -> Dict[str, str]:
30
+ """
31
+ Process PDF by uploading it to OpenAI and requesting cleaned text plus a summary.
32
+
33
+ Args:
34
+ pdf_path: Path to PDF file
35
+
36
+ Returns: {"filename": str, "text": str, "summary": str}
37
+ """
38
+ filename = Path(pdf_path).name
39
+ print(f"Processing {filename} with LLM via file upload...")
40
+
41
+ uploaded_file = None
42
+
43
+ try:
44
+ # Upload file
45
+ with open(pdf_path, "rb") as pdf_file:
46
+ uploaded_file = self.client.files.create(
47
+ file=pdf_file,
48
+ purpose="user_data"
49
+ )
50
+
51
+ prompt = (
52
+ "You are processing an Arabic legal document. "
53
+ "Extract ONLY the main content text (remove headers, footers, page numbers, duplicate elements). "
54
+ "Clean the text to remove formatting artifacts. "
55
+ "Generate a concise summary in Arabic covering all important content. "
56
+ '\nReturn ONLY valid JSON with exactly these fields: {"text": "...", "summary": "..."}'
57
+ )
58
+
59
+ # Use SDK responses API
60
+ response = self.client.responses.create(
61
+ model=self.model,
62
+ input=[
63
+ {
64
+ "role": "user",
65
+ "content": [
66
+ {
67
+ "type": "input_file",
68
+ "file_id": uploaded_file.id,
69
+ },
70
+ {
71
+ "type": "input_text",
72
+ "text": prompt,
73
+ },
74
+ ],
75
+ }
76
+ ],
77
+ )
78
+
79
+ # Extract output_text from response
80
+ response_text = response.output_text
81
+ if not response_text:
82
+ raise ValueError("No text returned from OpenAI response.")
83
+
84
+ result = json.loads(response_text)
85
+ combined_text = result.get("text", "")
86
+ final_summary = result.get("summary", "")
87
+ except Exception as e:
88
+ print(f"Error processing {filename} via OpenAI: {e}")
89
+ raise
90
+ finally:
91
+ if uploaded_file:
92
+ try:
93
+ self.client.files.delete(uploaded_file.id)
94
+ except Exception as cleanup_error:
95
+ print(f"Warning: failed to delete uploaded file for {filename}: {cleanup_error}")
96
+
97
+ return {
98
+ "filename": filename,
99
+ "text": combined_text,
100
+ "summary": final_summary
101
+ }
102
+
103
+ def process_all_pdfs(self, documents_folder: str, skip_existing: bool = True) -> List[Dict[str, str]]:
104
+ """
105
+ Process all PDF files in a folder, skipping already processed documents.
106
+
107
+ Args:
108
+ documents_folder: Path to folder containing PDF files
109
+ skip_existing: If True, skip PDFs that are already in processed_documents.json
110
+
111
+ Returns: List of newly processed documents
112
+ """
113
+ folder = Path(documents_folder)
114
+ if not folder.exists():
115
+ raise ValueError(f"Folder {documents_folder} does not exist")
116
+
117
+ # Load existing processed documents
118
+ existing_docs = []
119
+ existing_filenames = set()
120
+ if skip_existing:
121
+ existing_docs = self.load_from_json()
122
+ existing_filenames = {doc.get("filename") for doc in existing_docs if doc.get("filename")}
123
+ if existing_filenames:
124
+ print(f"Found {len(existing_filenames)} already processed documents")
125
+
126
+ pdf_files = list(folder.glob("*.pdf"))
127
+ new_processed_docs = []
128
+ skipped_count = 0
129
+
130
+ for pdf_file in pdf_files:
131
+ filename = pdf_file.name
132
+
133
+ # Skip if already processed
134
+ if skip_existing and filename in existing_filenames:
135
+ print(f"⊘ Skipped (already processed): {filename}")
136
+ skipped_count += 1
137
+ continue
138
+
139
+ # Process new document
140
+ try:
141
+ result = self.process_pdf_with_llm(str(pdf_file))
142
+ new_processed_docs.append(result)
143
+ print(f"✓ Processed: {result['filename']}")
144
+ except Exception as e:
145
+ print(f"✗ Failed to process {pdf_file.name}: {e}")
146
+
147
+ # Merge with existing documents and save
148
+ if new_processed_docs:
149
+ all_docs = existing_docs + new_processed_docs
150
+ self.save_to_json(all_docs)
151
+ print(f"Processed {len(new_processed_docs)} new documents, skipped {skipped_count} existing")
152
+ elif skipped_count > 0:
153
+ print(f"All documents already processed. Skipped {skipped_count} documents.")
154
+
155
+ return new_processed_docs
156
+
157
+ def save_to_json(self, processed_docs: List[Dict[str, str]], json_path: Optional[str] = None, append: bool = False):
158
+ """
159
+ Save processed documents to JSON file.
160
+
161
+ Args:
162
+ processed_docs: List of documents to save
163
+ json_path: Optional path to JSON file
164
+ append: If True, append to existing file (avoiding duplicates). If False, overwrite.
165
+ """
166
+ if json_path is None:
167
+ project_root = Path(__file__).resolve().parents[1]
168
+ json_path = str(project_root / "processed_documents.json")
169
+ json_path = Path(json_path)
170
+
171
+ if append and json_path.exists():
172
+ # Load existing and merge, avoiding duplicates
173
+ existing_docs = self.load_from_json(json_path)
174
+ existing_filenames = {doc.get("filename") for doc in existing_docs}
175
+
176
+ # Add only new documents
177
+ for doc in processed_docs:
178
+ if doc.get("filename") not in existing_filenames:
179
+ existing_docs.append(doc)
180
+
181
+ processed_docs = existing_docs
182
+
183
+ with open(json_path, "w", encoding="utf-8") as f:
184
+ json.dump(processed_docs, f, ensure_ascii=False, indent=2)
185
+ print(f"Saved {len(processed_docs)} documents to {json_path}")
186
+
187
+ def load_from_json(self, json_path: Optional[str] = None) -> List[Dict[str, str]]:
188
+ """Load processed documents from JSON file"""
189
+ if json_path is None:
190
+ project_root = Path(__file__).resolve().parents[1]
191
+ json_path = str(project_root / "processed_documents.json")
192
+ json_path = Path(json_path)
193
+ if not json_path.exists():
194
+ return []
195
+
196
+ with open(json_path, "r", encoding="utf-8") as f:
197
+ return json.load(f)
198
+
199
+ def get_text_by_filename(self, filename: str, json_path: Optional[str] = None) -> Optional[str]:
200
+ """Get full text for a document by filename"""
201
+ docs = self.load_from_json(json_path)
202
+ for doc in docs:
203
+ if doc.get("filename") == filename:
204
+ return doc.get("text", "")
205
+ return None
backend/embeddings.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import random
4
+ from typing import List
5
+ from pathlib import Path
6
+ from dotenv import load_dotenv
7
+ import httpx
8
+ from openai import OpenAI
9
+
10
+
11
+ def _chunk_list(items: List[str], chunk_size: int) -> List[List[str]]:
12
+ return [items[i:i + chunk_size] for i in range(0, len(items), chunk_size)]
13
+
14
+
15
+ class NoProxyHTTPClient(httpx.Client):
16
+ def __init__(self, *args, **kwargs):
17
+ # Ensure any 'proxies' key is ignored to prevent incompat issues
18
+ kwargs.pop("proxies", None)
19
+ super().__init__(*args, **kwargs)
20
+
21
+
22
+ class OpenAIEmbeddingsWrapper:
23
+ """
24
+ Minimal embeddings wrapper compatible with LangChain's embeddings interface.
25
+ Uses OpenAI Embeddings API via official SDK (client.embeddings.create) with batching and retries.
26
+ Forces a custom httpx.Client to avoid unexpected 'proxies' kw errors.
27
+ """
28
+ def __init__(self, model: str = "text-embedding-ada-002", api_key: str | None = None, timeout: float = 30.0):
29
+ # Load .env from project root (one level up from backend/)
30
+ project_root = Path(__file__).resolve().parents[1]
31
+ load_dotenv(project_root / ".env")
32
+ self.model = model
33
+ self.api_key = api_key or os.getenv("OPENAI_API_KEY")
34
+ if not self.api_key:
35
+ raise ValueError("OPENAI_API_KEY is required for embeddings")
36
+ # Timeout/backoff config
37
+ self.timeout = timeout
38
+ self.batch_size = int(os.getenv("OPENAI_EMBED_BATCH_SIZE", "64"))
39
+ self.max_retries = int(os.getenv("OPENAI_EMBED_MAX_RETRIES", "6"))
40
+ self.initial_backoff = float(os.getenv("OPENAI_EMBED_INITIAL_BACKOFF", "1.0"))
41
+ self.backoff_multiplier = float(os.getenv("OPENAI_EMBED_BACKOFF_MULTIPLIER", "2.0"))
42
+ # Initialize OpenAI SDK client with custom http client; rely on env for API key/base URL
43
+ os.environ.setdefault("OPENAI_API_KEY", self.api_key)
44
+ http_client = NoProxyHTTPClient(timeout=self.timeout)
45
+ self.client = OpenAI(http_client=http_client)
46
+
47
+ def _embed_once(self, inputs: List[str]) -> List[List[float]]:
48
+ resp = self.client.embeddings.create(
49
+ model=self.model,
50
+ input=inputs,
51
+ encoding_format="float",
52
+ )
53
+ return [item.embedding for item in resp.data]
54
+
55
+ def _embed_with_retries(self, inputs: List[str]) -> List[List[float]]:
56
+ attempt = 0
57
+ backoff = self.initial_backoff
58
+ while True:
59
+ try:
60
+ return self._embed_once(inputs)
61
+ except Exception as err:
62
+ status = None
63
+ try:
64
+ status = getattr(getattr(err, "response", None), "status_code", None)
65
+ except Exception:
66
+ status = None
67
+ if (status in (429, 500, 502, 503, 504) or status is None) and attempt < self.max_retries:
68
+ retry_after = 0.0
69
+ try:
70
+ retry_after = float(getattr(getattr(err, "response", None), "headers", {}).get("Retry-After", 0))
71
+ except Exception:
72
+ retry_after = 0.0
73
+ jitter = random.uniform(0, 0.5)
74
+ sleep_s = max(retry_after, backoff) + jitter
75
+ time.sleep(sleep_s)
76
+ attempt += 1
77
+ backoff *= self.backoff_multiplier
78
+ continue
79
+ raise
80
+
81
+ def _embed(self, inputs: List[str]) -> List[List[float]]:
82
+ all_embeddings: List[List[float]] = []
83
+ for batch in _chunk_list(inputs, self.batch_size):
84
+ embeds = self._embed_with_retries(batch)
85
+ all_embeddings.extend(embeds)
86
+ time.sleep(float(os.getenv("OPENAI_EMBED_INTER_BATCH_DELAY", "0.2")))
87
+ return all_embeddings
88
+
89
+ def embed_query(self, text: str) -> List[float]:
90
+ return self._embed([text])[0]
91
+
92
+ def embed_documents(self, texts: List[str]) -> List[List[float]]:
93
+ return self._embed(texts)
94
+
95
+ #شرح نظام الأحوال الشخصية
backend/main.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Query
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import FileResponse, JSONResponse
4
+ from pydantic import BaseModel
5
+ from typing import List, Optional
6
+ import os
7
+ from pathlib import Path
8
+ from urllib.parse import quote
9
+ from dotenv import load_dotenv
10
+ from rag_system import RAGSystem
11
+
12
+ # Load environment variables from .env file in project root
13
+ env_path = Path(__file__).parent.parent / ".env"
14
+ load_dotenv(env_path)
15
+
16
+ app = FastAPI(title="Law Document RAG API")
17
+
18
+ # CORS middleware to allow React frontend to connect
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"], # In production, specify your frontend URL
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
26
+
27
+ # Initialize RAG system
28
+ rag_system = None
29
+
30
+ class QuestionRequest(BaseModel):
31
+ question: str
32
+ use_history: Optional[bool] = True
33
+
34
+ class QuestionResponse(BaseModel):
35
+ answer: str
36
+ sources: List[str]
37
+
38
+ @app.on_event("startup")
39
+ async def startup_event():
40
+ """Process and index all PDFs on startup"""
41
+ global rag_system
42
+ try:
43
+ rag_system = RAGSystem()
44
+ # Process all PDFs from documents folder on startup
45
+ docs_folder = Path("documents")
46
+ if docs_folder.exists() and any(docs_folder.glob("*.pdf")):
47
+ print("Processing PDFs on startup...")
48
+ num_docs = rag_system.process_and_index_documents(str(docs_folder))
49
+ print(f"✓ Processed and indexed {num_docs} documents")
50
+ else:
51
+ print("No PDF files found in documents folder")
52
+ except Exception as e:
53
+ print(f"Warning: Could not initialize RAG system: {e}")
54
+ import traceback
55
+ traceback.print_exc()
56
+
57
+ @app.get("/")
58
+ async def root():
59
+ return {"message": "Law Document RAG API is running"}
60
+
61
+ @app.get("/health")
62
+ async def health():
63
+ return {"status": "healthy"}
64
+
65
+ @app.post("/ask", response_model=QuestionResponse)
66
+ async def ask_question(request: QuestionRequest):
67
+ """Answer a question using RAG with multi-turn chat history"""
68
+ global rag_system
69
+ if rag_system is None:
70
+ raise HTTPException(status_code=503, detail="RAG system not initialized. Please process documents first.")
71
+
72
+ if not request.question.strip():
73
+ raise HTTPException(status_code=400, detail="Question cannot be empty")
74
+
75
+ try:
76
+ answer, sources = rag_system.answer_question(
77
+ request.question,
78
+ use_history=request.use_history,
79
+ model_provider="qwen",
80
+ )
81
+ print("[/ask] Qwen answer:", answer)
82
+ print("[/ask] Sources:", sources)
83
+ return QuestionResponse(answer=answer, sources=sources)
84
+ except Exception as e:
85
+ raise HTTPException(status_code=500, detail=f"Error answering question: {str(e)}")
86
+
87
+ @app.post("/clear-history", response_model=dict)
88
+ async def clear_history():
89
+ """Clear chat history"""
90
+ global rag_system
91
+ if rag_system is None:
92
+ raise HTTPException(status_code=503, detail="RAG system not initialized")
93
+
94
+ rag_system.clear_chat_history()
95
+ return {"message": "Chat history cleared"}
96
+
97
+ @app.get("/documents/{filename}")
98
+ async def get_document(filename: str, mode: str = Query("download", enum=["download", "preview"])):
99
+ """Serve processed document files for preview or download"""
100
+ documents_dir = Path("documents").resolve()
101
+ file_path = (documents_dir / filename).resolve()
102
+
103
+ # Prevent directory traversal
104
+ if documents_dir not in file_path.parents and file_path != documents_dir:
105
+ raise HTTPException(status_code=403, detail="Access denied")
106
+
107
+ if not file_path.exists():
108
+ raise HTTPException(status_code=404, detail="Document not found")
109
+
110
+ file_extension = file_path.suffix.lower()
111
+
112
+ def build_headers(disposition_type: str) -> dict:
113
+ try:
114
+ ascii_name = filename.encode("ascii", "ignore").decode("ascii")
115
+ except Exception:
116
+ ascii_name = ""
117
+ ascii_name = ascii_name.replace('"', '').strip() or ("document.pdf" if file_extension == ".pdf" else "document")
118
+ encoded_name = quote(filename)
119
+ return {
120
+ "Content-Disposition": f"{disposition_type}; filename=\"{ascii_name}\"; filename*=UTF-8''{encoded_name}"
121
+ }
122
+
123
+ if mode == "preview":
124
+ if file_extension != ".pdf":
125
+ return JSONResponse({"filename": filename, "error": "Preview only available for PDF files"}, status_code=400)
126
+ return FileResponse(
127
+ str(file_path),
128
+ media_type="application/pdf",
129
+ filename=filename,
130
+ headers=build_headers("inline")
131
+ )
132
+
133
+ media_type = "application/pdf" if file_extension == ".pdf" else "application/octet-stream"
134
+ return FileResponse(
135
+ str(file_path),
136
+ media_type=media_type,
137
+ filename=filename,
138
+ headers=build_headers("attachment")
139
+ )
140
+
141
+ if __name__ == "__main__":
142
+ import uvicorn
143
+ uvicorn.run(app, host="0.0.0.0", port=8000)
144
+
backend/rag_system.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Optional, Dict
5
+ from langchain_community.vectorstores import FAISS
6
+ from langchain.schema import Document
7
+ from embeddings import OpenAIEmbeddingsWrapper
8
+ from document_processor import DocumentProcessor
9
+ from chat_history import ChatHistory
10
+ from openai import OpenAI
11
+ import httpx
12
+ import tiktoken
13
+
14
+
15
+ class NoProxyHTTPClient(httpx.Client):
16
+ def __init__(self, *args, **kwargs):
17
+ kwargs.pop("proxies", None)
18
+ super().__init__(*args, **kwargs)
19
+
20
+
21
+ class RAGSystem:
22
+ """RAG system that indexes summaries and retrieves full text from JSON"""
23
+
24
+ def __init__(self, vectorstore_path: str = "vectorstore", json_path: Optional[str] = None, openai_api_key: Optional[str] = None):
25
+ self.vectorstore_path = vectorstore_path
26
+ # JSON path relative to project root
27
+ if json_path is None:
28
+ project_root = Path(__file__).resolve().parents[1]
29
+ self.json_path = str(project_root / "processed_documents.json")
30
+ else:
31
+ self.json_path = json_path
32
+ self.vectorstore = None
33
+
34
+ # Initialize embeddings
35
+ api_key = openai_api_key or os.getenv("OPENAI_API_KEY")
36
+ if not api_key:
37
+ raise ValueError("OpenAI API key is required. Set OPENAI_API_KEY environment variable.")
38
+
39
+ self.embeddings = OpenAIEmbeddingsWrapper(api_key=api_key)
40
+
41
+ # Initialize document processor
42
+ self.processor = DocumentProcessor(api_key=api_key)
43
+
44
+ # Initialize LLM client for answering questions
45
+ os.environ.setdefault("OPENAI_API_KEY", api_key)
46
+ http_client = NoProxyHTTPClient(timeout=60.0)
47
+ self.llm_client = OpenAI(http_client=http_client)
48
+ self.llm_model = os.getenv("OPENAI_LLM_MODEL", "gpt-4o-mini")
49
+
50
+ # Chat history manager
51
+ self.chat_history = ChatHistory(max_history=int(os.getenv("CHAT_HISTORY_TURNS", "10")))
52
+
53
+ # Try to load existing vectorstore
54
+ self._load_vectorstore()
55
+
56
+ def _load_vectorstore(self):
57
+ """Load existing vectorstore if it exists"""
58
+ if os.path.exists(self.vectorstore_path):
59
+ try:
60
+ self.vectorstore = FAISS.load_local(
61
+ self.vectorstore_path,
62
+ embeddings=self.embeddings,
63
+ allow_dangerous_deserialization=True
64
+ )
65
+ # Ensure embedding function is callable
66
+ self.vectorstore.embedding_function = self.embeddings.embed_query
67
+ print(f"Loaded existing vectorstore from {self.vectorstore_path}")
68
+ except Exception as e:
69
+ print(f"Could not load existing vectorstore: {e}")
70
+ self.vectorstore = None
71
+
72
+ def _count_tokens(self, messages: List[Dict[str, str]]) -> int:
73
+ """Count tokens in messages for the current model"""
74
+ try:
75
+ encoding = tiktoken.encoding_for_model(self.llm_model)
76
+ except KeyError:
77
+ # Fallback to cl100k_base for unknown models
78
+ encoding = tiktoken.get_encoding("cl100k_base")
79
+
80
+ tokens = 0
81
+ for message in messages:
82
+ tokens += 4 # Every message follows <im_start>{role/name}\n{content}<im_end>\n
83
+ for key, value in message.items():
84
+ tokens += len(encoding.encode(str(value)))
85
+ if key == "name": # If there's a name, the role is omitted
86
+ tokens -= 1 # Role is always required and always 1 token
87
+ tokens += 2 # Every reply is primed with <im_start>assistant
88
+ return tokens
89
+
90
+ def _filter_thinking_process(self, text: str) -> str:
91
+ """Filter out thinking process markers from Qwen model responses"""
92
+ if not text:
93
+ return text
94
+
95
+ # Quick path: remove <think>...</think> sections via regex
96
+ import re
97
+ filtered = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
98
+
99
+ # Remove legacy line-based markers (think>, think:)
100
+ lines = []
101
+ for line in filtered.splitlines():
102
+ strip = line.strip()
103
+ if strip.lower().startswith("think>") or strip.lower().startswith("think:"):
104
+ continue
105
+ lines.append(line)
106
+
107
+ return "\n".join(lines).strip()
108
+
109
+ def process_and_index_documents(self, documents_folder: str) -> int:
110
+ """
111
+ Process all PDFs, save to JSON, and index summaries.
112
+ Skips already processed documents and only indexes new ones.
113
+ Returns: Number of new documents processed
114
+ """
115
+ # Step 1: Process all PDFs with LLM (skips existing ones)
116
+ print("Processing PDFs with LLM...")
117
+ new_processed_docs = self.processor.process_all_pdfs(documents_folder, skip_existing=True)
118
+
119
+ # Step 2: Ensure vectorstore exists - build from existing documents if needed
120
+ if self.vectorstore is None:
121
+ # Load all existing documents to build initial vectorstore
122
+ all_docs = self.processor.load_from_json()
123
+ if all_docs:
124
+ print("Building vectorstore from existing processed documents...")
125
+ all_summary_docs = []
126
+ for doc in all_docs:
127
+ all_summary_docs.append(Document(
128
+ page_content=doc["summary"],
129
+ metadata={"filename": doc["filename"]}
130
+ ))
131
+ self.vectorstore = FAISS.from_documents(all_summary_docs, embedding=self.embeddings)
132
+ os.makedirs(self.vectorstore_path, exist_ok=True)
133
+ self.vectorstore.save_local(self.vectorstore_path)
134
+ print(f"Built vectorstore from {len(all_docs)} existing documents")
135
+ else:
136
+ print("Warning: No processed documents found in JSON file.")
137
+
138
+ # Step 3: Index summaries for new documents only
139
+ if new_processed_docs:
140
+ print("Indexing summaries for new documents...")
141
+ summary_docs = []
142
+ for doc in new_processed_docs:
143
+ summary_docs.append(Document(
144
+ page_content=doc["summary"],
145
+ metadata={"filename": doc["filename"]}
146
+ ))
147
+
148
+ # Update vectorstore with new documents
149
+ if self.vectorstore is None:
150
+ print("Creating new vectorstore from new documents...")
151
+ self.vectorstore = FAISS.from_documents(summary_docs, embedding=self.embeddings)
152
+ else:
153
+ print("Updating existing vectorstore with new documents...")
154
+ self.vectorstore.add_documents(summary_docs)
155
+
156
+ # Save vectorstore
157
+ os.makedirs(self.vectorstore_path, exist_ok=True)
158
+ self.vectorstore.save_local(self.vectorstore_path)
159
+
160
+ print(f"Indexed {len(new_processed_docs)} new document summaries")
161
+ else:
162
+ if self.vectorstore is not None:
163
+ print("No new documents to process. Using existing vectorstore.")
164
+ else:
165
+ print("Warning: No documents found to process or index. Vectorstore not available.")
166
+
167
+ return len(new_processed_docs)
168
+
169
+ def answer_question(self, question: str, use_history: bool = True, model_provider: str = "openai") -> Tuple[str, List[str]]:
170
+ """
171
+ Answer a question using RAG with multi-turn chat history
172
+
173
+ Args:
174
+ question: The user's question
175
+ use_history: Whether to use chat history
176
+ model_provider: Model provider to use - "openai" (default) or "qwen"/"huggingface" for Qwen model
177
+
178
+ Returns:
179
+ Tuple of (answer, list of source filenames)
180
+ """
181
+ if self.vectorstore is None:
182
+ raise ValueError("No documents indexed. Please process documents first.")
183
+
184
+ # Ensure embedding function is callable
185
+ if getattr(self.vectorstore, "embedding_function", None) is None or not callable(self.vectorstore.embedding_function):
186
+ self.vectorstore.embedding_function = self.embeddings.embed_query
187
+
188
+ # Step 1: Find most similar summary
189
+ # Build search query with last chat turn context if history is enabled
190
+ search_query = question
191
+ if use_history:
192
+ last_turn = self.chat_history.get_last_turn()
193
+ if last_turn:
194
+ # Format last turn as text
195
+ last_turn_text = ""
196
+ for msg in last_turn:
197
+ if msg["role"] == "user":
198
+ last_turn_text += f"Previous Q: {msg['content']}\n"
199
+ elif msg["role"] == "assistant":
200
+ last_turn_text += f"Previous A: {msg['content']}\n"
201
+
202
+ # Combine with current question
203
+ search_query = f"{last_turn_text}\nCurrent Q: {question}"
204
+
205
+ similar_docs = self.vectorstore.similarity_search(search_query, k=1)
206
+
207
+ if not similar_docs:
208
+ return "I couldn't find any relevant information to answer your question.", []
209
+
210
+ # Step 2: Get filename from matched summary
211
+ matched_filename = similar_docs[0].metadata.get("filename", "")
212
+
213
+
214
+ if not matched_filename:
215
+ return "Error: No filename found in matched document metadata.", []
216
+
217
+ # Step 3: Retrieve full text from JSON
218
+ print(f"DEBUG: Searching for filename: '{matched_filename}'")
219
+ print(f"DEBUG: JSON path: {self.json_path}")
220
+
221
+ full_text = self.processor.get_text_by_filename(matched_filename, json_path=self.json_path)
222
+
223
+ if not full_text:
224
+ # Debug: Check what's in the JSON file
225
+ json_path = Path(self.json_path)
226
+ if not json_path.exists():
227
+ error_msg = f"Error: JSON file not found at {self.json_path}. Please process documents first."
228
+ print(f"DEBUG: {error_msg}")
229
+ return error_msg, [matched_filename]
230
+
231
+ try:
232
+ with open(json_path, "r", encoding="utf-8") as f:
233
+ docs = json.load(f)
234
+ available_filenames = [doc.get("filename", "unknown") for doc in docs] if isinstance(docs, list) else []
235
+ print(f"DEBUG: JSON file exists with {len(available_filenames) if isinstance(docs, list) else 0} documents")
236
+ print(f"DEBUG: Available filenames: {available_filenames}")
237
+
238
+ error_msg = f"Could not retrieve text for document: '{matched_filename}'. "
239
+ if available_filenames:
240
+ error_msg += f"Available filenames in JSON: {', '.join(available_filenames)}"
241
+ else:
242
+ error_msg += "JSON file is empty or invalid."
243
+ return error_msg, [matched_filename]
244
+ except Exception as e:
245
+ error_msg = f"Error loading JSON file: {str(e)}"
246
+ print(f"DEBUG: {error_msg}")
247
+ return error_msg, [matched_filename]
248
+
249
+ # Step 4: Build prompt with full text, question, and chat history
250
+ history_messages = []
251
+ if use_history:
252
+ # Get last 3 messages (get 2 turns = 4 messages, then take last 3)
253
+ history_messages = self.chat_history.get_recent_history(n_turns=2)
254
+
255
+ system_prompt = f"""You are a helpful legal document assistant. Answer questions based on the provided document text.
256
+
257
+ MODE 1 - General Questions:
258
+ - Understand the context and provide a clear, helpful answer
259
+ - You may paraphrase or summarize information from the document
260
+ - Explain concepts in your own words while staying true to the document's meaning
261
+ - Whenever you refere to the document, refer to it by the filename WITHOUT the extension such as ".pdf" or".doc": {matched_filename}
262
+
263
+ MODE 2 - Legal Articles/Terms (المادة):
264
+ - When the user asks about specific legal articles (المادة), legal terms, or exact regulations, you MUST quote the EXACT text from the document verbatim
265
+ - Copy the complete text word-for-word, including all numbers, punctuation, and formatting
266
+ - Do NOT paraphrase, summarize, or generate new text for legal articles
267
+ - NEVER create or generate legal text - only use what exists in the document
268
+
269
+ If the answer is not in the document, say so clearly. MUST Answer in Arabic."""
270
+
271
+ # Check if question contains legal article/term keywords
272
+ is_legal_term_question = any(keyword in question for keyword in ["المادة", "مادة", "article", "نص","النص", "نص القانون"])
273
+
274
+ legal_instruction = ""
275
+ if is_legal_term_question:
276
+ legal_instruction = "\n\nCRITICAL: The user is asking about a legal article or legal term. You MUST quote the EXACT text from the document verbatim. Copy the complete text word-for-word, including all numbers and punctuation. Do NOT paraphrase or generate any text."
277
+ else:
278
+ legal_instruction = "\n\nAnswer the question by understanding the context from the document. You may paraphrase or explain in your own words while staying true to the document's meaning."
279
+
280
+ user_prompt = f"""Document Text:
281
+ {full_text[:8000]} # Limit to avoid token limits
282
+
283
+ User Question: {question}
284
+ {legal_instruction}
285
+
286
+ Please answer the question based on the document text above. MUST Answer the Question in Arabic"""
287
+
288
+ messages = [
289
+ {"role": "system", "content": system_prompt}
290
+ ]
291
+
292
+ # Add chat history (excluding the last user message if it's the current question)
293
+ if history_messages:
294
+ # Add history but skip if last message is the same question
295
+ for msg in history_messages[:-1] if len(history_messages) > 0 and history_messages[-1].get("content") == question else history_messages:
296
+ messages.append(msg)
297
+
298
+ messages.append({"role": "user", "content": user_prompt})
299
+
300
+ # Step 5: Get answer from LLM
301
+ try:
302
+ # Initialize client based on model_provider
303
+ if model_provider.lower() in ["qwen", "huggingface"]:
304
+ # Use HuggingFace router with Qwen model
305
+ hf_token = os.getenv("HF_TOKEN")
306
+ if not hf_token:
307
+ raise ValueError("HF_TOKEN environment variable is required for Qwen model testing.")
308
+
309
+ http_client = NoProxyHTTPClient(timeout=60.0)
310
+ llm_client = OpenAI(
311
+ base_url="https://router.huggingface.co/v1",
312
+ api_key=hf_token,
313
+ http_client=http_client
314
+ )
315
+ llm_model = os.getenv("QWEN_MODEL", "Qwen/Qwen3-32B:groq")
316
+ else:
317
+ # Use OpenAI (default) openai
318
+ llm_client = self.llm_client
319
+ llm_model = self.llm_model
320
+
321
+ response = llm_client.chat.completions.create(
322
+ model=llm_model,
323
+ messages=messages,
324
+ temperature=0.3
325
+ )
326
+
327
+ answer = response.choices[0].message.content
328
+
329
+ # Filter thinking process from Qwen responses
330
+ if model_provider.lower() in ["qwen", "huggingface"]:
331
+ answer = self._filter_thinking_process(answer)
332
+
333
+ # Step 6: Update chat history
334
+ self.chat_history.add_message("user", question)
335
+ self.chat_history.add_message("assistant", answer)
336
+
337
+ return answer, [matched_filename]
338
+ except Exception as e:
339
+ error_msg = f"Error generating answer: {str(e)}"
340
+ self.chat_history.add_message("user", question)
341
+ self.chat_history.add_message("assistant", error_msg)
342
+ return error_msg, [matched_filename]
343
+
344
+ def clear_chat_history(self):
345
+ """Clear chat history"""
346
+ self.chat_history.clear()
frontend/build/asset-manifest.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": {
3
+ "main.css": "/static/css/main.3a0c885e.css",
4
+ "main.js": "/static/js/main.882def61.js",
5
+ "index.html": "/index.html",
6
+ "main.3a0c885e.css.map": "/static/css/main.3a0c885e.css.map",
7
+ "main.882def61.js.map": "/static/js/main.882def61.js.map"
8
+ },
9
+ "entrypoints": [
10
+ "static/css/main.3a0c885e.css",
11
+ "static/js/main.882def61.js"
12
+ ]
13
+ }
frontend/build/index.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Law Document RAG Chat Application"/><title>Law Document Assistant</title><script defer="defer" src="/static/js/main.882def61.js"></script><link href="/static/css/main.3a0c885e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
frontend/build/static/css/main.3a0c885e.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *{box-sizing:border-box;padding:0}*,body{margin:0}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:linear-gradient(135deg,#667eea,#764ba2);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;min-height:100vh}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{align-items:center;direction:rtl;display:flex;font-family:Segoe UI,Arial,Tahoma,Cairo,Noto Sans Arabic,sans-serif;justify-content:center;min-height:100vh;padding:20px}.chat-container{background:#fff;border-radius:20px;box-shadow:0 20px 60px #0000004d;display:flex;flex-direction:column;height:90vh;max-width:900px;overflow:hidden;width:100%}.chat-header{background:linear-gradient(135deg,#667eea,#764ba2);box-shadow:0 2px 10px #0000001a;color:#fff;padding:25px;text-align:center}.chat-header h1{font-size:28px;font-weight:600;margin:0 0 10px}.chat-header p{font-size:14px;margin:0 0 15px;opacity:.9}.messages-container{background:#f8f9fa;flex:1 1;overflow-y:auto;padding:20px}.welcome-message{color:#666;padding:60px 20px;text-align:center}.welcome-message h2{color:#333;font-size:24px;margin-bottom:15px}.welcome-message p{font-size:16px;margin:10px 0}.welcome-message .hint{color:#999;font-size:14px;font-style:italic}.message{animation:fadeIn .3s ease;display:flex;margin-bottom:20px}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.message.assistant,.message.user{justify-content:flex-start}.message-content{border-radius:18px;box-shadow:0 2px 8px #0000001a;max-width:70%;padding:15px 20px}.message.user .message-content{background:linear-gradient(135deg,#667eea,#764ba2);border-bottom-left-radius:4px;color:#fff}.message.assistant .message-content{background:#fff;border-bottom-right-radius:4px;color:#333}.message-header{font-size:12px;font-weight:600;margin-bottom:8px;opacity:.8}.message-text{word-wrap:break-word;direction:rtl;font-size:15px;line-height:1.8;text-align:right}.message-text.error{color:#d32f2f}.message-text p{margin:0 0 10px}.message-text strong{font-weight:700}.message-text ol,.message-text ul{margin:10px 0;padding-right:20px}.message-text li{margin-bottom:6px}.typing-indicator{display:inline-block}.typing-indicator:after{animation:dots 1.5s steps(4) infinite;content:"..."}@keyframes dots{0%,20%{content:"."}40%{content:".."}60%,to{content:"..."}}.sources{border-top:1px solid #0000001a;font-size:12px;margin-top:12px;padding-top:12px}.message.user .sources{border-top-color:#ffffff4d}.sources strong{display:block;margin-bottom:6px;opacity:.8}.sources ul{list-style:none;margin:0;padding:0}.sources li{display:flex;flex-direction:column;gap:6px;opacity:.9;padding:4px 0}.source-name{color:#333;font-weight:600}.source-actions{display:flex;gap:10px}.source-link{background:none;border:none;color:#4c6ef5;cursor:pointer;direction:rtl;font-size:14px;font-weight:600;padding:0;text-align:right;text-decoration:underline}.source-link:hover{color:#2a48c5}.source-link.download{color:#2f9e44}.source-link.download:hover{color:#1b6d2f}.preview-panel{background:#fdfdfd;border:1px solid #e0e0e0;border-radius:12px;direction:rtl;margin:20px;max-height:300px;overflow:auto;padding:15px}.preview-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:10px}.preview-header h3{margin:0}.preview-filename{color:#555;font-size:14px}.close-preview{background:none;border:none;color:#ff4d4f;cursor:pointer;font-size:22px;font-weight:700}.close-preview:hover{color:#d9363e}.preview-content pre{background:#f8f9fa;border-radius:8px;direction:rtl;font-size:14px;margin:0;padding:10px;text-align:right;white-space:pre-wrap}.preview-content{align-items:center;display:flex;justify-content:center;min-height:200px}.pdf-frame{background:#fff;border:none;border-radius:8px;box-shadow:inset 0 0 10px #0000000d;height:400px;width:100%}.input-container{background:#fff;border-top:1px solid #e0e0e0;display:flex;gap:10px;padding:20px}.message-input{border:2px solid #e0e0e0;border-radius:25px;direction:rtl;flex:1 1;font-size:15px;outline:none;padding:15px 20px;text-align:right;transition:border-color .3s ease}.message-input:focus{border-color:#667eea}.message-input:disabled{background:#f5f5f5;cursor:not-allowed}.send-button{background:linear-gradient(135deg,#667eea,#764ba2);border:none;border-radius:25px;color:#fff;cursor:pointer;font-size:15px;font-weight:600;padding:15px 30px;transition:transform .2s ease,box-shadow .2s ease}.send-button:hover:not(:disabled){box-shadow:0 4px 12px #667eea66;transform:translateY(-2px)}.send-button:disabled{cursor:not-allowed;opacity:.6;transform:none}.messages-container::-webkit-scrollbar{width:8px}.messages-container::-webkit-scrollbar-track{background:#f1f1f1;border-radius:10px}.messages-container::-webkit-scrollbar-thumb{background:#888;border-radius:10px}.messages-container::-webkit-scrollbar-thumb:hover{background:#555}@media (max-width:768px){.chat-container{border-radius:0;height:100vh}.message-content{max-width:85%}.chat-header h1{font-size:24px}}
2
+ /*# sourceMappingURL=main.3a0c885e.css.map*/
frontend/build/static/css/main.3a0c885e.css.map ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version":3,"file":"static/css/main.3a0c885e.css","mappings":"AAAA,EAGE,qBAAsB,CADtB,SAEF,CAEA,OALE,QAcF,CATA,KAKE,kCAAmC,CACnC,iCAAkC,CAClC,kDAA6D,CAL7D,mIAEY,CAIZ,gBACF,CAEA,KACE,uEAEF,CCpBA,KAGE,kBAAmB,CAGnB,aAAc,CALd,YAAa,CAMb,mEAAmF,CALnF,sBAAuB,CAEvB,gBAAiB,CACjB,YAGF,CAEA,gBAIE,eAAiB,CACjB,kBAAmB,CACnB,gCAA0C,CAC1C,YAAa,CACb,qBAAsB,CALtB,WAAY,CADZ,eAAgB,CAOhB,eAAgB,CARhB,UASF,CAEA,aACE,kDAA6D,CAI7D,+BAAyC,CAHzC,UAAY,CACZ,YAAa,CACb,iBAEF,CAEA,gBAEE,cAAe,CACf,eAAgB,CAFhB,eAGF,CAEA,eAGE,cAAe,CAFf,eAAkB,CAClB,UAEF,CAEA,oBAIE,kBAAmB,CAHnB,QAAO,CACP,eAAgB,CAChB,YAEF,CAEA,iBAGE,UAAW,CADX,iBAAkB,CADlB,iBAGF,CAEA,oBACE,UAAW,CAEX,cAAe,CADf,kBAEF,CAEA,mBAEE,cAAe,CADf,aAEF,CAEA,uBAEE,UAAW,CADX,cAAe,CAEf,iBACF,CAEA,SAGE,yBAA2B,CAD3B,YAAa,CADb,kBAGF,CAEA,kBACE,GACE,SAAU,CACV,0BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAMA,iCACE,0BACF,CAEA,iBAGE,kBAAmB,CACnB,8BAAwC,CAHxC,aAAc,CACd,iBAGF,CAEA,+BACE,kDAA6D,CAE7D,6BAA8B,CAD9B,UAEF,CAEA,oCACE,eAAiB,CAEjB,8BAA+B,CAD/B,UAEF,CAEA,gBACE,cAAe,CACf,eAAgB,CAChB,iBAAkB,CAClB,UACF,CAEA,cAGE,oBAAqB,CAErB,aAAc,CAJd,cAAe,CACf,eAAgB,CAEhB,gBAEF,CAEA,oBACE,aACF,CAEA,gBACE,eACF,CAEA,qBACE,eACF,CAEA,kCAGE,aAAc,CADd,kBAEF,CAEA,iBACE,iBACF,CAEA,kBACE,oBACF,CAEA,wBAEE,qCAA2C,CAD3C,aAEF,CAEA,gBACE,OACE,WACF,CACA,IACE,YACF,CACA,OACE,aACF,CACF,CAEA,SAGE,8BAAwC,CACxC,cAAe,CAHf,eAAgB,CAChB,gBAGF,CAEA,uBACE,0BACF,CAEA,gBACE,aAAc,CACd,iBAAkB,CAClB,UACF,CAEA,YACE,eAAgB,CAEhB,QAAS,CADT,SAEF,CAEA,YAGE,YAAa,CACb,qBAAsB,CACtB,OAAQ,CAHR,UAAY,CADZ,aAKF,CAEA,aAEE,UAAW,CADX,eAEF,CAEA,gBACE,YAAa,CACb,QACF,CAEA,aACE,eAAgB,CAChB,WAAY,CACZ,aAAc,CACd,cAAe,CAKf,aAAc,CAHd,cAAe,CACf,eAAgB,CAChB,SAAU,CAEV,gBAAiB,CALjB,yBAMF,CAEA,mBACE,aACF,CAEA,sBACE,aACF,CAEA,4BACE,aACF,CAEA,eAKE,kBAAmB,CAFnB,wBAAyB,CACzB,kBAAmB,CAInB,aAAc,CAPd,WAAY,CAKZ,gBAAiB,CACjB,aAAc,CALd,YAOF,CAEA,gBAGE,kBAAmB,CAFnB,YAAa,CACb,6BAA8B,CAE9B,kBACF,CAEA,mBACE,QACF,CAEA,kBAEE,UAAW,CADX,cAEF,CAEA,eACE,eAAgB,CAChB,WAAY,CAGZ,aAAc,CADd,cAAe,CADf,cAAe,CAGf,eACF,CAEA,qBACE,aACF,CAEA,qBAME,kBAAmB,CAEnB,iBAAkB,CANlB,aAAc,CAGd,cAAe,CADf,QAAS,CAGT,YAAa,CAJb,gBAAiB,CAFjB,oBAQF,CAEA,iBAIE,kBAAmB,CAFnB,YAAa,CACb,sBAAuB,CAFvB,gBAIF,CAEA,WAKE,eAAgB,CAFhB,WAAY,CACZ,iBAAkB,CAElB,mCAA8C,CAJ9C,YAAa,CADb,UAMF,CAEA,iBAGE,eAAiB,CACjB,4BAA6B,CAH7B,YAAa,CAIb,QAAS,CAHT,YAIF,CAEA,eAGE,wBAAyB,CACzB,kBAAmB,CAKnB,aAAc,CARd,QAAO,CAIP,cAAe,CACf,YAAa,CAJb,iBAAkB,CAMlB,gBAAiB,CADjB,gCAGF,CAEA,qBACE,oBACF,CAEA,wBACE,kBAAmB,CACnB,kBACF,CAEA,aAEE,kDAA6D,CAE7D,WAAY,CACZ,kBAAmB,CAFnB,UAAY,CAKZ,cAAe,CAFf,cAAe,CACf,eAAgB,CANhB,iBAAkB,CAQlB,iDACF,CAEA,kCAEE,+BAA+C,CAD/C,0BAEF,CAEA,sBAEE,kBAAmB,CADnB,UAAY,CAEZ,cACF,CAGA,uCACE,SACF,CAEA,6CACE,kBAAmB,CACnB,kBACF,CAEA,6CACE,eAAgB,CAChB,kBACF,CAEA,mDACE,eACF,CAGA,yBACE,gBAEE,eAAgB,CADhB,YAEF,CAEA,iBACE,aACF,CAEA,gBACE,cACF,CACF","sources":["index.css","App.css"],"sourcesContent":["* {\r\n margin: 0;\r\n padding: 0;\r\n box-sizing: border-box;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\r\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\r\n sans-serif;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\r\n min-height: 100vh;\r\n}\r\n\r\ncode {\r\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\r\n monospace;\r\n}\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n",".App {\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n padding: 20px;\n direction: rtl;\n font-family: 'Segoe UI', 'Arial', 'Tahoma', 'Cairo', 'Noto Sans Arabic', sans-serif;\n}\n\n.chat-container {\n width: 100%;\n max-width: 900px;\n height: 90vh;\n background: white;\n border-radius: 20px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.chat-header {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n padding: 25px;\n text-align: center;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.chat-header h1 {\n margin: 0 0 10px 0;\n font-size: 28px;\n font-weight: 600;\n}\n\n.chat-header p {\n margin: 0 0 15px 0;\n opacity: 0.9;\n font-size: 14px;\n}\n\n.messages-container {\n flex: 1;\n overflow-y: auto;\n padding: 20px;\n background: #f8f9fa;\n}\n\n.welcome-message {\n text-align: center;\n padding: 60px 20px;\n color: #666;\n}\n\n.welcome-message h2 {\n color: #333;\n margin-bottom: 15px;\n font-size: 24px;\n}\n\n.welcome-message p {\n margin: 10px 0;\n font-size: 16px;\n}\n\n.welcome-message .hint {\n font-size: 14px;\n color: #999;\n font-style: italic;\n}\n\n.message {\n margin-bottom: 20px;\n display: flex;\n animation: fadeIn 0.3s ease;\n}\n\n@keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.message.user {\n justify-content: flex-start;\n}\n\n.message.assistant {\n justify-content: flex-start;\n}\n\n.message-content {\n max-width: 70%;\n padding: 15px 20px;\n border-radius: 18px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n\n.message.user .message-content {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n border-bottom-left-radius: 4px;\n}\n\n.message.assistant .message-content {\n background: white;\n color: #333;\n border-bottom-right-radius: 4px;\n}\n\n.message-header {\n font-size: 12px;\n font-weight: 600;\n margin-bottom: 8px;\n opacity: 0.8;\n}\n\n.message-text {\n font-size: 15px;\n line-height: 1.8;\n word-wrap: break-word;\n text-align: right;\n direction: rtl;\n}\n\n.message-text.error {\n color: #d32f2f;\n}\n\n.message-text p {\n margin: 0 0 10px 0;\n}\n\n.message-text strong {\n font-weight: 700;\n}\n\n.message-text ul,\n.message-text ol {\n padding-right: 20px;\n margin: 10px 0;\n}\n\n.message-text li {\n margin-bottom: 6px;\n}\n\n.typing-indicator {\n display: inline-block;\n}\n\n.typing-indicator::after {\n content: '...';\n animation: dots 1.5s steps(4, end) infinite;\n}\n\n@keyframes dots {\n 0%, 20% {\n content: '.';\n }\n 40% {\n content: '..';\n }\n 60%, 100% {\n content: '...';\n }\n}\n\n.sources {\n margin-top: 12px;\n padding-top: 12px;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n font-size: 12px;\n}\n\n.message.user .sources {\n border-top-color: rgba(255, 255, 255, 0.3);\n}\n\n.sources strong {\n display: block;\n margin-bottom: 6px;\n opacity: 0.8;\n}\n\n.sources ul {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.sources li {\n padding: 4px 0;\n opacity: 0.9;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.source-name {\n font-weight: 600;\n color: #333;\n}\n\n.source-actions {\n display: flex;\n gap: 10px;\n}\n\n.source-link {\n background: none;\n border: none;\n color: #4c6ef5;\n cursor: pointer;\n text-decoration: underline;\n font-size: 14px;\n font-weight: 600;\n padding: 0;\n direction: rtl;\n text-align: right;\n}\n\n.source-link:hover {\n color: #2a48c5;\n}\n\n.source-link.download {\n color: #2f9e44;\n}\n\n.source-link.download:hover {\n color: #1b6d2f;\n}\n\n.preview-panel {\n margin: 20px;\n padding: 15px;\n border: 1px solid #e0e0e0;\n border-radius: 12px;\n background: #fdfdfd;\n max-height: 300px;\n overflow: auto;\n direction: rtl;\n}\n\n.preview-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 10px;\n}\n\n.preview-header h3 {\n margin: 0;\n}\n\n.preview-filename {\n font-size: 14px;\n color: #555;\n}\n\n.close-preview {\n background: none;\n border: none;\n font-size: 22px;\n cursor: pointer;\n color: #ff4d4f;\n font-weight: bold;\n}\n\n.close-preview:hover {\n color: #d9363e;\n}\n\n.preview-content pre {\n white-space: pre-wrap;\n direction: rtl;\n text-align: right;\n margin: 0;\n font-size: 14px;\n background: #f8f9fa;\n padding: 10px;\n border-radius: 8px;\n}\n\n.preview-content {\n min-height: 200px;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.pdf-frame {\n width: 100%;\n height: 400px;\n border: none;\n border-radius: 8px;\n background: #fff;\n box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);\n}\n\n.input-container {\n display: flex;\n padding: 20px;\n background: white;\n border-top: 1px solid #e0e0e0;\n gap: 10px;\n}\n\n.message-input {\n flex: 1;\n padding: 15px 20px;\n border: 2px solid #e0e0e0;\n border-radius: 25px;\n font-size: 15px;\n outline: none;\n transition: border-color 0.3s ease;\n text-align: right;\n direction: rtl;\n}\n\n.message-input:focus {\n border-color: #667eea;\n}\n\n.message-input:disabled {\n background: #f5f5f5;\n cursor: not-allowed;\n}\n\n.send-button {\n padding: 15px 30px;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n border: none;\n border-radius: 25px;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n.send-button:hover:not(:disabled) {\n transform: translateY(-2px);\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);\n}\n\n.send-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n transform: none;\n}\n\n/* Scrollbar styling */\n.messages-container::-webkit-scrollbar {\n width: 8px;\n}\n\n.messages-container::-webkit-scrollbar-track {\n background: #f1f1f1;\n border-radius: 10px;\n}\n\n.messages-container::-webkit-scrollbar-thumb {\n background: #888;\n border-radius: 10px;\n}\n\n.messages-container::-webkit-scrollbar-thumb:hover {\n background: #555;\n}\n\n/* Responsive design */\n@media (max-width: 768px) {\n .chat-container {\n height: 100vh;\n border-radius: 0;\n }\n\n .message-content {\n max-width: 85%;\n }\n\n .chat-header h1 {\n font-size: 24px;\n }\n}\n\n\n"],"names":[],"sourceRoot":""}
frontend/build/static/js/main.882def61.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/build/static/js/main.882def61.js.LICENSE.txt ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license React
3
+ * react-dom.production.min.js
4
+ *
5
+ * Copyright (c) Facebook, Inc. and its affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
10
+
11
+ /**
12
+ * @license React
13
+ * react-jsx-runtime.production.min.js
14
+ *
15
+ * Copyright (c) Facebook, Inc. and its affiliates.
16
+ *
17
+ * This source code is licensed under the MIT license found in the
18
+ * LICENSE file in the root directory of this source tree.
19
+ */
20
+
21
+ /**
22
+ * @license React
23
+ * react.production.min.js
24
+ *
25
+ * Copyright (c) Facebook, Inc. and its affiliates.
26
+ *
27
+ * This source code is licensed under the MIT license found in the
28
+ * LICENSE file in the root directory of this source tree.
29
+ */
30
+
31
+ /**
32
+ * @license React
33
+ * scheduler.production.min.js
34
+ *
35
+ * Copyright (c) Facebook, Inc. and its affiliates.
36
+ *
37
+ * This source code is licensed under the MIT license found in the
38
+ * LICENSE file in the root directory of this source tree.
39
+ */
frontend/build/static/js/main.882def61.js.map ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "law-rag-frontend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "react": "^18.2.0",
7
+ "react-dom": "^18.2.0",
8
+ "react-scripts": "5.0.1",
9
+ "axios": "^1.6.2",
10
+ "react-markdown": "^9.0.1",
11
+ "remark-gfm": "^4.0.0"
12
+ },
13
+ "scripts": {
14
+ "start": "react-scripts start",
15
+ "build": "react-scripts build",
16
+ "test": "react-scripts test",
17
+ "eject": "react-scripts eject"
18
+ },
19
+ "eslintConfig": {
20
+ "extends": [
21
+ "react-app"
22
+ ]
23
+ },
24
+ "browserslist": {
25
+ "production": [
26
+ ">0.2%",
27
+ "not dead",
28
+ "not op_mini all"
29
+ ],
30
+ "development": [
31
+ "last 1 chrome version",
32
+ "last 1 firefox version",
33
+ "last 1 safari version"
34
+ ]
35
+ }
36
+ }
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
frontend/public/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#000000" />
7
+ <meta
8
+ name="description"
9
+ content="Law Document RAG Chat Application"
10
+ />
11
+ <title>Law Document Assistant</title>
12
+ </head>
13
+ <body>
14
+ <noscript>You need to enable JavaScript to run this app.</noscript>
15
+ <div id="root"></div>
16
+ </body>
17
+ </html>
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
frontend/src/App.css ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .App {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ min-height: 100vh;
6
+ padding: 20px;
7
+ direction: rtl;
8
+ font-family: 'Segoe UI', 'Arial', 'Tahoma', 'Cairo', 'Noto Sans Arabic', sans-serif;
9
+ }
10
+
11
+ .chat-container {
12
+ width: 100%;
13
+ max-width: 900px;
14
+ height: 90vh;
15
+ background: white;
16
+ border-radius: 20px;
17
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
18
+ display: flex;
19
+ flex-direction: column;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .chat-header {
24
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25
+ color: white;
26
+ padding: 25px;
27
+ text-align: center;
28
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
29
+ }
30
+
31
+ .chat-header h1 {
32
+ margin: 0 0 10px 0;
33
+ font-size: 28px;
34
+ font-weight: 600;
35
+ }
36
+
37
+ .chat-header p {
38
+ margin: 0 0 15px 0;
39
+ opacity: 0.9;
40
+ font-size: 14px;
41
+ }
42
+
43
+ .messages-container {
44
+ flex: 1;
45
+ overflow-y: auto;
46
+ padding: 20px;
47
+ background: #f8f9fa;
48
+ }
49
+
50
+ .welcome-message {
51
+ text-align: center;
52
+ padding: 60px 20px;
53
+ color: #666;
54
+ }
55
+
56
+ .welcome-message h2 {
57
+ color: #333;
58
+ margin-bottom: 15px;
59
+ font-size: 24px;
60
+ }
61
+
62
+ .welcome-message p {
63
+ margin: 10px 0;
64
+ font-size: 16px;
65
+ }
66
+
67
+ .welcome-message .hint {
68
+ font-size: 14px;
69
+ color: #999;
70
+ font-style: italic;
71
+ }
72
+
73
+ .message {
74
+ margin-bottom: 20px;
75
+ display: flex;
76
+ animation: fadeIn 0.3s ease;
77
+ }
78
+
79
+ @keyframes fadeIn {
80
+ from {
81
+ opacity: 0;
82
+ transform: translateY(10px);
83
+ }
84
+ to {
85
+ opacity: 1;
86
+ transform: translateY(0);
87
+ }
88
+ }
89
+
90
+ .message.user {
91
+ justify-content: flex-start;
92
+ }
93
+
94
+ .message.assistant {
95
+ justify-content: flex-start;
96
+ }
97
+
98
+ .message-content {
99
+ max-width: 70%;
100
+ padding: 15px 20px;
101
+ border-radius: 18px;
102
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
103
+ }
104
+
105
+ .message.user .message-content {
106
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
107
+ color: white;
108
+ border-bottom-left-radius: 4px;
109
+ }
110
+
111
+ .message.assistant .message-content {
112
+ background: white;
113
+ color: #333;
114
+ border-bottom-right-radius: 4px;
115
+ }
116
+
117
+ .message-header {
118
+ font-size: 12px;
119
+ font-weight: 600;
120
+ margin-bottom: 8px;
121
+ opacity: 0.8;
122
+ }
123
+
124
+ .message-text {
125
+ font-size: 15px;
126
+ line-height: 1.8;
127
+ word-wrap: break-word;
128
+ text-align: right;
129
+ direction: rtl;
130
+ }
131
+
132
+ .message-text.error {
133
+ color: #d32f2f;
134
+ }
135
+
136
+ .message-text p {
137
+ margin: 0 0 10px 0;
138
+ }
139
+
140
+ .message-text strong {
141
+ font-weight: 700;
142
+ }
143
+
144
+ .message-text ul,
145
+ .message-text ol {
146
+ padding-right: 20px;
147
+ margin: 10px 0;
148
+ }
149
+
150
+ .message-text li {
151
+ margin-bottom: 6px;
152
+ }
153
+
154
+ .typing-indicator {
155
+ display: inline-block;
156
+ }
157
+
158
+ .typing-indicator::after {
159
+ content: '...';
160
+ animation: dots 1.5s steps(4, end) infinite;
161
+ }
162
+
163
+ @keyframes dots {
164
+ 0%, 20% {
165
+ content: '.';
166
+ }
167
+ 40% {
168
+ content: '..';
169
+ }
170
+ 60%, 100% {
171
+ content: '...';
172
+ }
173
+ }
174
+
175
+ .sources {
176
+ margin-top: 12px;
177
+ padding-top: 12px;
178
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
179
+ font-size: 12px;
180
+ }
181
+
182
+ .message.user .sources {
183
+ border-top-color: rgba(255, 255, 255, 0.3);
184
+ }
185
+
186
+ .sources strong {
187
+ display: block;
188
+ margin-bottom: 6px;
189
+ opacity: 0.8;
190
+ }
191
+
192
+ .sources ul {
193
+ list-style: none;
194
+ padding: 0;
195
+ margin: 0;
196
+ }
197
+
198
+ .sources li {
199
+ padding: 4px 0;
200
+ opacity: 0.9;
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 6px;
204
+ }
205
+
206
+ .source-name {
207
+ font-weight: 600;
208
+ color: #333;
209
+ }
210
+
211
+ .source-actions {
212
+ display: flex;
213
+ gap: 10px;
214
+ }
215
+
216
+ .source-link {
217
+ background: none;
218
+ border: none;
219
+ color: #4c6ef5;
220
+ cursor: pointer;
221
+ text-decoration: underline;
222
+ font-size: 14px;
223
+ font-weight: 600;
224
+ padding: 0;
225
+ direction: rtl;
226
+ text-align: right;
227
+ }
228
+
229
+ .source-link:hover {
230
+ color: #2a48c5;
231
+ }
232
+
233
+ .source-link.download {
234
+ color: #2f9e44;
235
+ }
236
+
237
+ .source-link.download:hover {
238
+ color: #1b6d2f;
239
+ }
240
+
241
+ .preview-panel {
242
+ margin: 20px;
243
+ padding: 15px;
244
+ border: 1px solid #e0e0e0;
245
+ border-radius: 12px;
246
+ background: #fdfdfd;
247
+ max-height: 300px;
248
+ overflow: auto;
249
+ direction: rtl;
250
+ }
251
+
252
+ .preview-header {
253
+ display: flex;
254
+ justify-content: space-between;
255
+ align-items: center;
256
+ margin-bottom: 10px;
257
+ }
258
+
259
+ .preview-header h3 {
260
+ margin: 0;
261
+ }
262
+
263
+ .preview-filename {
264
+ font-size: 14px;
265
+ color: #555;
266
+ }
267
+
268
+ .close-preview {
269
+ background: none;
270
+ border: none;
271
+ font-size: 22px;
272
+ cursor: pointer;
273
+ color: #ff4d4f;
274
+ font-weight: bold;
275
+ }
276
+
277
+ .close-preview:hover {
278
+ color: #d9363e;
279
+ }
280
+
281
+ .preview-content pre {
282
+ white-space: pre-wrap;
283
+ direction: rtl;
284
+ text-align: right;
285
+ margin: 0;
286
+ font-size: 14px;
287
+ background: #f8f9fa;
288
+ padding: 10px;
289
+ border-radius: 8px;
290
+ }
291
+
292
+ .preview-content {
293
+ min-height: 200px;
294
+ display: flex;
295
+ justify-content: center;
296
+ align-items: center;
297
+ }
298
+
299
+ .pdf-frame {
300
+ width: 100%;
301
+ height: 400px;
302
+ border: none;
303
+ border-radius: 8px;
304
+ background: #fff;
305
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
306
+ }
307
+
308
+ .input-container {
309
+ display: flex;
310
+ padding: 20px;
311
+ background: white;
312
+ border-top: 1px solid #e0e0e0;
313
+ gap: 10px;
314
+ }
315
+
316
+ .message-input {
317
+ flex: 1;
318
+ padding: 15px 20px;
319
+ border: 2px solid #e0e0e0;
320
+ border-radius: 25px;
321
+ font-size: 15px;
322
+ outline: none;
323
+ transition: border-color 0.3s ease;
324
+ text-align: right;
325
+ direction: rtl;
326
+ }
327
+
328
+ .message-input:focus {
329
+ border-color: #667eea;
330
+ }
331
+
332
+ .message-input:disabled {
333
+ background: #f5f5f5;
334
+ cursor: not-allowed;
335
+ }
336
+
337
+ .send-button {
338
+ padding: 15px 30px;
339
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
340
+ color: white;
341
+ border: none;
342
+ border-radius: 25px;
343
+ font-size: 15px;
344
+ font-weight: 600;
345
+ cursor: pointer;
346
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
347
+ }
348
+
349
+ .send-button:hover:not(:disabled) {
350
+ transform: translateY(-2px);
351
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
352
+ }
353
+
354
+ .send-button:disabled {
355
+ opacity: 0.6;
356
+ cursor: not-allowed;
357
+ transform: none;
358
+ }
359
+
360
+ /* Scrollbar styling */
361
+ .messages-container::-webkit-scrollbar {
362
+ width: 8px;
363
+ }
364
+
365
+ .messages-container::-webkit-scrollbar-track {
366
+ background: #f1f1f1;
367
+ border-radius: 10px;
368
+ }
369
+
370
+ .messages-container::-webkit-scrollbar-thumb {
371
+ background: #888;
372
+ border-radius: 10px;
373
+ }
374
+
375
+ .messages-container::-webkit-scrollbar-thumb:hover {
376
+ background: #555;
377
+ }
378
+
379
+ /* Responsive design */
380
+ @media (max-width: 768px) {
381
+ .chat-container {
382
+ height: 100vh;
383
+ border-radius: 0;
384
+ }
385
+
386
+ .message-content {
387
+ max-width: 85%;
388
+ }
389
+
390
+ .chat-header h1 {
391
+ font-size: 24px;
392
+ }
393
+ }
394
+
395
+
frontend/src/App.js ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import './App.css';
3
+ import axios from 'axios';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+
7
+ // Use relative URL for production (Hugging Face Spaces), absolute for local dev
8
+ const API_URL = process.env.REACT_APP_API_URL ||
9
+ (process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:8000');
10
+ const DOCS_URL = process.env.REACT_APP_DOCS_URL || `${API_URL}/documents`;
11
+
12
+ function App() {
13
+ const [messages, setMessages] = useState([]);
14
+ const [input, setInput] = useState('');
15
+ const [loading, setLoading] = useState(false);
16
+ const [previewUrl, setPreviewUrl] = useState(null);
17
+ const [previewFilename, setPreviewFilename] = useState('');
18
+ const [previewError, setPreviewError] = useState(null);
19
+ const [previewLoading, setPreviewLoading] = useState(false);
20
+ const messagesEndRef = useRef(null);
21
+ const previewUrlRef = useRef(null);
22
+
23
+ const scrollToBottom = () => {
24
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
25
+ };
26
+
27
+ useEffect(() => {
28
+ scrollToBottom();
29
+ }, [messages]);
30
+
31
+ useEffect(() => {
32
+ return () => {
33
+ if (previewUrlRef.current) {
34
+ URL.revokeObjectURL(previewUrlRef.current);
35
+ }
36
+ };
37
+ }, []);
38
+
39
+ const handleSend = async (e) => {
40
+ e.preventDefault();
41
+ if (!input.trim() || loading) return;
42
+
43
+ const userMessage = { role: 'user', content: input };
44
+ setMessages(prev => [...prev, userMessage]);
45
+ setInput('');
46
+ setLoading(true);
47
+
48
+ try {
49
+ const response = await axios.post(`${API_URL}/ask`, {
50
+ question: input
51
+ });
52
+
53
+ const assistantMessage = {
54
+ role: 'assistant',
55
+ content: response.data.answer,
56
+ sources: response.data.sources
57
+ };
58
+ setMessages(prev => [...prev, assistantMessage]);
59
+ } catch (error) {
60
+ const errorMessage = {
61
+ role: 'assistant',
62
+ content: error.response?.data?.detail || 'عذراً، حدث خطأ. يرجى المحاولة مرة أخرى.',
63
+ error: true
64
+ };
65
+ setMessages(prev => [...prev, errorMessage]);
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+
71
+ const handleClearHistory = async () => {
72
+ if (window.confirm('هل أنت متأكد من أنك تريد مسح سجل المحادثة؟')) {
73
+ try {
74
+ await axios.post(`${API_URL}/clear-history`);
75
+ setMessages([]); // Clear local messages state
76
+ } catch (error) {
77
+ console.error('Error clearing history:', error);
78
+ alert('فشل مسح السجل. يرجى المحاولة مرة أخرى.');
79
+ }
80
+ }
81
+ };
82
+
83
+ const handleDownload = (source) => {
84
+ if (!source) return;
85
+ const filename = source.split('/').pop() || source;
86
+ const url = `${DOCS_URL}/${encodeURIComponent(filename)}?mode=download`;
87
+ window.open(url, '_blank');
88
+ };
89
+
90
+ const handleClosePreview = () => {
91
+ if (previewUrlRef.current) {
92
+ URL.revokeObjectURL(previewUrlRef.current);
93
+ previewUrlRef.current = null;
94
+ }
95
+ setPreviewUrl(null);
96
+ setPreviewError(null);
97
+ setPreviewFilename('');
98
+ setPreviewLoading(false);
99
+ };
100
+
101
+ const handleSourceClick = async (source) => {
102
+ if (!source) return;
103
+ const filename = source.split('/').pop() || source;
104
+ const extension = filename.split('.').pop()?.toLowerCase();
105
+ setPreviewFilename(filename);
106
+ setPreviewError(null);
107
+ setPreviewLoading(true);
108
+ if (previewUrlRef.current) {
109
+ URL.revokeObjectURL(previewUrlRef.current);
110
+ previewUrlRef.current = null;
111
+ }
112
+ setPreviewUrl(null);
113
+ if (extension !== 'pdf') {
114
+ setPreviewError('المعاينة متاحة فقط لملفات PDF.');
115
+ setPreviewLoading(false);
116
+ return;
117
+ }
118
+ try {
119
+ const url = `${DOCS_URL}/${encodeURIComponent(filename)}?mode=preview`;
120
+ const response = await axios.get(url, { responseType: 'blob' });
121
+ const blob = new Blob([response.data], { type: 'application/pdf' });
122
+ const objectUrl = URL.createObjectURL(blob);
123
+ previewUrlRef.current = objectUrl;
124
+ setPreviewUrl(objectUrl);
125
+ } catch (error) {
126
+ setPreviewError(error.response?.data?.detail || 'تعذر تحميل المعاينة.');
127
+ } finally {
128
+ setPreviewLoading(false);
129
+ }
130
+ };
131
+
132
+ return (
133
+ <div className="App" dir="rtl">
134
+ <div className="chat-container">
135
+ <div className="chat-header">
136
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
137
+ <div>
138
+ <h1>⚖️ المساعد القانوني الذكي لنظام الأحوال الشخصية</h1>
139
+ {/* <p>اطرح أسئلة حول وثائقك القانونية</p> */}
140
+ </div>
141
+ {messages.length > 0 && (
142
+ <button
143
+ onClick={handleClearHistory}
144
+ className="clear-history-button"
145
+ title="مسح سجل المحادثة"
146
+ style={{
147
+ background: 'rgba(255, 255, 255, 0.2)',
148
+ border: '1px solid rgba(255, 255, 255, 0.3)',
149
+ cursor: 'pointer',
150
+ fontSize: '14px',
151
+ padding: '8px 16px',
152
+ borderRadius: '20px',
153
+ color: 'white',
154
+ display: 'flex',
155
+ alignItems: 'center',
156
+ gap: '6px',
157
+ transition: 'background-color 0.2s',
158
+ fontWeight: '500'
159
+ }}
160
+ onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.3)'}
161
+ onMouseLeave={(e) => e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'}
162
+ >
163
+ <span>🗑️</span>
164
+ <span>مسح المحادثة</span>
165
+ </button>
166
+ )}
167
+ </div>
168
+ </div>
169
+
170
+ <div className="messages-container">
171
+ {messages.length === 0 && (
172
+ <div className="welcome-message">
173
+ <h2>مرحباً!</h2>
174
+ <p>ابدأ بطرح سؤالا قانونيا...</p>
175
+ </div>
176
+ )}
177
+
178
+ {messages.map((msg, idx) => {
179
+ const isAssistant = msg.role === 'assistant';
180
+ const contentIsString = typeof msg.content === 'string';
181
+ const renderContent = () => {
182
+ if (isAssistant && contentIsString) {
183
+ return (
184
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
185
+ {msg.content}
186
+ </ReactMarkdown>
187
+ );
188
+ }
189
+ if (contentIsString) {
190
+ return msg.content;
191
+ }
192
+ try {
193
+ return JSON.stringify(msg.content, null, 2);
194
+ } catch (err) {
195
+ return 'محتوى غير قابل للعرض';
196
+ }
197
+ };
198
+ return (
199
+ <div key={idx} className={`message ${msg.role}`}>
200
+ <div className="message-content">
201
+ <div className="message-header">
202
+ {msg.role === 'user' ? '👤 أنت' : '🤖 المساعد'}
203
+ </div>
204
+ <div className={`message-text ${msg.error ? 'error' : ''}`}>
205
+ {renderContent()}
206
+ </div>
207
+ {msg.sources && msg.sources.length > 0 && (
208
+ <div className="sources">
209
+ <strong>المصادر:</strong>
210
+ <ul>
211
+ {msg.sources.map((source, i) => (
212
+ <li key={i}>
213
+ <span className="source-name">{source.split('/').pop()}</span>
214
+ <div className="source-actions">
215
+ <button
216
+ type="button"
217
+ className="source-link"
218
+ onClick={() => handleSourceClick(source)}
219
+ >
220
+ معاينة
221
+ </button>
222
+ <button
223
+ type="button"
224
+ className="source-link download"
225
+ onClick={() => handleDownload(source)}
226
+ >
227
+ تحميل
228
+ </button>
229
+ </div>
230
+ </li>
231
+ ))}
232
+ </ul>
233
+ </div>
234
+ )}
235
+ </div>
236
+ </div>
237
+ );
238
+ })}
239
+
240
+ {loading && (
241
+ <div className="message assistant">
242
+ <div className="message-content">
243
+ <div className="message-header">🤖 المساعد القانوني</div>
244
+ <div className="message-text">
245
+ <span className="typing-indicator">جاري التفكير...</span>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ )}
250
+
251
+ <div ref={messagesEndRef} />
252
+ </div>
253
+
254
+ <form className="input-container" onSubmit={handleSend}>
255
+ <input
256
+ type="text"
257
+ value={input}
258
+ onChange={(e) => setInput(e.target.value)}
259
+ placeholder="اطرح سؤالاً قانونيا ..."
260
+ disabled={loading}
261
+ className="message-input"
262
+ />
263
+ <button
264
+ type="submit"
265
+ disabled={loading || !input.trim()}
266
+ className="send-button"
267
+ >
268
+ إرسال
269
+ </button>
270
+ </form>
271
+
272
+ {(previewUrl || previewError || previewLoading) && (
273
+ <div className="preview-panel">
274
+ <div className="preview-header">
275
+ <h3>معاينة المصدر</h3>
276
+ {previewFilename && <span className="preview-filename">{previewFilename}</span>}
277
+ <button className="close-preview" onClick={handleClosePreview}>
278
+
279
+ </button>
280
+ </div>
281
+ <div className="preview-content">
282
+ {previewLoading ? (
283
+ <p>جاري تحميل المعاينة...</p>
284
+ ) : previewError ? (
285
+ <p className="error">{previewError}</p>
286
+ ) : (
287
+ <iframe
288
+ src={previewUrl}
289
+ title={`معاينة ${previewFilename}`}
290
+ className="pdf-frame"
291
+ />
292
+ )}
293
+ </div>
294
+ </div>
295
+ )}
296
+ </div>
297
+ </div>
298
+ );
299
+ }
300
+
301
+ export default App;
302
+
frontend/src/index.css ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ margin: 0;
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
10
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
11
+ sans-serif;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ min-height: 100vh;
16
+ }
17
+
18
+ code {
19
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
20
+ monospace;
21
+ }
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
frontend/src/index.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App';
5
+
6
+ const root = ReactDOM.createRoot(document.getElementById('root'));
7
+ root.render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>
11
+ );
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
package-lock.json ADDED
@@ -0,0 +1,1535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "law_project1",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "dependencies": {
8
+ "react-markdown": "^10.1.0",
9
+ "remark-gfm": "^4.0.1"
10
+ }
11
+ },
12
+ "node_modules/@types/debug": {
13
+ "version": "4.1.12",
14
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
15
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "@types/ms": "*"
19
+ }
20
+ },
21
+ "node_modules/@types/estree": {
22
+ "version": "1.0.8",
23
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
24
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
25
+ "license": "MIT"
26
+ },
27
+ "node_modules/@types/estree-jsx": {
28
+ "version": "1.0.5",
29
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
30
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@types/estree": "*"
34
+ }
35
+ },
36
+ "node_modules/@types/hast": {
37
+ "version": "3.0.4",
38
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
39
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
40
+ "license": "MIT",
41
+ "dependencies": {
42
+ "@types/unist": "*"
43
+ }
44
+ },
45
+ "node_modules/@types/mdast": {
46
+ "version": "4.0.4",
47
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
48
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
49
+ "license": "MIT",
50
+ "dependencies": {
51
+ "@types/unist": "*"
52
+ }
53
+ },
54
+ "node_modules/@types/ms": {
55
+ "version": "2.1.0",
56
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
57
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
58
+ "license": "MIT"
59
+ },
60
+ "node_modules/@types/react": {
61
+ "version": "19.2.5",
62
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
63
+ "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
64
+ "license": "MIT",
65
+ "peer": true,
66
+ "dependencies": {
67
+ "csstype": "^3.0.2"
68
+ }
69
+ },
70
+ "node_modules/@types/unist": {
71
+ "version": "3.0.3",
72
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
73
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
74
+ "license": "MIT"
75
+ },
76
+ "node_modules/@ungap/structured-clone": {
77
+ "version": "1.3.0",
78
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
79
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
80
+ "license": "ISC"
81
+ },
82
+ "node_modules/bail": {
83
+ "version": "2.0.2",
84
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
85
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
86
+ "license": "MIT",
87
+ "funding": {
88
+ "type": "github",
89
+ "url": "https://github.com/sponsors/wooorm"
90
+ }
91
+ },
92
+ "node_modules/ccount": {
93
+ "version": "2.0.1",
94
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
95
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
96
+ "license": "MIT",
97
+ "funding": {
98
+ "type": "github",
99
+ "url": "https://github.com/sponsors/wooorm"
100
+ }
101
+ },
102
+ "node_modules/character-entities": {
103
+ "version": "2.0.2",
104
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
105
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
106
+ "license": "MIT",
107
+ "funding": {
108
+ "type": "github",
109
+ "url": "https://github.com/sponsors/wooorm"
110
+ }
111
+ },
112
+ "node_modules/character-entities-html4": {
113
+ "version": "2.1.0",
114
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
115
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
116
+ "license": "MIT",
117
+ "funding": {
118
+ "type": "github",
119
+ "url": "https://github.com/sponsors/wooorm"
120
+ }
121
+ },
122
+ "node_modules/character-entities-legacy": {
123
+ "version": "3.0.0",
124
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
125
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
126
+ "license": "MIT",
127
+ "funding": {
128
+ "type": "github",
129
+ "url": "https://github.com/sponsors/wooorm"
130
+ }
131
+ },
132
+ "node_modules/character-reference-invalid": {
133
+ "version": "2.0.1",
134
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
135
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
136
+ "license": "MIT",
137
+ "funding": {
138
+ "type": "github",
139
+ "url": "https://github.com/sponsors/wooorm"
140
+ }
141
+ },
142
+ "node_modules/comma-separated-tokens": {
143
+ "version": "2.0.3",
144
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
145
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
146
+ "license": "MIT",
147
+ "funding": {
148
+ "type": "github",
149
+ "url": "https://github.com/sponsors/wooorm"
150
+ }
151
+ },
152
+ "node_modules/csstype": {
153
+ "version": "3.2.2",
154
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz",
155
+ "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==",
156
+ "license": "MIT"
157
+ },
158
+ "node_modules/debug": {
159
+ "version": "4.4.3",
160
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
161
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
162
+ "license": "MIT",
163
+ "dependencies": {
164
+ "ms": "^2.1.3"
165
+ },
166
+ "engines": {
167
+ "node": ">=6.0"
168
+ },
169
+ "peerDependenciesMeta": {
170
+ "supports-color": {
171
+ "optional": true
172
+ }
173
+ }
174
+ },
175
+ "node_modules/decode-named-character-reference": {
176
+ "version": "1.2.0",
177
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
178
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
179
+ "license": "MIT",
180
+ "dependencies": {
181
+ "character-entities": "^2.0.0"
182
+ },
183
+ "funding": {
184
+ "type": "github",
185
+ "url": "https://github.com/sponsors/wooorm"
186
+ }
187
+ },
188
+ "node_modules/dequal": {
189
+ "version": "2.0.3",
190
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
191
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
192
+ "license": "MIT",
193
+ "engines": {
194
+ "node": ">=6"
195
+ }
196
+ },
197
+ "node_modules/devlop": {
198
+ "version": "1.1.0",
199
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
200
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
201
+ "license": "MIT",
202
+ "dependencies": {
203
+ "dequal": "^2.0.0"
204
+ },
205
+ "funding": {
206
+ "type": "github",
207
+ "url": "https://github.com/sponsors/wooorm"
208
+ }
209
+ },
210
+ "node_modules/escape-string-regexp": {
211
+ "version": "5.0.0",
212
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
213
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
214
+ "license": "MIT",
215
+ "engines": {
216
+ "node": ">=12"
217
+ },
218
+ "funding": {
219
+ "url": "https://github.com/sponsors/sindresorhus"
220
+ }
221
+ },
222
+ "node_modules/estree-util-is-identifier-name": {
223
+ "version": "3.0.0",
224
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
225
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
226
+ "license": "MIT",
227
+ "funding": {
228
+ "type": "opencollective",
229
+ "url": "https://opencollective.com/unified"
230
+ }
231
+ },
232
+ "node_modules/extend": {
233
+ "version": "3.0.2",
234
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
235
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
236
+ "license": "MIT"
237
+ },
238
+ "node_modules/hast-util-to-jsx-runtime": {
239
+ "version": "2.3.6",
240
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
241
+ "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
242
+ "license": "MIT",
243
+ "dependencies": {
244
+ "@types/estree": "^1.0.0",
245
+ "@types/hast": "^3.0.0",
246
+ "@types/unist": "^3.0.0",
247
+ "comma-separated-tokens": "^2.0.0",
248
+ "devlop": "^1.0.0",
249
+ "estree-util-is-identifier-name": "^3.0.0",
250
+ "hast-util-whitespace": "^3.0.0",
251
+ "mdast-util-mdx-expression": "^2.0.0",
252
+ "mdast-util-mdx-jsx": "^3.0.0",
253
+ "mdast-util-mdxjs-esm": "^2.0.0",
254
+ "property-information": "^7.0.0",
255
+ "space-separated-tokens": "^2.0.0",
256
+ "style-to-js": "^1.0.0",
257
+ "unist-util-position": "^5.0.0",
258
+ "vfile-message": "^4.0.0"
259
+ },
260
+ "funding": {
261
+ "type": "opencollective",
262
+ "url": "https://opencollective.com/unified"
263
+ }
264
+ },
265
+ "node_modules/hast-util-whitespace": {
266
+ "version": "3.0.0",
267
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
268
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
269
+ "license": "MIT",
270
+ "dependencies": {
271
+ "@types/hast": "^3.0.0"
272
+ },
273
+ "funding": {
274
+ "type": "opencollective",
275
+ "url": "https://opencollective.com/unified"
276
+ }
277
+ },
278
+ "node_modules/html-url-attributes": {
279
+ "version": "3.0.1",
280
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
281
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
282
+ "license": "MIT",
283
+ "funding": {
284
+ "type": "opencollective",
285
+ "url": "https://opencollective.com/unified"
286
+ }
287
+ },
288
+ "node_modules/inline-style-parser": {
289
+ "version": "0.2.7",
290
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
291
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
292
+ "license": "MIT"
293
+ },
294
+ "node_modules/is-alphabetical": {
295
+ "version": "2.0.1",
296
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
297
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
298
+ "license": "MIT",
299
+ "funding": {
300
+ "type": "github",
301
+ "url": "https://github.com/sponsors/wooorm"
302
+ }
303
+ },
304
+ "node_modules/is-alphanumerical": {
305
+ "version": "2.0.1",
306
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
307
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
308
+ "license": "MIT",
309
+ "dependencies": {
310
+ "is-alphabetical": "^2.0.0",
311
+ "is-decimal": "^2.0.0"
312
+ },
313
+ "funding": {
314
+ "type": "github",
315
+ "url": "https://github.com/sponsors/wooorm"
316
+ }
317
+ },
318
+ "node_modules/is-decimal": {
319
+ "version": "2.0.1",
320
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
321
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
322
+ "license": "MIT",
323
+ "funding": {
324
+ "type": "github",
325
+ "url": "https://github.com/sponsors/wooorm"
326
+ }
327
+ },
328
+ "node_modules/is-hexadecimal": {
329
+ "version": "2.0.1",
330
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
331
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
332
+ "license": "MIT",
333
+ "funding": {
334
+ "type": "github",
335
+ "url": "https://github.com/sponsors/wooorm"
336
+ }
337
+ },
338
+ "node_modules/is-plain-obj": {
339
+ "version": "4.1.0",
340
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
341
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
342
+ "license": "MIT",
343
+ "engines": {
344
+ "node": ">=12"
345
+ },
346
+ "funding": {
347
+ "url": "https://github.com/sponsors/sindresorhus"
348
+ }
349
+ },
350
+ "node_modules/longest-streak": {
351
+ "version": "3.1.0",
352
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
353
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
354
+ "license": "MIT",
355
+ "funding": {
356
+ "type": "github",
357
+ "url": "https://github.com/sponsors/wooorm"
358
+ }
359
+ },
360
+ "node_modules/markdown-table": {
361
+ "version": "3.0.4",
362
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
363
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
364
+ "license": "MIT",
365
+ "funding": {
366
+ "type": "github",
367
+ "url": "https://github.com/sponsors/wooorm"
368
+ }
369
+ },
370
+ "node_modules/mdast-util-find-and-replace": {
371
+ "version": "3.0.2",
372
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
373
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
374
+ "license": "MIT",
375
+ "dependencies": {
376
+ "@types/mdast": "^4.0.0",
377
+ "escape-string-regexp": "^5.0.0",
378
+ "unist-util-is": "^6.0.0",
379
+ "unist-util-visit-parents": "^6.0.0"
380
+ },
381
+ "funding": {
382
+ "type": "opencollective",
383
+ "url": "https://opencollective.com/unified"
384
+ }
385
+ },
386
+ "node_modules/mdast-util-from-markdown": {
387
+ "version": "2.0.2",
388
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
389
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
390
+ "license": "MIT",
391
+ "dependencies": {
392
+ "@types/mdast": "^4.0.0",
393
+ "@types/unist": "^3.0.0",
394
+ "decode-named-character-reference": "^1.0.0",
395
+ "devlop": "^1.0.0",
396
+ "mdast-util-to-string": "^4.0.0",
397
+ "micromark": "^4.0.0",
398
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
399
+ "micromark-util-decode-string": "^2.0.0",
400
+ "micromark-util-normalize-identifier": "^2.0.0",
401
+ "micromark-util-symbol": "^2.0.0",
402
+ "micromark-util-types": "^2.0.0",
403
+ "unist-util-stringify-position": "^4.0.0"
404
+ },
405
+ "funding": {
406
+ "type": "opencollective",
407
+ "url": "https://opencollective.com/unified"
408
+ }
409
+ },
410
+ "node_modules/mdast-util-gfm": {
411
+ "version": "3.1.0",
412
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
413
+ "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
414
+ "license": "MIT",
415
+ "dependencies": {
416
+ "mdast-util-from-markdown": "^2.0.0",
417
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
418
+ "mdast-util-gfm-footnote": "^2.0.0",
419
+ "mdast-util-gfm-strikethrough": "^2.0.0",
420
+ "mdast-util-gfm-table": "^2.0.0",
421
+ "mdast-util-gfm-task-list-item": "^2.0.0",
422
+ "mdast-util-to-markdown": "^2.0.0"
423
+ },
424
+ "funding": {
425
+ "type": "opencollective",
426
+ "url": "https://opencollective.com/unified"
427
+ }
428
+ },
429
+ "node_modules/mdast-util-gfm-autolink-literal": {
430
+ "version": "2.0.1",
431
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
432
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
433
+ "license": "MIT",
434
+ "dependencies": {
435
+ "@types/mdast": "^4.0.0",
436
+ "ccount": "^2.0.0",
437
+ "devlop": "^1.0.0",
438
+ "mdast-util-find-and-replace": "^3.0.0",
439
+ "micromark-util-character": "^2.0.0"
440
+ },
441
+ "funding": {
442
+ "type": "opencollective",
443
+ "url": "https://opencollective.com/unified"
444
+ }
445
+ },
446
+ "node_modules/mdast-util-gfm-footnote": {
447
+ "version": "2.1.0",
448
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
449
+ "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
450
+ "license": "MIT",
451
+ "dependencies": {
452
+ "@types/mdast": "^4.0.0",
453
+ "devlop": "^1.1.0",
454
+ "mdast-util-from-markdown": "^2.0.0",
455
+ "mdast-util-to-markdown": "^2.0.0",
456
+ "micromark-util-normalize-identifier": "^2.0.0"
457
+ },
458
+ "funding": {
459
+ "type": "opencollective",
460
+ "url": "https://opencollective.com/unified"
461
+ }
462
+ },
463
+ "node_modules/mdast-util-gfm-strikethrough": {
464
+ "version": "2.0.0",
465
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
466
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "@types/mdast": "^4.0.0",
470
+ "mdast-util-from-markdown": "^2.0.0",
471
+ "mdast-util-to-markdown": "^2.0.0"
472
+ },
473
+ "funding": {
474
+ "type": "opencollective",
475
+ "url": "https://opencollective.com/unified"
476
+ }
477
+ },
478
+ "node_modules/mdast-util-gfm-table": {
479
+ "version": "2.0.0",
480
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
481
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
482
+ "license": "MIT",
483
+ "dependencies": {
484
+ "@types/mdast": "^4.0.0",
485
+ "devlop": "^1.0.0",
486
+ "markdown-table": "^3.0.0",
487
+ "mdast-util-from-markdown": "^2.0.0",
488
+ "mdast-util-to-markdown": "^2.0.0"
489
+ },
490
+ "funding": {
491
+ "type": "opencollective",
492
+ "url": "https://opencollective.com/unified"
493
+ }
494
+ },
495
+ "node_modules/mdast-util-gfm-task-list-item": {
496
+ "version": "2.0.0",
497
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
498
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
499
+ "license": "MIT",
500
+ "dependencies": {
501
+ "@types/mdast": "^4.0.0",
502
+ "devlop": "^1.0.0",
503
+ "mdast-util-from-markdown": "^2.0.0",
504
+ "mdast-util-to-markdown": "^2.0.0"
505
+ },
506
+ "funding": {
507
+ "type": "opencollective",
508
+ "url": "https://opencollective.com/unified"
509
+ }
510
+ },
511
+ "node_modules/mdast-util-mdx-expression": {
512
+ "version": "2.0.1",
513
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
514
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
515
+ "license": "MIT",
516
+ "dependencies": {
517
+ "@types/estree-jsx": "^1.0.0",
518
+ "@types/hast": "^3.0.0",
519
+ "@types/mdast": "^4.0.0",
520
+ "devlop": "^1.0.0",
521
+ "mdast-util-from-markdown": "^2.0.0",
522
+ "mdast-util-to-markdown": "^2.0.0"
523
+ },
524
+ "funding": {
525
+ "type": "opencollective",
526
+ "url": "https://opencollective.com/unified"
527
+ }
528
+ },
529
+ "node_modules/mdast-util-mdx-jsx": {
530
+ "version": "3.2.0",
531
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
532
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
533
+ "license": "MIT",
534
+ "dependencies": {
535
+ "@types/estree-jsx": "^1.0.0",
536
+ "@types/hast": "^3.0.0",
537
+ "@types/mdast": "^4.0.0",
538
+ "@types/unist": "^3.0.0",
539
+ "ccount": "^2.0.0",
540
+ "devlop": "^1.1.0",
541
+ "mdast-util-from-markdown": "^2.0.0",
542
+ "mdast-util-to-markdown": "^2.0.0",
543
+ "parse-entities": "^4.0.0",
544
+ "stringify-entities": "^4.0.0",
545
+ "unist-util-stringify-position": "^4.0.0",
546
+ "vfile-message": "^4.0.0"
547
+ },
548
+ "funding": {
549
+ "type": "opencollective",
550
+ "url": "https://opencollective.com/unified"
551
+ }
552
+ },
553
+ "node_modules/mdast-util-mdxjs-esm": {
554
+ "version": "2.0.1",
555
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
556
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
557
+ "license": "MIT",
558
+ "dependencies": {
559
+ "@types/estree-jsx": "^1.0.0",
560
+ "@types/hast": "^3.0.0",
561
+ "@types/mdast": "^4.0.0",
562
+ "devlop": "^1.0.0",
563
+ "mdast-util-from-markdown": "^2.0.0",
564
+ "mdast-util-to-markdown": "^2.0.0"
565
+ },
566
+ "funding": {
567
+ "type": "opencollective",
568
+ "url": "https://opencollective.com/unified"
569
+ }
570
+ },
571
+ "node_modules/mdast-util-phrasing": {
572
+ "version": "4.1.0",
573
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
574
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
575
+ "license": "MIT",
576
+ "dependencies": {
577
+ "@types/mdast": "^4.0.0",
578
+ "unist-util-is": "^6.0.0"
579
+ },
580
+ "funding": {
581
+ "type": "opencollective",
582
+ "url": "https://opencollective.com/unified"
583
+ }
584
+ },
585
+ "node_modules/mdast-util-to-hast": {
586
+ "version": "13.2.0",
587
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
588
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
589
+ "license": "MIT",
590
+ "dependencies": {
591
+ "@types/hast": "^3.0.0",
592
+ "@types/mdast": "^4.0.0",
593
+ "@ungap/structured-clone": "^1.0.0",
594
+ "devlop": "^1.0.0",
595
+ "micromark-util-sanitize-uri": "^2.0.0",
596
+ "trim-lines": "^3.0.0",
597
+ "unist-util-position": "^5.0.0",
598
+ "unist-util-visit": "^5.0.0",
599
+ "vfile": "^6.0.0"
600
+ },
601
+ "funding": {
602
+ "type": "opencollective",
603
+ "url": "https://opencollective.com/unified"
604
+ }
605
+ },
606
+ "node_modules/mdast-util-to-markdown": {
607
+ "version": "2.1.2",
608
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
609
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
610
+ "license": "MIT",
611
+ "dependencies": {
612
+ "@types/mdast": "^4.0.0",
613
+ "@types/unist": "^3.0.0",
614
+ "longest-streak": "^3.0.0",
615
+ "mdast-util-phrasing": "^4.0.0",
616
+ "mdast-util-to-string": "^4.0.0",
617
+ "micromark-util-classify-character": "^2.0.0",
618
+ "micromark-util-decode-string": "^2.0.0",
619
+ "unist-util-visit": "^5.0.0",
620
+ "zwitch": "^2.0.0"
621
+ },
622
+ "funding": {
623
+ "type": "opencollective",
624
+ "url": "https://opencollective.com/unified"
625
+ }
626
+ },
627
+ "node_modules/mdast-util-to-string": {
628
+ "version": "4.0.0",
629
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
630
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
631
+ "license": "MIT",
632
+ "dependencies": {
633
+ "@types/mdast": "^4.0.0"
634
+ },
635
+ "funding": {
636
+ "type": "opencollective",
637
+ "url": "https://opencollective.com/unified"
638
+ }
639
+ },
640
+ "node_modules/micromark": {
641
+ "version": "4.0.2",
642
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
643
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
644
+ "funding": [
645
+ {
646
+ "type": "GitHub Sponsors",
647
+ "url": "https://github.com/sponsors/unifiedjs"
648
+ },
649
+ {
650
+ "type": "OpenCollective",
651
+ "url": "https://opencollective.com/unified"
652
+ }
653
+ ],
654
+ "license": "MIT",
655
+ "dependencies": {
656
+ "@types/debug": "^4.0.0",
657
+ "debug": "^4.0.0",
658
+ "decode-named-character-reference": "^1.0.0",
659
+ "devlop": "^1.0.0",
660
+ "micromark-core-commonmark": "^2.0.0",
661
+ "micromark-factory-space": "^2.0.0",
662
+ "micromark-util-character": "^2.0.0",
663
+ "micromark-util-chunked": "^2.0.0",
664
+ "micromark-util-combine-extensions": "^2.0.0",
665
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
666
+ "micromark-util-encode": "^2.0.0",
667
+ "micromark-util-normalize-identifier": "^2.0.0",
668
+ "micromark-util-resolve-all": "^2.0.0",
669
+ "micromark-util-sanitize-uri": "^2.0.0",
670
+ "micromark-util-subtokenize": "^2.0.0",
671
+ "micromark-util-symbol": "^2.0.0",
672
+ "micromark-util-types": "^2.0.0"
673
+ }
674
+ },
675
+ "node_modules/micromark-core-commonmark": {
676
+ "version": "2.0.3",
677
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
678
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
679
+ "funding": [
680
+ {
681
+ "type": "GitHub Sponsors",
682
+ "url": "https://github.com/sponsors/unifiedjs"
683
+ },
684
+ {
685
+ "type": "OpenCollective",
686
+ "url": "https://opencollective.com/unified"
687
+ }
688
+ ],
689
+ "license": "MIT",
690
+ "dependencies": {
691
+ "decode-named-character-reference": "^1.0.0",
692
+ "devlop": "^1.0.0",
693
+ "micromark-factory-destination": "^2.0.0",
694
+ "micromark-factory-label": "^2.0.0",
695
+ "micromark-factory-space": "^2.0.0",
696
+ "micromark-factory-title": "^2.0.0",
697
+ "micromark-factory-whitespace": "^2.0.0",
698
+ "micromark-util-character": "^2.0.0",
699
+ "micromark-util-chunked": "^2.0.0",
700
+ "micromark-util-classify-character": "^2.0.0",
701
+ "micromark-util-html-tag-name": "^2.0.0",
702
+ "micromark-util-normalize-identifier": "^2.0.0",
703
+ "micromark-util-resolve-all": "^2.0.0",
704
+ "micromark-util-subtokenize": "^2.0.0",
705
+ "micromark-util-symbol": "^2.0.0",
706
+ "micromark-util-types": "^2.0.0"
707
+ }
708
+ },
709
+ "node_modules/micromark-extension-gfm": {
710
+ "version": "3.0.0",
711
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
712
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
713
+ "license": "MIT",
714
+ "dependencies": {
715
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
716
+ "micromark-extension-gfm-footnote": "^2.0.0",
717
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
718
+ "micromark-extension-gfm-table": "^2.0.0",
719
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
720
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
721
+ "micromark-util-combine-extensions": "^2.0.0",
722
+ "micromark-util-types": "^2.0.0"
723
+ },
724
+ "funding": {
725
+ "type": "opencollective",
726
+ "url": "https://opencollective.com/unified"
727
+ }
728
+ },
729
+ "node_modules/micromark-extension-gfm-autolink-literal": {
730
+ "version": "2.1.0",
731
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
732
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
733
+ "license": "MIT",
734
+ "dependencies": {
735
+ "micromark-util-character": "^2.0.0",
736
+ "micromark-util-sanitize-uri": "^2.0.0",
737
+ "micromark-util-symbol": "^2.0.0",
738
+ "micromark-util-types": "^2.0.0"
739
+ },
740
+ "funding": {
741
+ "type": "opencollective",
742
+ "url": "https://opencollective.com/unified"
743
+ }
744
+ },
745
+ "node_modules/micromark-extension-gfm-footnote": {
746
+ "version": "2.1.0",
747
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
748
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
749
+ "license": "MIT",
750
+ "dependencies": {
751
+ "devlop": "^1.0.0",
752
+ "micromark-core-commonmark": "^2.0.0",
753
+ "micromark-factory-space": "^2.0.0",
754
+ "micromark-util-character": "^2.0.0",
755
+ "micromark-util-normalize-identifier": "^2.0.0",
756
+ "micromark-util-sanitize-uri": "^2.0.0",
757
+ "micromark-util-symbol": "^2.0.0",
758
+ "micromark-util-types": "^2.0.0"
759
+ },
760
+ "funding": {
761
+ "type": "opencollective",
762
+ "url": "https://opencollective.com/unified"
763
+ }
764
+ },
765
+ "node_modules/micromark-extension-gfm-strikethrough": {
766
+ "version": "2.1.0",
767
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
768
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
769
+ "license": "MIT",
770
+ "dependencies": {
771
+ "devlop": "^1.0.0",
772
+ "micromark-util-chunked": "^2.0.0",
773
+ "micromark-util-classify-character": "^2.0.0",
774
+ "micromark-util-resolve-all": "^2.0.0",
775
+ "micromark-util-symbol": "^2.0.0",
776
+ "micromark-util-types": "^2.0.0"
777
+ },
778
+ "funding": {
779
+ "type": "opencollective",
780
+ "url": "https://opencollective.com/unified"
781
+ }
782
+ },
783
+ "node_modules/micromark-extension-gfm-table": {
784
+ "version": "2.1.1",
785
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
786
+ "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
787
+ "license": "MIT",
788
+ "dependencies": {
789
+ "devlop": "^1.0.0",
790
+ "micromark-factory-space": "^2.0.0",
791
+ "micromark-util-character": "^2.0.0",
792
+ "micromark-util-symbol": "^2.0.0",
793
+ "micromark-util-types": "^2.0.0"
794
+ },
795
+ "funding": {
796
+ "type": "opencollective",
797
+ "url": "https://opencollective.com/unified"
798
+ }
799
+ },
800
+ "node_modules/micromark-extension-gfm-tagfilter": {
801
+ "version": "2.0.0",
802
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
803
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
804
+ "license": "MIT",
805
+ "dependencies": {
806
+ "micromark-util-types": "^2.0.0"
807
+ },
808
+ "funding": {
809
+ "type": "opencollective",
810
+ "url": "https://opencollective.com/unified"
811
+ }
812
+ },
813
+ "node_modules/micromark-extension-gfm-task-list-item": {
814
+ "version": "2.1.0",
815
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
816
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
817
+ "license": "MIT",
818
+ "dependencies": {
819
+ "devlop": "^1.0.0",
820
+ "micromark-factory-space": "^2.0.0",
821
+ "micromark-util-character": "^2.0.0",
822
+ "micromark-util-symbol": "^2.0.0",
823
+ "micromark-util-types": "^2.0.0"
824
+ },
825
+ "funding": {
826
+ "type": "opencollective",
827
+ "url": "https://opencollective.com/unified"
828
+ }
829
+ },
830
+ "node_modules/micromark-factory-destination": {
831
+ "version": "2.0.1",
832
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
833
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
834
+ "funding": [
835
+ {
836
+ "type": "GitHub Sponsors",
837
+ "url": "https://github.com/sponsors/unifiedjs"
838
+ },
839
+ {
840
+ "type": "OpenCollective",
841
+ "url": "https://opencollective.com/unified"
842
+ }
843
+ ],
844
+ "license": "MIT",
845
+ "dependencies": {
846
+ "micromark-util-character": "^2.0.0",
847
+ "micromark-util-symbol": "^2.0.0",
848
+ "micromark-util-types": "^2.0.0"
849
+ }
850
+ },
851
+ "node_modules/micromark-factory-label": {
852
+ "version": "2.0.1",
853
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
854
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
855
+ "funding": [
856
+ {
857
+ "type": "GitHub Sponsors",
858
+ "url": "https://github.com/sponsors/unifiedjs"
859
+ },
860
+ {
861
+ "type": "OpenCollective",
862
+ "url": "https://opencollective.com/unified"
863
+ }
864
+ ],
865
+ "license": "MIT",
866
+ "dependencies": {
867
+ "devlop": "^1.0.0",
868
+ "micromark-util-character": "^2.0.0",
869
+ "micromark-util-symbol": "^2.0.0",
870
+ "micromark-util-types": "^2.0.0"
871
+ }
872
+ },
873
+ "node_modules/micromark-factory-space": {
874
+ "version": "2.0.1",
875
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
876
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
877
+ "funding": [
878
+ {
879
+ "type": "GitHub Sponsors",
880
+ "url": "https://github.com/sponsors/unifiedjs"
881
+ },
882
+ {
883
+ "type": "OpenCollective",
884
+ "url": "https://opencollective.com/unified"
885
+ }
886
+ ],
887
+ "license": "MIT",
888
+ "dependencies": {
889
+ "micromark-util-character": "^2.0.0",
890
+ "micromark-util-types": "^2.0.0"
891
+ }
892
+ },
893
+ "node_modules/micromark-factory-title": {
894
+ "version": "2.0.1",
895
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
896
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
897
+ "funding": [
898
+ {
899
+ "type": "GitHub Sponsors",
900
+ "url": "https://github.com/sponsors/unifiedjs"
901
+ },
902
+ {
903
+ "type": "OpenCollective",
904
+ "url": "https://opencollective.com/unified"
905
+ }
906
+ ],
907
+ "license": "MIT",
908
+ "dependencies": {
909
+ "micromark-factory-space": "^2.0.0",
910
+ "micromark-util-character": "^2.0.0",
911
+ "micromark-util-symbol": "^2.0.0",
912
+ "micromark-util-types": "^2.0.0"
913
+ }
914
+ },
915
+ "node_modules/micromark-factory-whitespace": {
916
+ "version": "2.0.1",
917
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
918
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
919
+ "funding": [
920
+ {
921
+ "type": "GitHub Sponsors",
922
+ "url": "https://github.com/sponsors/unifiedjs"
923
+ },
924
+ {
925
+ "type": "OpenCollective",
926
+ "url": "https://opencollective.com/unified"
927
+ }
928
+ ],
929
+ "license": "MIT",
930
+ "dependencies": {
931
+ "micromark-factory-space": "^2.0.0",
932
+ "micromark-util-character": "^2.0.0",
933
+ "micromark-util-symbol": "^2.0.0",
934
+ "micromark-util-types": "^2.0.0"
935
+ }
936
+ },
937
+ "node_modules/micromark-util-character": {
938
+ "version": "2.1.1",
939
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
940
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
941
+ "funding": [
942
+ {
943
+ "type": "GitHub Sponsors",
944
+ "url": "https://github.com/sponsors/unifiedjs"
945
+ },
946
+ {
947
+ "type": "OpenCollective",
948
+ "url": "https://opencollective.com/unified"
949
+ }
950
+ ],
951
+ "license": "MIT",
952
+ "dependencies": {
953
+ "micromark-util-symbol": "^2.0.0",
954
+ "micromark-util-types": "^2.0.0"
955
+ }
956
+ },
957
+ "node_modules/micromark-util-chunked": {
958
+ "version": "2.0.1",
959
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
960
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
961
+ "funding": [
962
+ {
963
+ "type": "GitHub Sponsors",
964
+ "url": "https://github.com/sponsors/unifiedjs"
965
+ },
966
+ {
967
+ "type": "OpenCollective",
968
+ "url": "https://opencollective.com/unified"
969
+ }
970
+ ],
971
+ "license": "MIT",
972
+ "dependencies": {
973
+ "micromark-util-symbol": "^2.0.0"
974
+ }
975
+ },
976
+ "node_modules/micromark-util-classify-character": {
977
+ "version": "2.0.1",
978
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
979
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
980
+ "funding": [
981
+ {
982
+ "type": "GitHub Sponsors",
983
+ "url": "https://github.com/sponsors/unifiedjs"
984
+ },
985
+ {
986
+ "type": "OpenCollective",
987
+ "url": "https://opencollective.com/unified"
988
+ }
989
+ ],
990
+ "license": "MIT",
991
+ "dependencies": {
992
+ "micromark-util-character": "^2.0.0",
993
+ "micromark-util-symbol": "^2.0.0",
994
+ "micromark-util-types": "^2.0.0"
995
+ }
996
+ },
997
+ "node_modules/micromark-util-combine-extensions": {
998
+ "version": "2.0.1",
999
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
1000
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
1001
+ "funding": [
1002
+ {
1003
+ "type": "GitHub Sponsors",
1004
+ "url": "https://github.com/sponsors/unifiedjs"
1005
+ },
1006
+ {
1007
+ "type": "OpenCollective",
1008
+ "url": "https://opencollective.com/unified"
1009
+ }
1010
+ ],
1011
+ "license": "MIT",
1012
+ "dependencies": {
1013
+ "micromark-util-chunked": "^2.0.0",
1014
+ "micromark-util-types": "^2.0.0"
1015
+ }
1016
+ },
1017
+ "node_modules/micromark-util-decode-numeric-character-reference": {
1018
+ "version": "2.0.2",
1019
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
1020
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
1021
+ "funding": [
1022
+ {
1023
+ "type": "GitHub Sponsors",
1024
+ "url": "https://github.com/sponsors/unifiedjs"
1025
+ },
1026
+ {
1027
+ "type": "OpenCollective",
1028
+ "url": "https://opencollective.com/unified"
1029
+ }
1030
+ ],
1031
+ "license": "MIT",
1032
+ "dependencies": {
1033
+ "micromark-util-symbol": "^2.0.0"
1034
+ }
1035
+ },
1036
+ "node_modules/micromark-util-decode-string": {
1037
+ "version": "2.0.1",
1038
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
1039
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
1040
+ "funding": [
1041
+ {
1042
+ "type": "GitHub Sponsors",
1043
+ "url": "https://github.com/sponsors/unifiedjs"
1044
+ },
1045
+ {
1046
+ "type": "OpenCollective",
1047
+ "url": "https://opencollective.com/unified"
1048
+ }
1049
+ ],
1050
+ "license": "MIT",
1051
+ "dependencies": {
1052
+ "decode-named-character-reference": "^1.0.0",
1053
+ "micromark-util-character": "^2.0.0",
1054
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
1055
+ "micromark-util-symbol": "^2.0.0"
1056
+ }
1057
+ },
1058
+ "node_modules/micromark-util-encode": {
1059
+ "version": "2.0.1",
1060
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
1061
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
1062
+ "funding": [
1063
+ {
1064
+ "type": "GitHub Sponsors",
1065
+ "url": "https://github.com/sponsors/unifiedjs"
1066
+ },
1067
+ {
1068
+ "type": "OpenCollective",
1069
+ "url": "https://opencollective.com/unified"
1070
+ }
1071
+ ],
1072
+ "license": "MIT"
1073
+ },
1074
+ "node_modules/micromark-util-html-tag-name": {
1075
+ "version": "2.0.1",
1076
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
1077
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
1078
+ "funding": [
1079
+ {
1080
+ "type": "GitHub Sponsors",
1081
+ "url": "https://github.com/sponsors/unifiedjs"
1082
+ },
1083
+ {
1084
+ "type": "OpenCollective",
1085
+ "url": "https://opencollective.com/unified"
1086
+ }
1087
+ ],
1088
+ "license": "MIT"
1089
+ },
1090
+ "node_modules/micromark-util-normalize-identifier": {
1091
+ "version": "2.0.1",
1092
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
1093
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
1094
+ "funding": [
1095
+ {
1096
+ "type": "GitHub Sponsors",
1097
+ "url": "https://github.com/sponsors/unifiedjs"
1098
+ },
1099
+ {
1100
+ "type": "OpenCollective",
1101
+ "url": "https://opencollective.com/unified"
1102
+ }
1103
+ ],
1104
+ "license": "MIT",
1105
+ "dependencies": {
1106
+ "micromark-util-symbol": "^2.0.0"
1107
+ }
1108
+ },
1109
+ "node_modules/micromark-util-resolve-all": {
1110
+ "version": "2.0.1",
1111
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
1112
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
1113
+ "funding": [
1114
+ {
1115
+ "type": "GitHub Sponsors",
1116
+ "url": "https://github.com/sponsors/unifiedjs"
1117
+ },
1118
+ {
1119
+ "type": "OpenCollective",
1120
+ "url": "https://opencollective.com/unified"
1121
+ }
1122
+ ],
1123
+ "license": "MIT",
1124
+ "dependencies": {
1125
+ "micromark-util-types": "^2.0.0"
1126
+ }
1127
+ },
1128
+ "node_modules/micromark-util-sanitize-uri": {
1129
+ "version": "2.0.1",
1130
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
1131
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
1132
+ "funding": [
1133
+ {
1134
+ "type": "GitHub Sponsors",
1135
+ "url": "https://github.com/sponsors/unifiedjs"
1136
+ },
1137
+ {
1138
+ "type": "OpenCollective",
1139
+ "url": "https://opencollective.com/unified"
1140
+ }
1141
+ ],
1142
+ "license": "MIT",
1143
+ "dependencies": {
1144
+ "micromark-util-character": "^2.0.0",
1145
+ "micromark-util-encode": "^2.0.0",
1146
+ "micromark-util-symbol": "^2.0.0"
1147
+ }
1148
+ },
1149
+ "node_modules/micromark-util-subtokenize": {
1150
+ "version": "2.1.0",
1151
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
1152
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
1153
+ "funding": [
1154
+ {
1155
+ "type": "GitHub Sponsors",
1156
+ "url": "https://github.com/sponsors/unifiedjs"
1157
+ },
1158
+ {
1159
+ "type": "OpenCollective",
1160
+ "url": "https://opencollective.com/unified"
1161
+ }
1162
+ ],
1163
+ "license": "MIT",
1164
+ "dependencies": {
1165
+ "devlop": "^1.0.0",
1166
+ "micromark-util-chunked": "^2.0.0",
1167
+ "micromark-util-symbol": "^2.0.0",
1168
+ "micromark-util-types": "^2.0.0"
1169
+ }
1170
+ },
1171
+ "node_modules/micromark-util-symbol": {
1172
+ "version": "2.0.1",
1173
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
1174
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
1175
+ "funding": [
1176
+ {
1177
+ "type": "GitHub Sponsors",
1178
+ "url": "https://github.com/sponsors/unifiedjs"
1179
+ },
1180
+ {
1181
+ "type": "OpenCollective",
1182
+ "url": "https://opencollective.com/unified"
1183
+ }
1184
+ ],
1185
+ "license": "MIT"
1186
+ },
1187
+ "node_modules/micromark-util-types": {
1188
+ "version": "2.0.2",
1189
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
1190
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
1191
+ "funding": [
1192
+ {
1193
+ "type": "GitHub Sponsors",
1194
+ "url": "https://github.com/sponsors/unifiedjs"
1195
+ },
1196
+ {
1197
+ "type": "OpenCollective",
1198
+ "url": "https://opencollective.com/unified"
1199
+ }
1200
+ ],
1201
+ "license": "MIT"
1202
+ },
1203
+ "node_modules/ms": {
1204
+ "version": "2.1.3",
1205
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1206
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1207
+ "license": "MIT"
1208
+ },
1209
+ "node_modules/parse-entities": {
1210
+ "version": "4.0.2",
1211
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
1212
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
1213
+ "license": "MIT",
1214
+ "dependencies": {
1215
+ "@types/unist": "^2.0.0",
1216
+ "character-entities-legacy": "^3.0.0",
1217
+ "character-reference-invalid": "^2.0.0",
1218
+ "decode-named-character-reference": "^1.0.0",
1219
+ "is-alphanumerical": "^2.0.0",
1220
+ "is-decimal": "^2.0.0",
1221
+ "is-hexadecimal": "^2.0.0"
1222
+ },
1223
+ "funding": {
1224
+ "type": "github",
1225
+ "url": "https://github.com/sponsors/wooorm"
1226
+ }
1227
+ },
1228
+ "node_modules/parse-entities/node_modules/@types/unist": {
1229
+ "version": "2.0.11",
1230
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
1231
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
1232
+ "license": "MIT"
1233
+ },
1234
+ "node_modules/property-information": {
1235
+ "version": "7.1.0",
1236
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
1237
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
1238
+ "license": "MIT",
1239
+ "funding": {
1240
+ "type": "github",
1241
+ "url": "https://github.com/sponsors/wooorm"
1242
+ }
1243
+ },
1244
+ "node_modules/react": {
1245
+ "version": "19.2.0",
1246
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
1247
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
1248
+ "license": "MIT",
1249
+ "peer": true,
1250
+ "engines": {
1251
+ "node": ">=0.10.0"
1252
+ }
1253
+ },
1254
+ "node_modules/react-markdown": {
1255
+ "version": "10.1.0",
1256
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
1257
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
1258
+ "license": "MIT",
1259
+ "dependencies": {
1260
+ "@types/hast": "^3.0.0",
1261
+ "@types/mdast": "^4.0.0",
1262
+ "devlop": "^1.0.0",
1263
+ "hast-util-to-jsx-runtime": "^2.0.0",
1264
+ "html-url-attributes": "^3.0.0",
1265
+ "mdast-util-to-hast": "^13.0.0",
1266
+ "remark-parse": "^11.0.0",
1267
+ "remark-rehype": "^11.0.0",
1268
+ "unified": "^11.0.0",
1269
+ "unist-util-visit": "^5.0.0",
1270
+ "vfile": "^6.0.0"
1271
+ },
1272
+ "funding": {
1273
+ "type": "opencollective",
1274
+ "url": "https://opencollective.com/unified"
1275
+ },
1276
+ "peerDependencies": {
1277
+ "@types/react": ">=18",
1278
+ "react": ">=18"
1279
+ }
1280
+ },
1281
+ "node_modules/remark-gfm": {
1282
+ "version": "4.0.1",
1283
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
1284
+ "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
1285
+ "license": "MIT",
1286
+ "dependencies": {
1287
+ "@types/mdast": "^4.0.0",
1288
+ "mdast-util-gfm": "^3.0.0",
1289
+ "micromark-extension-gfm": "^3.0.0",
1290
+ "remark-parse": "^11.0.0",
1291
+ "remark-stringify": "^11.0.0",
1292
+ "unified": "^11.0.0"
1293
+ },
1294
+ "funding": {
1295
+ "type": "opencollective",
1296
+ "url": "https://opencollective.com/unified"
1297
+ }
1298
+ },
1299
+ "node_modules/remark-parse": {
1300
+ "version": "11.0.0",
1301
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
1302
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
1303
+ "license": "MIT",
1304
+ "dependencies": {
1305
+ "@types/mdast": "^4.0.0",
1306
+ "mdast-util-from-markdown": "^2.0.0",
1307
+ "micromark-util-types": "^2.0.0",
1308
+ "unified": "^11.0.0"
1309
+ },
1310
+ "funding": {
1311
+ "type": "opencollective",
1312
+ "url": "https://opencollective.com/unified"
1313
+ }
1314
+ },
1315
+ "node_modules/remark-rehype": {
1316
+ "version": "11.1.2",
1317
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
1318
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
1319
+ "license": "MIT",
1320
+ "dependencies": {
1321
+ "@types/hast": "^3.0.0",
1322
+ "@types/mdast": "^4.0.0",
1323
+ "mdast-util-to-hast": "^13.0.0",
1324
+ "unified": "^11.0.0",
1325
+ "vfile": "^6.0.0"
1326
+ },
1327
+ "funding": {
1328
+ "type": "opencollective",
1329
+ "url": "https://opencollective.com/unified"
1330
+ }
1331
+ },
1332
+ "node_modules/remark-stringify": {
1333
+ "version": "11.0.0",
1334
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
1335
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
1336
+ "license": "MIT",
1337
+ "dependencies": {
1338
+ "@types/mdast": "^4.0.0",
1339
+ "mdast-util-to-markdown": "^2.0.0",
1340
+ "unified": "^11.0.0"
1341
+ },
1342
+ "funding": {
1343
+ "type": "opencollective",
1344
+ "url": "https://opencollective.com/unified"
1345
+ }
1346
+ },
1347
+ "node_modules/space-separated-tokens": {
1348
+ "version": "2.0.2",
1349
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
1350
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
1351
+ "license": "MIT",
1352
+ "funding": {
1353
+ "type": "github",
1354
+ "url": "https://github.com/sponsors/wooorm"
1355
+ }
1356
+ },
1357
+ "node_modules/stringify-entities": {
1358
+ "version": "4.0.4",
1359
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
1360
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
1361
+ "license": "MIT",
1362
+ "dependencies": {
1363
+ "character-entities-html4": "^2.0.0",
1364
+ "character-entities-legacy": "^3.0.0"
1365
+ },
1366
+ "funding": {
1367
+ "type": "github",
1368
+ "url": "https://github.com/sponsors/wooorm"
1369
+ }
1370
+ },
1371
+ "node_modules/style-to-js": {
1372
+ "version": "1.1.21",
1373
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
1374
+ "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
1375
+ "license": "MIT",
1376
+ "dependencies": {
1377
+ "style-to-object": "1.0.14"
1378
+ }
1379
+ },
1380
+ "node_modules/style-to-object": {
1381
+ "version": "1.0.14",
1382
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
1383
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
1384
+ "license": "MIT",
1385
+ "dependencies": {
1386
+ "inline-style-parser": "0.2.7"
1387
+ }
1388
+ },
1389
+ "node_modules/trim-lines": {
1390
+ "version": "3.0.1",
1391
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
1392
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
1393
+ "license": "MIT",
1394
+ "funding": {
1395
+ "type": "github",
1396
+ "url": "https://github.com/sponsors/wooorm"
1397
+ }
1398
+ },
1399
+ "node_modules/trough": {
1400
+ "version": "2.2.0",
1401
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
1402
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
1403
+ "license": "MIT",
1404
+ "funding": {
1405
+ "type": "github",
1406
+ "url": "https://github.com/sponsors/wooorm"
1407
+ }
1408
+ },
1409
+ "node_modules/unified": {
1410
+ "version": "11.0.5",
1411
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
1412
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
1413
+ "license": "MIT",
1414
+ "dependencies": {
1415
+ "@types/unist": "^3.0.0",
1416
+ "bail": "^2.0.0",
1417
+ "devlop": "^1.0.0",
1418
+ "extend": "^3.0.0",
1419
+ "is-plain-obj": "^4.0.0",
1420
+ "trough": "^2.0.0",
1421
+ "vfile": "^6.0.0"
1422
+ },
1423
+ "funding": {
1424
+ "type": "opencollective",
1425
+ "url": "https://opencollective.com/unified"
1426
+ }
1427
+ },
1428
+ "node_modules/unist-util-is": {
1429
+ "version": "6.0.1",
1430
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
1431
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
1432
+ "license": "MIT",
1433
+ "dependencies": {
1434
+ "@types/unist": "^3.0.0"
1435
+ },
1436
+ "funding": {
1437
+ "type": "opencollective",
1438
+ "url": "https://opencollective.com/unified"
1439
+ }
1440
+ },
1441
+ "node_modules/unist-util-position": {
1442
+ "version": "5.0.0",
1443
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
1444
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
1445
+ "license": "MIT",
1446
+ "dependencies": {
1447
+ "@types/unist": "^3.0.0"
1448
+ },
1449
+ "funding": {
1450
+ "type": "opencollective",
1451
+ "url": "https://opencollective.com/unified"
1452
+ }
1453
+ },
1454
+ "node_modules/unist-util-stringify-position": {
1455
+ "version": "4.0.0",
1456
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
1457
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
1458
+ "license": "MIT",
1459
+ "dependencies": {
1460
+ "@types/unist": "^3.0.0"
1461
+ },
1462
+ "funding": {
1463
+ "type": "opencollective",
1464
+ "url": "https://opencollective.com/unified"
1465
+ }
1466
+ },
1467
+ "node_modules/unist-util-visit": {
1468
+ "version": "5.0.0",
1469
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
1470
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
1471
+ "license": "MIT",
1472
+ "dependencies": {
1473
+ "@types/unist": "^3.0.0",
1474
+ "unist-util-is": "^6.0.0",
1475
+ "unist-util-visit-parents": "^6.0.0"
1476
+ },
1477
+ "funding": {
1478
+ "type": "opencollective",
1479
+ "url": "https://opencollective.com/unified"
1480
+ }
1481
+ },
1482
+ "node_modules/unist-util-visit-parents": {
1483
+ "version": "6.0.2",
1484
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
1485
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
1486
+ "license": "MIT",
1487
+ "dependencies": {
1488
+ "@types/unist": "^3.0.0",
1489
+ "unist-util-is": "^6.0.0"
1490
+ },
1491
+ "funding": {
1492
+ "type": "opencollective",
1493
+ "url": "https://opencollective.com/unified"
1494
+ }
1495
+ },
1496
+ "node_modules/vfile": {
1497
+ "version": "6.0.3",
1498
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
1499
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
1500
+ "license": "MIT",
1501
+ "dependencies": {
1502
+ "@types/unist": "^3.0.0",
1503
+ "vfile-message": "^4.0.0"
1504
+ },
1505
+ "funding": {
1506
+ "type": "opencollective",
1507
+ "url": "https://opencollective.com/unified"
1508
+ }
1509
+ },
1510
+ "node_modules/vfile-message": {
1511
+ "version": "4.0.3",
1512
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
1513
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
1514
+ "license": "MIT",
1515
+ "dependencies": {
1516
+ "@types/unist": "^3.0.0",
1517
+ "unist-util-stringify-position": "^4.0.0"
1518
+ },
1519
+ "funding": {
1520
+ "type": "opencollective",
1521
+ "url": "https://opencollective.com/unified"
1522
+ }
1523
+ },
1524
+ "node_modules/zwitch": {
1525
+ "version": "2.0.4",
1526
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
1527
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
1528
+ "license": "MIT",
1529
+ "funding": {
1530
+ "type": "github",
1531
+ "url": "https://github.com/sponsors/wooorm"
1532
+ }
1533
+ }
1534
+ }
1535
+ }
package.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "dependencies": {
3
+ "react-markdown": "^10.1.0",
4
+ "remark-gfm": "^4.0.1"
5
+ }
6
+ }
processed_documents.json ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "law-document-rag"
3
+ version = "0.1.0"
4
+ description = "Law Document RAG Chat Application using FastAPI, LangChain, and FAISS"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.12"
7
+ dependencies = [
8
+ "fastapi==0.104.1",
9
+ "uvicorn[standard]==0.24.0",
10
+ "langchain==0.1.16",
11
+ "langchain-community==0.0.36",
12
+ "openai>=1.50.0",
13
+ "httpx==0.27.2",
14
+ "requests==2.31.0",
15
+ "faiss-cpu==1.7.4",
16
+ "pypdf==3.17.4",
17
+ "pdfplumber==0.11.0",
18
+ "pymupdf==1.23.8",
19
+ "pillow==10.2.0",
20
+ "python-docx==1.1.0",
21
+ "docx2txt==0.8",
22
+ "unstructured==0.11.6",
23
+ "pydantic==2.5.2",
24
+ "python-multipart==0.0.6",
25
+ "python-dotenv==1.0.0",
26
+ "tiktoken>=0.12.0",
27
+ ]
uv.lock ADDED
The diff for this file is too large to render. See raw diff