Upload 47 files
Browse files- .env.example +15 -15
- .gitattributes +2 -1
- .gitignore +13 -13
- CONFIG_NOTES.md +38 -0
- CUSTOMIZATION_GUIDE.md +55 -0
- Dockerfile +31 -0
- PHPMailer/Exception.php +40 -0
- PHPMailer/PHPMailer.php +0 -0
- PHPMailer/SMTP.php +1509 -0
- PROJECT_OVERVIEW.md +30 -0
- README.md +164 -0
- app.py +6 -0
- app/__init__.py +0 -0
- app/admin/__init__.py +1 -0
- app/admin/router.py +346 -0
- app/admin/templates/admin.html +1143 -0
- app/api/__init__.py +0 -0
- app/api/schemas.py +27 -0
- app/core/__init__.py +0 -0
- app/core/config.py +50 -0
- app/main.py +195 -0
- app/services/__init__.py +0 -0
- app/services/chunker.py +39 -0
- app/services/document_loader.py +37 -0
- app/services/embeddings.py +34 -0
- app/services/llm.py +117 -0
- app/services/rag_pipeline.py +58 -0
- app/services/rate_limiter.py +24 -0
- app/services/reranker.py +26 -0
- app/services/session_store.py +284 -0
- app/services/vector_store.py +127 -0
- app/ui_gradio.py +275 -0
- data/index/.gitkeep +0 -0
- data/sessions/.gitkeep +0 -0
- docker-compose.yml +14 -0
- docs/MartechSol_Employee_HandBook (7) (1).pdf +3 -0
- docs/MartechSol_Employee_Handbook_Extracted.txt +1091 -0
- floaitng-icon/KhloeSAC.lottie +3 -0
- martech_sol_logo.jpg +0 -0
- requirements.txt +13 -0
- robocopy_backup.log +81 -0
- static/addon.html +557 -0
- static/bot-icon.lottie +3 -0
- static/chat-loader.js +48 -0
- wordpress code/WP_INTEGRATION_SCRIPT.txt +12 -0
- wordpress code/addon.html +477 -0
- wordpress code/addon_backup.html +175 -0
.env.example
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
-
LLM_PROVIDER=groq
|
| 2 |
-
GROQ_API_KEY=your_groq_api_key
|
| 3 |
-
GROQ_MODEL=llama-3.1-8b-instant
|
| 4 |
-
HF_API_KEY=your_huggingface_api_key
|
| 5 |
-
HF_MODEL=meta-llama/Llama-3.1-8B-Instruct
|
| 6 |
-
EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
|
| 7 |
-
DOCS_DIR=docs
|
| 8 |
-
INDEX_DIR=data/index
|
| 9 |
-
SESSIONS_DIR=data/sessions
|
| 10 |
-
TOP_K=4
|
| 11 |
-
CORS_ALLOW_ORIGINS=*
|
| 12 |
-
API_KEY=
|
| 13 |
-
RATE_LIMIT_REQUESTS=60
|
| 14 |
-
RATE_LIMIT_WINDOW_SECONDS=60
|
| 15 |
-
RAG_API_URL=
|
|
|
|
| 1 |
+
LLM_PROVIDER=groq
|
| 2 |
+
GROQ_API_KEY=your_groq_api_key
|
| 3 |
+
GROQ_MODEL=llama-3.1-8b-instant
|
| 4 |
+
HF_API_KEY=your_huggingface_api_key
|
| 5 |
+
HF_MODEL=meta-llama/Llama-3.1-8B-Instruct
|
| 6 |
+
EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
|
| 7 |
+
DOCS_DIR=docs
|
| 8 |
+
INDEX_DIR=data/index
|
| 9 |
+
SESSIONS_DIR=data/sessions
|
| 10 |
+
TOP_K=4
|
| 11 |
+
CORS_ALLOW_ORIGINS=*
|
| 12 |
+
API_KEY=
|
| 13 |
+
RATE_LIMIT_REQUESTS=60
|
| 14 |
+
RATE_LIMIT_WINDOW_SECONDS=60
|
| 15 |
+
RAG_API_URL=
|
.gitattributes
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
*.lottie filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 1 |
+
*.lottie filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
docs/MartechSol_Employee_HandBook[[:space:]](7)[[:space:]](1).pdf filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
.env
|
| 2 |
-
__pycache__/
|
| 3 |
-
.pytest_cache/
|
| 4 |
-
.mypy_cache/
|
| 5 |
-
.venv/
|
| 6 |
-
venv/
|
| 7 |
-
data/index/*
|
| 8 |
-
!data/index/.gitkeep
|
| 9 |
-
# Session data — user privacy, not committed to repo
|
| 10 |
-
data/sessions/*.json
|
| 11 |
-
!data/sessions/.gitkeep
|
| 12 |
-
# Binary docs — managed directly on HF Space, not via git
|
| 13 |
-
docs/*.pdf
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
.pytest_cache/
|
| 4 |
+
.mypy_cache/
|
| 5 |
+
.venv/
|
| 6 |
+
venv/
|
| 7 |
+
data/index/*
|
| 8 |
+
!data/index/.gitkeep
|
| 9 |
+
# Session data — user privacy, not committed to repo
|
| 10 |
+
data/sessions/*.json
|
| 11 |
+
!data/sessions/.gitkeep
|
| 12 |
+
# Binary docs — managed directly on HF Space, not via git
|
| 13 |
+
docs/*.pdf
|
CONFIG_NOTES.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration Backup - Martechsol RAG Chatbot
|
| 2 |
+
**Date:** 2026-04-28
|
| 3 |
+
**Source:** C:\Users\DELL\Desktop\RAG_backup2
|
| 4 |
+
**Destination:** D:\RAG_Backup_2026_04_28
|
| 5 |
+
|
| 6 |
+
## Core Settings (from app/core/config.py)
|
| 7 |
+
- **App Name:** Fast RAG Chatbot
|
| 8 |
+
- **LLM Provider:** groq (default)
|
| 9 |
+
- **Groq Model:** llama-3.1-8b-instant
|
| 10 |
+
- **HF Model:** meta-llama/Llama-3.1-8B-Instruct
|
| 11 |
+
- **Embedding Model:** BAAI/bge-small-en-v1.5
|
| 12 |
+
- **Docs Directory:** docs/
|
| 13 |
+
- **Index Directory:** data/index/
|
| 14 |
+
- **Sessions Directory:** data/sessions/
|
| 15 |
+
- **Chunk Size:** 420 tokens
|
| 16 |
+
- **Overlap:** 80 tokens
|
| 17 |
+
- **Top K:** 4
|
| 18 |
+
|
| 19 |
+
## Admin Credentials
|
| 20 |
+
- **Username:** martech_admin
|
| 21 |
+
- **Password:** martech_admin_303
|
| 22 |
+
- **OTP Expiry:** 300 seconds (5 minutes)
|
| 23 |
+
|
| 24 |
+
## Security
|
| 25 |
+
- **Auth Method:** HTTP Basic Auth + OTP (email-based)
|
| 26 |
+
- **Email for OTP:** randomjoedown@gmail.com
|
| 27 |
+
- **SMTP Server:** smtp.gmail.com (Port 465)
|
| 28 |
+
|
| 29 |
+
## API / Integration
|
| 30 |
+
- **Endpoint:** /api/chat
|
| 31 |
+
- **Widget Endpoint:** /widget
|
| 32 |
+
- **Admin Endpoint:** /admin
|
| 33 |
+
- **CORS:** Allowed for all (*) for testing
|
| 34 |
+
|
| 35 |
+
## Infrastructure
|
| 36 |
+
- **Hugging Face Path:** /data (for persistent storage)
|
| 37 |
+
- **Local Path:** data/ (for local storage)
|
| 38 |
+
- **Dockerfile:** Based on python:3.10-slim, serves on port 7860
|
CUSTOMIZATION_GUIDE.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Martechsol Assistant Customization Guide
|
| 2 |
+
|
| 3 |
+
This guide explains how to customize the visual appearance and behavior of your Gradio-based chat interface.
|
| 4 |
+
|
| 5 |
+
## 1. Modifying the UI Layout (app/ui_gradio.py)
|
| 6 |
+
|
| 7 |
+
The interface is built using `gradio.Blocks`. You can modify the structure in the `with gr.Blocks(...) as demo:` section.
|
| 8 |
+
|
| 9 |
+
### Common Components:
|
| 10 |
+
- **`chatbot`**: The main chat window. We use `elem_id="chatbot-window"` for CSS targeting.
|
| 11 |
+
- **`msg`**: The text input box.
|
| 12 |
+
- **`send`**: The primary action button.
|
| 13 |
+
|
| 14 |
+
## 2. Custom CSS (app/ui_gradio.py)
|
| 15 |
+
|
| 16 |
+
At the top of `app/ui_gradio.py`, there is a `custom_css` string. This is the most powerful way to change the look of your app.
|
| 17 |
+
|
| 18 |
+
### How to target elements:
|
| 19 |
+
We use `elem_id` in Python to give elements stable names.
|
| 20 |
+
|
| 21 |
+
Example:
|
| 22 |
+
```python
|
| 23 |
+
chatbot = gr.Chatbot(elem_id="my-custom-chat")
|
| 24 |
+
```
|
| 25 |
+
Then in CSS:
|
| 26 |
+
```css
|
| 27 |
+
#my-custom-chat {
|
| 28 |
+
background-color: #f0f0f0;
|
| 29 |
+
}
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### Important Selectors:
|
| 33 |
+
- **`footer`**: Targets the Gradio branding at the bottom.
|
| 34 |
+
- **`.gradio-container`**: The main outer wrapper of your app.
|
| 35 |
+
- **`[data-testid="user"]`**: Targets user messages.
|
| 36 |
+
- **`[data-testid="bot"]`**: Targets assistant messages.
|
| 37 |
+
|
| 38 |
+
## 3. Persistent Data on Hugging Face
|
| 39 |
+
|
| 40 |
+
Your data is stored in the `/data` volume.
|
| 41 |
+
- **Sessions**: `/data/sessions/`
|
| 42 |
+
- **FAISS Index**: `/data/index/`
|
| 43 |
+
|
| 44 |
+
To manage these, you can use the **Admin Panel** at `/admin`.
|
| 45 |
+
|
| 46 |
+
## 4. Tips for Widget Embedding
|
| 47 |
+
|
| 48 |
+
If you are embedding this as an iframe (like in `addon.html`):
|
| 49 |
+
1. **Remove redundant headers**: We hide the Markdown titles in the CSS (`#title-area { display: none; }`) so the iframe looks like a part of your page.
|
| 50 |
+
2. **Handle height**: We set `height: auto !important` and `min-height: 40px !important` on the chatbot so it doesn't take up 500px by default.
|
| 51 |
+
3. **Transparent Background**: You can add `body { background-color: transparent !important; }` to the `custom_css` if your theme supports it.
|
| 52 |
+
|
| 53 |
+
## 5. Deployment
|
| 54 |
+
|
| 55 |
+
Whenever you save a file, the changes are automatically pushed to your Hugging Face Space. The Space will rebuild automatically (takes about 1-2 minutes).
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Set environment variables
|
| 4 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 5 |
+
ENV PYTHONUNBUFFERED=1
|
| 6 |
+
ENV HOME=/home/user
|
| 7 |
+
|
| 8 |
+
# Create a non-root user for Hugging Face
|
| 9 |
+
RUN useradd -m -u 1000 user
|
| 10 |
+
WORKDIR $HOME/app
|
| 11 |
+
|
| 12 |
+
# Pre-create data directories with correct ownership
|
| 13 |
+
RUN mkdir -p $HOME/app/data/index $HOME/app/data/sessions && \
|
| 14 |
+
chown -R user:user $HOME/app
|
| 15 |
+
|
| 16 |
+
# Switch to non-root user
|
| 17 |
+
USER user
|
| 18 |
+
ENV PATH=$HOME/.local/bin:$PATH
|
| 19 |
+
|
| 20 |
+
# Install dependencies
|
| 21 |
+
COPY --chown=user requirements.txt .
|
| 22 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# Copy application code
|
| 25 |
+
COPY --chown=user . .
|
| 26 |
+
|
| 27 |
+
# Expose port
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Start application
|
| 31 |
+
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
PHPMailer/Exception.php
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* PHPMailer Exception class.
|
| 5 |
+
* PHP Version 5.5.
|
| 6 |
+
*
|
| 7 |
+
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
| 8 |
+
*
|
| 9 |
+
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
| 10 |
+
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
| 11 |
+
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
| 12 |
+
* @author Brent R. Matzelle (original founder)
|
| 13 |
+
* @copyright 2012 - 2020 Marcus Bointon
|
| 14 |
+
* @copyright 2010 - 2012 Jim Jagielski
|
| 15 |
+
* @copyright 2004 - 2009 Andy Prevost
|
| 16 |
+
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
|
| 17 |
+
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
| 18 |
+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
| 19 |
+
* FITNESS FOR A PARTICULAR PURPOSE.
|
| 20 |
+
*/
|
| 21 |
+
|
| 22 |
+
namespace PHPMailer\PHPMailer;
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* PHPMailer exception handler.
|
| 26 |
+
*
|
| 27 |
+
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
|
| 28 |
+
*/
|
| 29 |
+
class Exception extends \Exception
|
| 30 |
+
{
|
| 31 |
+
/**
|
| 32 |
+
* Prettify error message output.
|
| 33 |
+
*
|
| 34 |
+
* @return string
|
| 35 |
+
*/
|
| 36 |
+
public function errorMessage()
|
| 37 |
+
{
|
| 38 |
+
return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
|
| 39 |
+
}
|
| 40 |
+
}
|
PHPMailer/PHPMailer.php
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
PHPMailer/SMTP.php
ADDED
|
@@ -0,0 +1,1509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* PHPMailer RFC821 SMTP email transport class.
|
| 5 |
+
* PHP Version 5.5.
|
| 6 |
+
*
|
| 7 |
+
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
| 8 |
+
*
|
| 9 |
+
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
| 10 |
+
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
| 11 |
+
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
| 12 |
+
* @author Brent R. Matzelle (original founder)
|
| 13 |
+
* @copyright 2012 - 2020 Marcus Bointon
|
| 14 |
+
* @copyright 2010 - 2012 Jim Jagielski
|
| 15 |
+
* @copyright 2004 - 2009 Andy Prevost
|
| 16 |
+
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
|
| 17 |
+
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
| 18 |
+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
| 19 |
+
* FITNESS FOR A PARTICULAR PURPOSE.
|
| 20 |
+
*/
|
| 21 |
+
|
| 22 |
+
namespace PHPMailer\PHPMailer;
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* PHPMailer RFC821 SMTP email transport class.
|
| 26 |
+
* Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
|
| 27 |
+
*
|
| 28 |
+
* @author Chris Ryan
|
| 29 |
+
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
|
| 30 |
+
*/
|
| 31 |
+
class SMTP
|
| 32 |
+
{
|
| 33 |
+
/**
|
| 34 |
+
* The PHPMailer SMTP version number.
|
| 35 |
+
*
|
| 36 |
+
* @var string
|
| 37 |
+
*/
|
| 38 |
+
const VERSION = '6.9.3';
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* SMTP line break constant.
|
| 42 |
+
*
|
| 43 |
+
* @var string
|
| 44 |
+
*/
|
| 45 |
+
const LE = "\r\n";
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* The SMTP port to use if one is not specified.
|
| 49 |
+
*
|
| 50 |
+
* @var int
|
| 51 |
+
*/
|
| 52 |
+
const DEFAULT_PORT = 25;
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* The SMTPs port to use if one is not specified.
|
| 56 |
+
*
|
| 57 |
+
* @var int
|
| 58 |
+
*/
|
| 59 |
+
const DEFAULT_SECURE_PORT = 465;
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
|
| 63 |
+
* *excluding* a trailing CRLF break.
|
| 64 |
+
*
|
| 65 |
+
* @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.6
|
| 66 |
+
*
|
| 67 |
+
* @var int
|
| 68 |
+
*/
|
| 69 |
+
const MAX_LINE_LENGTH = 998;
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
|
| 73 |
+
* *including* a trailing CRLF line break.
|
| 74 |
+
*
|
| 75 |
+
* @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.5
|
| 76 |
+
*
|
| 77 |
+
* @var int
|
| 78 |
+
*/
|
| 79 |
+
const MAX_REPLY_LENGTH = 512;
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Debug level for no output.
|
| 83 |
+
*
|
| 84 |
+
* @var int
|
| 85 |
+
*/
|
| 86 |
+
const DEBUG_OFF = 0;
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Debug level to show client -> server messages.
|
| 90 |
+
*
|
| 91 |
+
* @var int
|
| 92 |
+
*/
|
| 93 |
+
const DEBUG_CLIENT = 1;
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Debug level to show client -> server and server -> client messages.
|
| 97 |
+
*
|
| 98 |
+
* @var int
|
| 99 |
+
*/
|
| 100 |
+
const DEBUG_SERVER = 2;
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Debug level to show connection status, client -> server and server -> client messages.
|
| 104 |
+
*
|
| 105 |
+
* @var int
|
| 106 |
+
*/
|
| 107 |
+
const DEBUG_CONNECTION = 3;
|
| 108 |
+
|
| 109 |
+
/**
|
| 110 |
+
* Debug level to show all messages.
|
| 111 |
+
*
|
| 112 |
+
* @var int
|
| 113 |
+
*/
|
| 114 |
+
const DEBUG_LOWLEVEL = 4;
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* Debug output level.
|
| 118 |
+
* Options:
|
| 119 |
+
* * self::DEBUG_OFF (`0`) No debug output, default
|
| 120 |
+
* * self::DEBUG_CLIENT (`1`) Client commands
|
| 121 |
+
* * self::DEBUG_SERVER (`2`) Client commands and server responses
|
| 122 |
+
* * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
|
| 123 |
+
* * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
|
| 124 |
+
*
|
| 125 |
+
* @var int
|
| 126 |
+
*/
|
| 127 |
+
public $do_debug = self::DEBUG_OFF;
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* How to handle debug output.
|
| 131 |
+
* Options:
|
| 132 |
+
* * `echo` Output plain-text as-is, appropriate for CLI
|
| 133 |
+
* * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
|
| 134 |
+
* * `error_log` Output to error log as configured in php.ini
|
| 135 |
+
* Alternatively, you can provide a callable expecting two params: a message string and the debug level:
|
| 136 |
+
*
|
| 137 |
+
* ```php
|
| 138 |
+
* $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
|
| 139 |
+
* ```
|
| 140 |
+
*
|
| 141 |
+
* Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
|
| 142 |
+
* level output is used:
|
| 143 |
+
*
|
| 144 |
+
* ```php
|
| 145 |
+
* $mail->Debugoutput = new myPsr3Logger;
|
| 146 |
+
* ```
|
| 147 |
+
*
|
| 148 |
+
* @var string|callable|\Psr\Log\LoggerInterface
|
| 149 |
+
*/
|
| 150 |
+
public $Debugoutput = 'echo';
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* Whether to use VERP.
|
| 154 |
+
*
|
| 155 |
+
* @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
|
| 156 |
+
* @see https://www.postfix.org/VERP_README.html Info on VERP
|
| 157 |
+
*
|
| 158 |
+
* @var bool
|
| 159 |
+
*/
|
| 160 |
+
public $do_verp = false;
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* The timeout value for connection, in seconds.
|
| 164 |
+
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
|
| 165 |
+
* This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
|
| 166 |
+
*
|
| 167 |
+
* @see https://www.rfc-editor.org/rfc/rfc2821#section-4.5.3.2
|
| 168 |
+
*
|
| 169 |
+
* @var int
|
| 170 |
+
*/
|
| 171 |
+
public $Timeout = 300;
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* How long to wait for commands to complete, in seconds.
|
| 175 |
+
* Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
|
| 176 |
+
*
|
| 177 |
+
* @var int
|
| 178 |
+
*/
|
| 179 |
+
public $Timelimit = 300;
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Patterns to extract an SMTP transaction id from reply to a DATA command.
|
| 183 |
+
* The first capture group in each regex will be used as the ID.
|
| 184 |
+
* MS ESMTP returns the message ID, which may not be correct for internal tracking.
|
| 185 |
+
*
|
| 186 |
+
* @var string[]
|
| 187 |
+
*/
|
| 188 |
+
protected $smtp_transaction_id_patterns = [
|
| 189 |
+
'exim' => '/[\d]{3} OK id=(.*)/',
|
| 190 |
+
'sendmail' => '/[\d]{3} 2\.0\.0 (.*) Message/',
|
| 191 |
+
'postfix' => '/[\d]{3} 2\.0\.0 Ok: queued as (.*)/',
|
| 192 |
+
'Microsoft_ESMTP' => '/[0-9]{3} 2\.[\d]\.0 (.*)@(?:.*) Queued mail for delivery/',
|
| 193 |
+
'Amazon_SES' => '/[\d]{3} Ok (.*)/',
|
| 194 |
+
'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
|
| 195 |
+
'CampaignMonitor' => '/[\d]{3} 2\.0\.0 OK:([a-zA-Z\d]{48})/',
|
| 196 |
+
'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
|
| 197 |
+
'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
|
| 198 |
+
'Mailjet' => '/[\d]{3} OK queued as (.*)/',
|
| 199 |
+
];
|
| 200 |
+
|
| 201 |
+
/**
|
| 202 |
+
* Allowed SMTP XCLIENT attributes.
|
| 203 |
+
* Must be allowed by the SMTP server. EHLO response is not checked.
|
| 204 |
+
*
|
| 205 |
+
* @see https://www.postfix.org/XCLIENT_README.html
|
| 206 |
+
*
|
| 207 |
+
* @var array
|
| 208 |
+
*/
|
| 209 |
+
public static $xclient_allowed_attributes = [
|
| 210 |
+
'NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN', 'DESTADDR', 'DESTPORT'
|
| 211 |
+
];
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* The last transaction ID issued in response to a DATA command,
|
| 215 |
+
* if one was detected.
|
| 216 |
+
*
|
| 217 |
+
* @var string|bool|null
|
| 218 |
+
*/
|
| 219 |
+
protected $last_smtp_transaction_id;
|
| 220 |
+
|
| 221 |
+
/**
|
| 222 |
+
* The socket for the server connection.
|
| 223 |
+
*
|
| 224 |
+
* @var ?resource
|
| 225 |
+
*/
|
| 226 |
+
protected $smtp_conn;
|
| 227 |
+
|
| 228 |
+
/**
|
| 229 |
+
* Error information, if any, for the last SMTP command.
|
| 230 |
+
*
|
| 231 |
+
* @var array
|
| 232 |
+
*/
|
| 233 |
+
protected $error = [
|
| 234 |
+
'error' => '',
|
| 235 |
+
'detail' => '',
|
| 236 |
+
'smtp_code' => '',
|
| 237 |
+
'smtp_code_ex' => '',
|
| 238 |
+
];
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* The reply the server sent to us for HELO.
|
| 242 |
+
* If null, no HELO string has yet been received.
|
| 243 |
+
*
|
| 244 |
+
* @var string|null
|
| 245 |
+
*/
|
| 246 |
+
protected $helo_rply;
|
| 247 |
+
|
| 248 |
+
/**
|
| 249 |
+
* The set of SMTP extensions sent in reply to EHLO command.
|
| 250 |
+
* Indexes of the array are extension names.
|
| 251 |
+
* Value at index 'HELO' or 'EHLO' (according to command that was sent)
|
| 252 |
+
* represents the server name. In case of HELO it is the only element of the array.
|
| 253 |
+
* Other values can be boolean TRUE or an array containing extension options.
|
| 254 |
+
* If null, no HELO/EHLO string has yet been received.
|
| 255 |
+
*
|
| 256 |
+
* @var array|null
|
| 257 |
+
*/
|
| 258 |
+
protected $server_caps;
|
| 259 |
+
|
| 260 |
+
/**
|
| 261 |
+
* The most recent reply received from the server.
|
| 262 |
+
*
|
| 263 |
+
* @var string
|
| 264 |
+
*/
|
| 265 |
+
protected $last_reply = '';
|
| 266 |
+
|
| 267 |
+
/**
|
| 268 |
+
* Output debugging info via a user-selected method.
|
| 269 |
+
*
|
| 270 |
+
* @param string $str Debug string to output
|
| 271 |
+
* @param int $level The debug level of this message; see DEBUG_* constants
|
| 272 |
+
*
|
| 273 |
+
* @see SMTP::$Debugoutput
|
| 274 |
+
* @see SMTP::$do_debug
|
| 275 |
+
*/
|
| 276 |
+
protected function edebug($str, $level = 0)
|
| 277 |
+
{
|
| 278 |
+
if ($level > $this->do_debug) {
|
| 279 |
+
return;
|
| 280 |
+
}
|
| 281 |
+
//Is this a PSR-3 logger?
|
| 282 |
+
if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
|
| 283 |
+
//Remove trailing line breaks potentially added by calls to SMTP::client_send()
|
| 284 |
+
$this->Debugoutput->debug(rtrim($str, "\r\n"));
|
| 285 |
+
|
| 286 |
+
return;
|
| 287 |
+
}
|
| 288 |
+
//Avoid clash with built-in function names
|
| 289 |
+
if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
|
| 290 |
+
call_user_func($this->Debugoutput, $str, $level);
|
| 291 |
+
|
| 292 |
+
return;
|
| 293 |
+
}
|
| 294 |
+
switch ($this->Debugoutput) {
|
| 295 |
+
case 'error_log':
|
| 296 |
+
//Don't output, just log
|
| 297 |
+
/** @noinspection ForgottenDebugOutputInspection */
|
| 298 |
+
error_log($str);
|
| 299 |
+
break;
|
| 300 |
+
case 'html':
|
| 301 |
+
//Cleans up output a bit for a better looking, HTML-safe output
|
| 302 |
+
echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
|
| 303 |
+
preg_replace('/[\r\n]+/', '', $str),
|
| 304 |
+
ENT_QUOTES,
|
| 305 |
+
'UTF-8'
|
| 306 |
+
), "<br>\n";
|
| 307 |
+
break;
|
| 308 |
+
case 'echo':
|
| 309 |
+
default:
|
| 310 |
+
//Normalize line breaks
|
| 311 |
+
$str = preg_replace('/\r\n|\r/m', "\n", $str);
|
| 312 |
+
echo gmdate('Y-m-d H:i:s'),
|
| 313 |
+
"\t",
|
| 314 |
+
//Trim trailing space
|
| 315 |
+
trim(
|
| 316 |
+
//Indent for readability, except for trailing break
|
| 317 |
+
str_replace(
|
| 318 |
+
"\n",
|
| 319 |
+
"\n \t ",
|
| 320 |
+
trim($str)
|
| 321 |
+
)
|
| 322 |
+
),
|
| 323 |
+
"\n";
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/**
|
| 328 |
+
* Connect to an SMTP server.
|
| 329 |
+
*
|
| 330 |
+
* @param string $host SMTP server IP or host name
|
| 331 |
+
* @param int $port The port number to connect to
|
| 332 |
+
* @param int $timeout How long to wait for the connection to open
|
| 333 |
+
* @param array $options An array of options for stream_context_create()
|
| 334 |
+
*
|
| 335 |
+
* @return bool
|
| 336 |
+
*/
|
| 337 |
+
public function connect($host, $port = null, $timeout = 30, $options = [])
|
| 338 |
+
{
|
| 339 |
+
//Clear errors to avoid confusion
|
| 340 |
+
$this->setError('');
|
| 341 |
+
//Make sure we are __not__ connected
|
| 342 |
+
if ($this->connected()) {
|
| 343 |
+
//Already connected, generate error
|
| 344 |
+
$this->setError('Already connected to a server');
|
| 345 |
+
|
| 346 |
+
return false;
|
| 347 |
+
}
|
| 348 |
+
if (empty($port)) {
|
| 349 |
+
$port = self::DEFAULT_PORT;
|
| 350 |
+
}
|
| 351 |
+
//Connect to the SMTP server
|
| 352 |
+
$this->edebug(
|
| 353 |
+
"Connection: opening to $host:$port, timeout=$timeout, options=" .
|
| 354 |
+
(count($options) > 0 ? var_export($options, true) : 'array()'),
|
| 355 |
+
self::DEBUG_CONNECTION
|
| 356 |
+
);
|
| 357 |
+
|
| 358 |
+
$this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
|
| 359 |
+
|
| 360 |
+
if ($this->smtp_conn === false) {
|
| 361 |
+
//Error info already set inside `getSMTPConnection()`
|
| 362 |
+
return false;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
$this->edebug('Connection: opened', self::DEBUG_CONNECTION);
|
| 366 |
+
|
| 367 |
+
//Get any announcement
|
| 368 |
+
$this->last_reply = $this->get_lines();
|
| 369 |
+
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
|
| 370 |
+
$responseCode = (int)substr($this->last_reply, 0, 3);
|
| 371 |
+
if ($responseCode === 220) {
|
| 372 |
+
return true;
|
| 373 |
+
}
|
| 374 |
+
//Anything other than a 220 response means something went wrong
|
| 375 |
+
//RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
|
| 376 |
+
//https://www.rfc-editor.org/rfc/rfc5321#section-3.1
|
| 377 |
+
if ($responseCode === 554) {
|
| 378 |
+
$this->quit();
|
| 379 |
+
}
|
| 380 |
+
//This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
|
| 381 |
+
$this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
|
| 382 |
+
$this->close();
|
| 383 |
+
return false;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
/**
|
| 387 |
+
* Create connection to the SMTP server.
|
| 388 |
+
*
|
| 389 |
+
* @param string $host SMTP server IP or host name
|
| 390 |
+
* @param int $port The port number to connect to
|
| 391 |
+
* @param int $timeout How long to wait for the connection to open
|
| 392 |
+
* @param array $options An array of options for stream_context_create()
|
| 393 |
+
*
|
| 394 |
+
* @return false|resource
|
| 395 |
+
*/
|
| 396 |
+
protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
|
| 397 |
+
{
|
| 398 |
+
static $streamok;
|
| 399 |
+
//This is enabled by default since 5.0.0 but some providers disable it
|
| 400 |
+
//Check this once and cache the result
|
| 401 |
+
if (null === $streamok) {
|
| 402 |
+
$streamok = function_exists('stream_socket_client');
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
$errno = 0;
|
| 406 |
+
$errstr = '';
|
| 407 |
+
if ($streamok) {
|
| 408 |
+
$socket_context = stream_context_create($options);
|
| 409 |
+
set_error_handler(function () {
|
| 410 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 411 |
+
});
|
| 412 |
+
$connection = stream_socket_client(
|
| 413 |
+
$host . ':' . $port,
|
| 414 |
+
$errno,
|
| 415 |
+
$errstr,
|
| 416 |
+
$timeout,
|
| 417 |
+
STREAM_CLIENT_CONNECT,
|
| 418 |
+
$socket_context
|
| 419 |
+
);
|
| 420 |
+
} else {
|
| 421 |
+
//Fall back to fsockopen which should work in more places, but is missing some features
|
| 422 |
+
$this->edebug(
|
| 423 |
+
'Connection: stream_socket_client not available, falling back to fsockopen',
|
| 424 |
+
self::DEBUG_CONNECTION
|
| 425 |
+
);
|
| 426 |
+
set_error_handler(function () {
|
| 427 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 428 |
+
});
|
| 429 |
+
$connection = fsockopen(
|
| 430 |
+
$host,
|
| 431 |
+
$port,
|
| 432 |
+
$errno,
|
| 433 |
+
$errstr,
|
| 434 |
+
$timeout
|
| 435 |
+
);
|
| 436 |
+
}
|
| 437 |
+
restore_error_handler();
|
| 438 |
+
|
| 439 |
+
//Verify we connected properly
|
| 440 |
+
if (!is_resource($connection)) {
|
| 441 |
+
$this->setError(
|
| 442 |
+
'Failed to connect to server',
|
| 443 |
+
'',
|
| 444 |
+
(string) $errno,
|
| 445 |
+
$errstr
|
| 446 |
+
);
|
| 447 |
+
$this->edebug(
|
| 448 |
+
'SMTP ERROR: ' . $this->error['error']
|
| 449 |
+
. ": $errstr ($errno)",
|
| 450 |
+
self::DEBUG_CLIENT
|
| 451 |
+
);
|
| 452 |
+
|
| 453 |
+
return false;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
//SMTP server can take longer to respond, give longer timeout for first read
|
| 457 |
+
//Windows does not have support for this timeout function
|
| 458 |
+
if (strpos(PHP_OS, 'WIN') !== 0) {
|
| 459 |
+
$max = (int)ini_get('max_execution_time');
|
| 460 |
+
//Don't bother if unlimited, or if set_time_limit is disabled
|
| 461 |
+
if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
|
| 462 |
+
@set_time_limit($timeout);
|
| 463 |
+
}
|
| 464 |
+
stream_set_timeout($connection, $timeout, 0);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
return $connection;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
/**
|
| 471 |
+
* Initiate a TLS (encrypted) session.
|
| 472 |
+
*
|
| 473 |
+
* @return bool
|
| 474 |
+
*/
|
| 475 |
+
public function startTLS()
|
| 476 |
+
{
|
| 477 |
+
if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
|
| 478 |
+
return false;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
//Allow the best TLS version(s) we can
|
| 482 |
+
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
|
| 483 |
+
|
| 484 |
+
//PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
|
| 485 |
+
//so add them back in manually if we can
|
| 486 |
+
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
|
| 487 |
+
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
|
| 488 |
+
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
//Begin encrypted connection
|
| 492 |
+
set_error_handler(function () {
|
| 493 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 494 |
+
});
|
| 495 |
+
$crypto_ok = stream_socket_enable_crypto(
|
| 496 |
+
$this->smtp_conn,
|
| 497 |
+
true,
|
| 498 |
+
$crypto_method
|
| 499 |
+
);
|
| 500 |
+
restore_error_handler();
|
| 501 |
+
|
| 502 |
+
return (bool) $crypto_ok;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/**
|
| 506 |
+
* Perform SMTP authentication.
|
| 507 |
+
* Must be run after hello().
|
| 508 |
+
*
|
| 509 |
+
* @see hello()
|
| 510 |
+
*
|
| 511 |
+
* @param string $username The user name
|
| 512 |
+
* @param string $password The password
|
| 513 |
+
* @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
|
| 514 |
+
* @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
|
| 515 |
+
*
|
| 516 |
+
* @return bool True if successfully authenticated
|
| 517 |
+
*/
|
| 518 |
+
public function authenticate(
|
| 519 |
+
$username,
|
| 520 |
+
$password,
|
| 521 |
+
$authtype = null,
|
| 522 |
+
$OAuth = null
|
| 523 |
+
) {
|
| 524 |
+
if (!$this->server_caps) {
|
| 525 |
+
$this->setError('Authentication is not allowed before HELO/EHLO');
|
| 526 |
+
|
| 527 |
+
return false;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
if (array_key_exists('EHLO', $this->server_caps)) {
|
| 531 |
+
//SMTP extensions are available; try to find a proper authentication method
|
| 532 |
+
if (!array_key_exists('AUTH', $this->server_caps)) {
|
| 533 |
+
$this->setError('Authentication is not allowed at this stage');
|
| 534 |
+
//'at this stage' means that auth may be allowed after the stage changes
|
| 535 |
+
//e.g. after STARTTLS
|
| 536 |
+
|
| 537 |
+
return false;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
$this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
|
| 541 |
+
$this->edebug(
|
| 542 |
+
'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
|
| 543 |
+
self::DEBUG_LOWLEVEL
|
| 544 |
+
);
|
| 545 |
+
|
| 546 |
+
//If we have requested a specific auth type, check the server supports it before trying others
|
| 547 |
+
if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
|
| 548 |
+
$this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
|
| 549 |
+
$authtype = null;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
if (empty($authtype)) {
|
| 553 |
+
//If no auth mechanism is specified, attempt to use these, in this order
|
| 554 |
+
//Try CRAM-MD5 first as it's more secure than the others
|
| 555 |
+
foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
|
| 556 |
+
if (in_array($method, $this->server_caps['AUTH'], true)) {
|
| 557 |
+
$authtype = $method;
|
| 558 |
+
break;
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
if (empty($authtype)) {
|
| 562 |
+
$this->setError('No supported authentication methods found');
|
| 563 |
+
|
| 564 |
+
return false;
|
| 565 |
+
}
|
| 566 |
+
$this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
|
| 570 |
+
$this->setError("The requested authentication method \"$authtype\" is not supported by the server");
|
| 571 |
+
|
| 572 |
+
return false;
|
| 573 |
+
}
|
| 574 |
+
} elseif (empty($authtype)) {
|
| 575 |
+
$authtype = 'LOGIN';
|
| 576 |
+
}
|
| 577 |
+
switch ($authtype) {
|
| 578 |
+
case 'PLAIN':
|
| 579 |
+
//Start authentication
|
| 580 |
+
if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
|
| 581 |
+
return false;
|
| 582 |
+
}
|
| 583 |
+
//Send encoded username and password
|
| 584 |
+
if (
|
| 585 |
+
//Format from https://www.rfc-editor.org/rfc/rfc4616#section-2
|
| 586 |
+
//We skip the first field (it's forgery), so the string starts with a null byte
|
| 587 |
+
!$this->sendCommand(
|
| 588 |
+
'User & Password',
|
| 589 |
+
base64_encode("\0" . $username . "\0" . $password),
|
| 590 |
+
235
|
| 591 |
+
)
|
| 592 |
+
) {
|
| 593 |
+
return false;
|
| 594 |
+
}
|
| 595 |
+
break;
|
| 596 |
+
case 'LOGIN':
|
| 597 |
+
//Start authentication
|
| 598 |
+
if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
|
| 599 |
+
return false;
|
| 600 |
+
}
|
| 601 |
+
if (!$this->sendCommand('Username', base64_encode($username), 334)) {
|
| 602 |
+
return false;
|
| 603 |
+
}
|
| 604 |
+
if (!$this->sendCommand('Password', base64_encode($password), 235)) {
|
| 605 |
+
return false;
|
| 606 |
+
}
|
| 607 |
+
break;
|
| 608 |
+
case 'CRAM-MD5':
|
| 609 |
+
//Start authentication
|
| 610 |
+
if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
|
| 611 |
+
return false;
|
| 612 |
+
}
|
| 613 |
+
//Get the challenge
|
| 614 |
+
$challenge = base64_decode(substr($this->last_reply, 4));
|
| 615 |
+
|
| 616 |
+
//Build the response
|
| 617 |
+
$response = $username . ' ' . $this->hmac($challenge, $password);
|
| 618 |
+
|
| 619 |
+
//send encoded credentials
|
| 620 |
+
return $this->sendCommand('Username', base64_encode($response), 235);
|
| 621 |
+
case 'XOAUTH2':
|
| 622 |
+
//The OAuth instance must be set up prior to requesting auth.
|
| 623 |
+
if (null === $OAuth) {
|
| 624 |
+
return false;
|
| 625 |
+
}
|
| 626 |
+
$oauth = $OAuth->getOauth64();
|
| 627 |
+
|
| 628 |
+
//Start authentication
|
| 629 |
+
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
|
| 630 |
+
return false;
|
| 631 |
+
}
|
| 632 |
+
break;
|
| 633 |
+
default:
|
| 634 |
+
$this->setError("Authentication method \"$authtype\" is not supported");
|
| 635 |
+
|
| 636 |
+
return false;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
return true;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
/**
|
| 643 |
+
* Calculate an MD5 HMAC hash.
|
| 644 |
+
* Works like hash_hmac('md5', $data, $key)
|
| 645 |
+
* in case that function is not available.
|
| 646 |
+
*
|
| 647 |
+
* @param string $data The data to hash
|
| 648 |
+
* @param string $key The key to hash with
|
| 649 |
+
*
|
| 650 |
+
* @return string
|
| 651 |
+
*/
|
| 652 |
+
protected function hmac($data, $key)
|
| 653 |
+
{
|
| 654 |
+
if (function_exists('hash_hmac')) {
|
| 655 |
+
return hash_hmac('md5', $data, $key);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
//The following borrowed from
|
| 659 |
+
//https://www.php.net/manual/en/function.mhash.php#27225
|
| 660 |
+
|
| 661 |
+
//RFC 2104 HMAC implementation for php.
|
| 662 |
+
//Creates an md5 HMAC.
|
| 663 |
+
//Eliminates the need to install mhash to compute a HMAC
|
| 664 |
+
//by Lance Rushing
|
| 665 |
+
|
| 666 |
+
$bytelen = 64; //byte length for md5
|
| 667 |
+
if (strlen($key) > $bytelen) {
|
| 668 |
+
$key = pack('H*', md5($key));
|
| 669 |
+
}
|
| 670 |
+
$key = str_pad($key, $bytelen, chr(0x00));
|
| 671 |
+
$ipad = str_pad('', $bytelen, chr(0x36));
|
| 672 |
+
$opad = str_pad('', $bytelen, chr(0x5c));
|
| 673 |
+
$k_ipad = $key ^ $ipad;
|
| 674 |
+
$k_opad = $key ^ $opad;
|
| 675 |
+
|
| 676 |
+
return md5($k_opad . pack('H*', md5($k_ipad . $data)));
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
/**
|
| 680 |
+
* Check connection state.
|
| 681 |
+
*
|
| 682 |
+
* @return bool True if connected
|
| 683 |
+
*/
|
| 684 |
+
public function connected()
|
| 685 |
+
{
|
| 686 |
+
if (is_resource($this->smtp_conn)) {
|
| 687 |
+
$sock_status = stream_get_meta_data($this->smtp_conn);
|
| 688 |
+
if ($sock_status['eof']) {
|
| 689 |
+
//The socket is valid but we are not connected
|
| 690 |
+
$this->edebug(
|
| 691 |
+
'SMTP NOTICE: EOF caught while checking if connected',
|
| 692 |
+
self::DEBUG_CLIENT
|
| 693 |
+
);
|
| 694 |
+
$this->close();
|
| 695 |
+
|
| 696 |
+
return false;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
return true; //everything looks good
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
return false;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
/**
|
| 706 |
+
* Close the socket and clean up the state of the class.
|
| 707 |
+
* Don't use this function without first trying to use QUIT.
|
| 708 |
+
*
|
| 709 |
+
* @see quit()
|
| 710 |
+
*/
|
| 711 |
+
public function close()
|
| 712 |
+
{
|
| 713 |
+
$this->server_caps = null;
|
| 714 |
+
$this->helo_rply = null;
|
| 715 |
+
if (is_resource($this->smtp_conn)) {
|
| 716 |
+
//Close the connection and cleanup
|
| 717 |
+
fclose($this->smtp_conn);
|
| 718 |
+
$this->smtp_conn = null; //Makes for cleaner serialization
|
| 719 |
+
$this->edebug('Connection: closed', self::DEBUG_CONNECTION);
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
/**
|
| 724 |
+
* Send an SMTP DATA command.
|
| 725 |
+
* Issues a data command and sends the msg_data to the server,
|
| 726 |
+
* finalizing the mail transaction. $msg_data is the message
|
| 727 |
+
* that is to be sent with the headers. Each header needs to be
|
| 728 |
+
* on a single line followed by a <CRLF> with the message headers
|
| 729 |
+
* and the message body being separated by an additional <CRLF>.
|
| 730 |
+
* Implements RFC 821: DATA <CRLF>.
|
| 731 |
+
*
|
| 732 |
+
* @param string $msg_data Message data to send
|
| 733 |
+
*
|
| 734 |
+
* @return bool
|
| 735 |
+
*/
|
| 736 |
+
public function data($msg_data)
|
| 737 |
+
{
|
| 738 |
+
//This will use the standard timelimit
|
| 739 |
+
if (!$this->sendCommand('DATA', 'DATA', 354)) {
|
| 740 |
+
return false;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
/* The server is ready to accept data!
|
| 744 |
+
* According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
|
| 745 |
+
* so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
|
| 746 |
+
* smaller lines to fit within the limit.
|
| 747 |
+
* We will also look for lines that start with a '.' and prepend an additional '.'.
|
| 748 |
+
* NOTE: this does not count towards line-length limit.
|
| 749 |
+
*/
|
| 750 |
+
|
| 751 |
+
//Normalize line breaks before exploding
|
| 752 |
+
$lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
|
| 753 |
+
|
| 754 |
+
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
|
| 755 |
+
* of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
|
| 756 |
+
* process all lines before a blank line as headers.
|
| 757 |
+
*/
|
| 758 |
+
|
| 759 |
+
$field = substr($lines[0], 0, strpos($lines[0], ':'));
|
| 760 |
+
$in_headers = false;
|
| 761 |
+
if (!empty($field) && strpos($field, ' ') === false) {
|
| 762 |
+
$in_headers = true;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
foreach ($lines as $line) {
|
| 766 |
+
$lines_out = [];
|
| 767 |
+
if ($in_headers && $line === '') {
|
| 768 |
+
$in_headers = false;
|
| 769 |
+
}
|
| 770 |
+
//Break this line up into several smaller lines if it's too long
|
| 771 |
+
//Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
|
| 772 |
+
while (isset($line[self::MAX_LINE_LENGTH])) {
|
| 773 |
+
//Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
|
| 774 |
+
//so as to avoid breaking in the middle of a word
|
| 775 |
+
$pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
|
| 776 |
+
//Deliberately matches both false and 0
|
| 777 |
+
if (!$pos) {
|
| 778 |
+
//No nice break found, add a hard break
|
| 779 |
+
$pos = self::MAX_LINE_LENGTH - 1;
|
| 780 |
+
$lines_out[] = substr($line, 0, $pos);
|
| 781 |
+
$line = substr($line, $pos);
|
| 782 |
+
} else {
|
| 783 |
+
//Break at the found point
|
| 784 |
+
$lines_out[] = substr($line, 0, $pos);
|
| 785 |
+
//Move along by the amount we dealt with
|
| 786 |
+
$line = substr($line, $pos + 1);
|
| 787 |
+
}
|
| 788 |
+
//If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
|
| 789 |
+
if ($in_headers) {
|
| 790 |
+
$line = "\t" . $line;
|
| 791 |
+
}
|
| 792 |
+
}
|
| 793 |
+
$lines_out[] = $line;
|
| 794 |
+
|
| 795 |
+
//Send the lines to the server
|
| 796 |
+
foreach ($lines_out as $line_out) {
|
| 797 |
+
//Dot-stuffing as per RFC5321 section 4.5.2
|
| 798 |
+
//https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
|
| 799 |
+
if (!empty($line_out) && $line_out[0] === '.') {
|
| 800 |
+
$line_out = '.' . $line_out;
|
| 801 |
+
}
|
| 802 |
+
$this->client_send($line_out . static::LE, 'DATA');
|
| 803 |
+
}
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
//Message data has been sent, complete the command
|
| 807 |
+
//Increase timelimit for end of DATA command
|
| 808 |
+
$savetimelimit = $this->Timelimit;
|
| 809 |
+
$this->Timelimit *= 2;
|
| 810 |
+
$result = $this->sendCommand('DATA END', '.', 250);
|
| 811 |
+
$this->recordLastTransactionID();
|
| 812 |
+
//Restore timelimit
|
| 813 |
+
$this->Timelimit = $savetimelimit;
|
| 814 |
+
|
| 815 |
+
return $result;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
/**
|
| 819 |
+
* Send an SMTP HELO or EHLO command.
|
| 820 |
+
* Used to identify the sending server to the receiving server.
|
| 821 |
+
* This makes sure that client and server are in a known state.
|
| 822 |
+
* Implements RFC 821: HELO <SP> <domain> <CRLF>
|
| 823 |
+
* and RFC 2821 EHLO.
|
| 824 |
+
*
|
| 825 |
+
* @param string $host The host name or IP to connect to
|
| 826 |
+
*
|
| 827 |
+
* @return bool
|
| 828 |
+
*/
|
| 829 |
+
public function hello($host = '')
|
| 830 |
+
{
|
| 831 |
+
//Try extended hello first (RFC 2821)
|
| 832 |
+
if ($this->sendHello('EHLO', $host)) {
|
| 833 |
+
return true;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
//Some servers shut down the SMTP service here (RFC 5321)
|
| 837 |
+
if (substr($this->helo_rply, 0, 3) == '421') {
|
| 838 |
+
return false;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
return $this->sendHello('HELO', $host);
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
/**
|
| 845 |
+
* Send an SMTP HELO or EHLO command.
|
| 846 |
+
* Low-level implementation used by hello().
|
| 847 |
+
*
|
| 848 |
+
* @param string $hello The HELO string
|
| 849 |
+
* @param string $host The hostname to say we are
|
| 850 |
+
*
|
| 851 |
+
* @return bool
|
| 852 |
+
*
|
| 853 |
+
* @see hello()
|
| 854 |
+
*/
|
| 855 |
+
protected function sendHello($hello, $host)
|
| 856 |
+
{
|
| 857 |
+
$noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
|
| 858 |
+
$this->helo_rply = $this->last_reply;
|
| 859 |
+
if ($noerror) {
|
| 860 |
+
$this->parseHelloFields($hello);
|
| 861 |
+
} else {
|
| 862 |
+
$this->server_caps = null;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
return $noerror;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
/**
|
| 869 |
+
* Parse a reply to HELO/EHLO command to discover server extensions.
|
| 870 |
+
* In case of HELO, the only parameter that can be discovered is a server name.
|
| 871 |
+
*
|
| 872 |
+
* @param string $type `HELO` or `EHLO`
|
| 873 |
+
*/
|
| 874 |
+
protected function parseHelloFields($type)
|
| 875 |
+
{
|
| 876 |
+
$this->server_caps = [];
|
| 877 |
+
$lines = explode("\n", $this->helo_rply);
|
| 878 |
+
|
| 879 |
+
foreach ($lines as $n => $s) {
|
| 880 |
+
//First 4 chars contain response code followed by - or space
|
| 881 |
+
$s = trim(substr($s, 4));
|
| 882 |
+
if (empty($s)) {
|
| 883 |
+
continue;
|
| 884 |
+
}
|
| 885 |
+
$fields = explode(' ', $s);
|
| 886 |
+
if (!empty($fields)) {
|
| 887 |
+
if (!$n) {
|
| 888 |
+
$name = $type;
|
| 889 |
+
$fields = $fields[0];
|
| 890 |
+
} else {
|
| 891 |
+
$name = array_shift($fields);
|
| 892 |
+
switch ($name) {
|
| 893 |
+
case 'SIZE':
|
| 894 |
+
$fields = ($fields ? $fields[0] : 0);
|
| 895 |
+
break;
|
| 896 |
+
case 'AUTH':
|
| 897 |
+
if (!is_array($fields)) {
|
| 898 |
+
$fields = [];
|
| 899 |
+
}
|
| 900 |
+
break;
|
| 901 |
+
default:
|
| 902 |
+
$fields = true;
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
$this->server_caps[$name] = $fields;
|
| 906 |
+
}
|
| 907 |
+
}
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
/**
|
| 911 |
+
* Send an SMTP MAIL command.
|
| 912 |
+
* Starts a mail transaction from the email address specified in
|
| 913 |
+
* $from. Returns true if successful or false otherwise. If True
|
| 914 |
+
* the mail transaction is started and then one or more recipient
|
| 915 |
+
* commands may be called followed by a data command.
|
| 916 |
+
* Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
|
| 917 |
+
*
|
| 918 |
+
* @param string $from Source address of this message
|
| 919 |
+
*
|
| 920 |
+
* @return bool
|
| 921 |
+
*/
|
| 922 |
+
public function mail($from)
|
| 923 |
+
{
|
| 924 |
+
$useVerp = ($this->do_verp ? ' XVERP' : '');
|
| 925 |
+
|
| 926 |
+
return $this->sendCommand(
|
| 927 |
+
'MAIL FROM',
|
| 928 |
+
'MAIL FROM:<' . $from . '>' . $useVerp,
|
| 929 |
+
250
|
| 930 |
+
);
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
/**
|
| 934 |
+
* Send an SMTP QUIT command.
|
| 935 |
+
* Closes the socket if there is no error or the $close_on_error argument is true.
|
| 936 |
+
* Implements from RFC 821: QUIT <CRLF>.
|
| 937 |
+
*
|
| 938 |
+
* @param bool $close_on_error Should the connection close if an error occurs?
|
| 939 |
+
*
|
| 940 |
+
* @return bool
|
| 941 |
+
*/
|
| 942 |
+
public function quit($close_on_error = true)
|
| 943 |
+
{
|
| 944 |
+
$noerror = $this->sendCommand('QUIT', 'QUIT', 221);
|
| 945 |
+
$err = $this->error; //Save any error
|
| 946 |
+
if ($noerror || $close_on_error) {
|
| 947 |
+
$this->close();
|
| 948 |
+
$this->error = $err; //Restore any error from the quit command
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
return $noerror;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
/**
|
| 955 |
+
* Send an SMTP RCPT command.
|
| 956 |
+
* Sets the TO argument to $toaddr.
|
| 957 |
+
* Returns true if the recipient was accepted false if it was rejected.
|
| 958 |
+
* Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
|
| 959 |
+
*
|
| 960 |
+
* @param string $address The address the message is being sent to
|
| 961 |
+
* @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
|
| 962 |
+
* or DELAY. If you specify NEVER all other notifications are ignored.
|
| 963 |
+
*
|
| 964 |
+
* @return bool
|
| 965 |
+
*/
|
| 966 |
+
public function recipient($address, $dsn = '')
|
| 967 |
+
{
|
| 968 |
+
if (empty($dsn)) {
|
| 969 |
+
$rcpt = 'RCPT TO:<' . $address . '>';
|
| 970 |
+
} else {
|
| 971 |
+
$dsn = strtoupper($dsn);
|
| 972 |
+
$notify = [];
|
| 973 |
+
|
| 974 |
+
if (strpos($dsn, 'NEVER') !== false) {
|
| 975 |
+
$notify[] = 'NEVER';
|
| 976 |
+
} else {
|
| 977 |
+
foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
|
| 978 |
+
if (strpos($dsn, $value) !== false) {
|
| 979 |
+
$notify[] = $value;
|
| 980 |
+
}
|
| 981 |
+
}
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
$rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
return $this->sendCommand(
|
| 988 |
+
'RCPT TO',
|
| 989 |
+
$rcpt,
|
| 990 |
+
[250, 251]
|
| 991 |
+
);
|
| 992 |
+
}
|
| 993 |
+
|
| 994 |
+
/**
|
| 995 |
+
* Send SMTP XCLIENT command to server and check its return code.
|
| 996 |
+
*
|
| 997 |
+
* @return bool True on success
|
| 998 |
+
*/
|
| 999 |
+
public function xclient(array $vars)
|
| 1000 |
+
{
|
| 1001 |
+
$xclient_options = "";
|
| 1002 |
+
foreach ($vars as $key => $value) {
|
| 1003 |
+
if (in_array($key, SMTP::$xclient_allowed_attributes)) {
|
| 1004 |
+
$xclient_options .= " {$key}={$value}";
|
| 1005 |
+
}
|
| 1006 |
+
}
|
| 1007 |
+
if (!$xclient_options) {
|
| 1008 |
+
return true;
|
| 1009 |
+
}
|
| 1010 |
+
return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 250);
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
/**
|
| 1014 |
+
* Send an SMTP RSET command.
|
| 1015 |
+
* Abort any transaction that is currently in progress.
|
| 1016 |
+
* Implements RFC 821: RSET <CRLF>.
|
| 1017 |
+
*
|
| 1018 |
+
* @return bool True on success
|
| 1019 |
+
*/
|
| 1020 |
+
public function reset()
|
| 1021 |
+
{
|
| 1022 |
+
return $this->sendCommand('RSET', 'RSET', 250);
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
/**
|
| 1026 |
+
* Send a command to an SMTP server and check its return code.
|
| 1027 |
+
*
|
| 1028 |
+
* @param string $command The command name - not sent to the server
|
| 1029 |
+
* @param string $commandstring The actual command to send
|
| 1030 |
+
* @param int|array $expect One or more expected integer success codes
|
| 1031 |
+
*
|
| 1032 |
+
* @return bool True on success
|
| 1033 |
+
*/
|
| 1034 |
+
protected function sendCommand($command, $commandstring, $expect)
|
| 1035 |
+
{
|
| 1036 |
+
if (!$this->connected()) {
|
| 1037 |
+
$this->setError("Called $command without being connected");
|
| 1038 |
+
|
| 1039 |
+
return false;
|
| 1040 |
+
}
|
| 1041 |
+
//Reject line breaks in all commands
|
| 1042 |
+
if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
|
| 1043 |
+
$this->setError("Command '$command' contained line breaks");
|
| 1044 |
+
|
| 1045 |
+
return false;
|
| 1046 |
+
}
|
| 1047 |
+
$this->client_send($commandstring . static::LE, $command);
|
| 1048 |
+
|
| 1049 |
+
$this->last_reply = $this->get_lines();
|
| 1050 |
+
//Fetch SMTP code and possible error code explanation
|
| 1051 |
+
$matches = [];
|
| 1052 |
+
if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
|
| 1053 |
+
$code = (int) $matches[1];
|
| 1054 |
+
$code_ex = (count($matches) > 2 ? $matches[2] : null);
|
| 1055 |
+
//Cut off error code from each response line
|
| 1056 |
+
$detail = preg_replace(
|
| 1057 |
+
"/{$code}[ -]" .
|
| 1058 |
+
($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
|
| 1059 |
+
'',
|
| 1060 |
+
$this->last_reply
|
| 1061 |
+
);
|
| 1062 |
+
} else {
|
| 1063 |
+
//Fall back to simple parsing if regex fails
|
| 1064 |
+
$code = (int) substr($this->last_reply, 0, 3);
|
| 1065 |
+
$code_ex = null;
|
| 1066 |
+
$detail = substr($this->last_reply, 4);
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
$this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
|
| 1070 |
+
|
| 1071 |
+
if (!in_array($code, (array) $expect, true)) {
|
| 1072 |
+
$this->setError(
|
| 1073 |
+
"$command command failed",
|
| 1074 |
+
$detail,
|
| 1075 |
+
$code,
|
| 1076 |
+
$code_ex
|
| 1077 |
+
);
|
| 1078 |
+
$this->edebug(
|
| 1079 |
+
'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
|
| 1080 |
+
self::DEBUG_CLIENT
|
| 1081 |
+
);
|
| 1082 |
+
|
| 1083 |
+
return false;
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
//Don't clear the error store when using keepalive
|
| 1087 |
+
if ($command !== 'RSET') {
|
| 1088 |
+
$this->setError('');
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
return true;
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
/**
|
| 1095 |
+
* Send an SMTP SAML command.
|
| 1096 |
+
* Starts a mail transaction from the email address specified in $from.
|
| 1097 |
+
* Returns true if successful or false otherwise. If True
|
| 1098 |
+
* the mail transaction is started and then one or more recipient
|
| 1099 |
+
* commands may be called followed by a data command. This command
|
| 1100 |
+
* will send the message to the users terminal if they are logged
|
| 1101 |
+
* in and send them an email.
|
| 1102 |
+
* Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
|
| 1103 |
+
*
|
| 1104 |
+
* @param string $from The address the message is from
|
| 1105 |
+
*
|
| 1106 |
+
* @return bool
|
| 1107 |
+
*/
|
| 1108 |
+
public function sendAndMail($from)
|
| 1109 |
+
{
|
| 1110 |
+
return $this->sendCommand('SAML', "SAML FROM:$from", 250);
|
| 1111 |
+
}
|
| 1112 |
+
|
| 1113 |
+
/**
|
| 1114 |
+
* Send an SMTP VRFY command.
|
| 1115 |
+
*
|
| 1116 |
+
* @param string $name The name to verify
|
| 1117 |
+
*
|
| 1118 |
+
* @return bool
|
| 1119 |
+
*/
|
| 1120 |
+
public function verify($name)
|
| 1121 |
+
{
|
| 1122 |
+
return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
|
| 1123 |
+
}
|
| 1124 |
+
|
| 1125 |
+
/**
|
| 1126 |
+
* Send an SMTP NOOP command.
|
| 1127 |
+
* Used to keep keep-alives alive, doesn't actually do anything.
|
| 1128 |
+
*
|
| 1129 |
+
* @return bool
|
| 1130 |
+
*/
|
| 1131 |
+
public function noop()
|
| 1132 |
+
{
|
| 1133 |
+
return $this->sendCommand('NOOP', 'NOOP', 250);
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
/**
|
| 1137 |
+
* Send an SMTP TURN command.
|
| 1138 |
+
* This is an optional command for SMTP that this class does not support.
|
| 1139 |
+
* This method is here to make the RFC821 Definition complete for this class
|
| 1140 |
+
* and _may_ be implemented in future.
|
| 1141 |
+
* Implements from RFC 821: TURN <CRLF>.
|
| 1142 |
+
*
|
| 1143 |
+
* @return bool
|
| 1144 |
+
*/
|
| 1145 |
+
public function turn()
|
| 1146 |
+
{
|
| 1147 |
+
$this->setError('The SMTP TURN command is not implemented');
|
| 1148 |
+
$this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
|
| 1149 |
+
|
| 1150 |
+
return false;
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
/**
|
| 1154 |
+
* Send raw data to the server.
|
| 1155 |
+
*
|
| 1156 |
+
* @param string $data The data to send
|
| 1157 |
+
* @param string $command Optionally, the command this is part of, used only for controlling debug output
|
| 1158 |
+
*
|
| 1159 |
+
* @return int|bool The number of bytes sent to the server or false on error
|
| 1160 |
+
*/
|
| 1161 |
+
public function client_send($data, $command = '')
|
| 1162 |
+
{
|
| 1163 |
+
//If SMTP transcripts are left enabled, or debug output is posted online
|
| 1164 |
+
//it can leak credentials, so hide credentials in all but lowest level
|
| 1165 |
+
if (
|
| 1166 |
+
self::DEBUG_LOWLEVEL > $this->do_debug &&
|
| 1167 |
+
in_array($command, ['User & Password', 'Username', 'Password'], true)
|
| 1168 |
+
) {
|
| 1169 |
+
$this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
|
| 1170 |
+
} else {
|
| 1171 |
+
$this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
|
| 1172 |
+
}
|
| 1173 |
+
set_error_handler(function () {
|
| 1174 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 1175 |
+
});
|
| 1176 |
+
$result = fwrite($this->smtp_conn, $data);
|
| 1177 |
+
restore_error_handler();
|
| 1178 |
+
|
| 1179 |
+
return $result;
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
/**
|
| 1183 |
+
* Get the latest error.
|
| 1184 |
+
*
|
| 1185 |
+
* @return array
|
| 1186 |
+
*/
|
| 1187 |
+
public function getError()
|
| 1188 |
+
{
|
| 1189 |
+
return $this->error;
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
/**
|
| 1193 |
+
* Get SMTP extensions available on the server.
|
| 1194 |
+
*
|
| 1195 |
+
* @return array|null
|
| 1196 |
+
*/
|
| 1197 |
+
public function getServerExtList()
|
| 1198 |
+
{
|
| 1199 |
+
return $this->server_caps;
|
| 1200 |
+
}
|
| 1201 |
+
|
| 1202 |
+
/**
|
| 1203 |
+
* Get metadata about the SMTP server from its HELO/EHLO response.
|
| 1204 |
+
* The method works in three ways, dependent on argument value and current state:
|
| 1205 |
+
* 1. HELO/EHLO has not been sent - returns null and populates $this->error.
|
| 1206 |
+
* 2. HELO has been sent -
|
| 1207 |
+
* $name == 'HELO': returns server name
|
| 1208 |
+
* $name == 'EHLO': returns boolean false
|
| 1209 |
+
* $name == any other string: returns null and populates $this->error
|
| 1210 |
+
* 3. EHLO has been sent -
|
| 1211 |
+
* $name == 'HELO'|'EHLO': returns the server name
|
| 1212 |
+
* $name == any other string: if extension $name exists, returns True
|
| 1213 |
+
* or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
|
| 1214 |
+
*
|
| 1215 |
+
* @param string $name Name of SMTP extension or 'HELO'|'EHLO'
|
| 1216 |
+
*
|
| 1217 |
+
* @return string|bool|null
|
| 1218 |
+
*/
|
| 1219 |
+
public function getServerExt($name)
|
| 1220 |
+
{
|
| 1221 |
+
if (!$this->server_caps) {
|
| 1222 |
+
$this->setError('No HELO/EHLO was sent');
|
| 1223 |
+
|
| 1224 |
+
return null;
|
| 1225 |
+
}
|
| 1226 |
+
|
| 1227 |
+
if (!array_key_exists($name, $this->server_caps)) {
|
| 1228 |
+
if ('HELO' === $name) {
|
| 1229 |
+
return $this->server_caps['EHLO'];
|
| 1230 |
+
}
|
| 1231 |
+
if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
|
| 1232 |
+
return false;
|
| 1233 |
+
}
|
| 1234 |
+
$this->setError('HELO handshake was used; No information about server extensions available');
|
| 1235 |
+
|
| 1236 |
+
return null;
|
| 1237 |
+
}
|
| 1238 |
+
|
| 1239 |
+
return $this->server_caps[$name];
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
/**
|
| 1243 |
+
* Get the last reply from the server.
|
| 1244 |
+
*
|
| 1245 |
+
* @return string
|
| 1246 |
+
*/
|
| 1247 |
+
public function getLastReply()
|
| 1248 |
+
{
|
| 1249 |
+
return $this->last_reply;
|
| 1250 |
+
}
|
| 1251 |
+
|
| 1252 |
+
/**
|
| 1253 |
+
* Read the SMTP server's response.
|
| 1254 |
+
* Either before eof or socket timeout occurs on the operation.
|
| 1255 |
+
* With SMTP we can tell if we have more lines to read if the
|
| 1256 |
+
* 4th character is '-' symbol. If it is a space then we don't
|
| 1257 |
+
* need to read anything else.
|
| 1258 |
+
*
|
| 1259 |
+
* @return string
|
| 1260 |
+
*/
|
| 1261 |
+
protected function get_lines()
|
| 1262 |
+
{
|
| 1263 |
+
//If the connection is bad, give up straight away
|
| 1264 |
+
if (!is_resource($this->smtp_conn)) {
|
| 1265 |
+
return '';
|
| 1266 |
+
}
|
| 1267 |
+
$data = '';
|
| 1268 |
+
$endtime = 0;
|
| 1269 |
+
stream_set_timeout($this->smtp_conn, $this->Timeout);
|
| 1270 |
+
if ($this->Timelimit > 0) {
|
| 1271 |
+
$endtime = time() + $this->Timelimit;
|
| 1272 |
+
}
|
| 1273 |
+
$selR = [$this->smtp_conn];
|
| 1274 |
+
$selW = null;
|
| 1275 |
+
while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
|
| 1276 |
+
//Must pass vars in here as params are by reference
|
| 1277 |
+
//solution for signals inspired by https://github.com/symfony/symfony/pull/6540
|
| 1278 |
+
set_error_handler(function () {
|
| 1279 |
+
call_user_func_array([$this, 'errorHandler'], func_get_args());
|
| 1280 |
+
});
|
| 1281 |
+
$n = stream_select($selR, $selW, $selW, $this->Timelimit);
|
| 1282 |
+
restore_error_handler();
|
| 1283 |
+
|
| 1284 |
+
if ($n === false) {
|
| 1285 |
+
$message = $this->getError()['detail'];
|
| 1286 |
+
|
| 1287 |
+
$this->edebug(
|
| 1288 |
+
'SMTP -> get_lines(): select failed (' . $message . ')',
|
| 1289 |
+
self::DEBUG_LOWLEVEL
|
| 1290 |
+
);
|
| 1291 |
+
|
| 1292 |
+
//stream_select returns false when the `select` system call is interrupted
|
| 1293 |
+
//by an incoming signal, try the select again
|
| 1294 |
+
if (stripos($message, 'interrupted system call') !== false) {
|
| 1295 |
+
$this->edebug(
|
| 1296 |
+
'SMTP -> get_lines(): retrying stream_select',
|
| 1297 |
+
self::DEBUG_LOWLEVEL
|
| 1298 |
+
);
|
| 1299 |
+
$this->setError('');
|
| 1300 |
+
continue;
|
| 1301 |
+
}
|
| 1302 |
+
|
| 1303 |
+
break;
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
if (!$n) {
|
| 1307 |
+
$this->edebug(
|
| 1308 |
+
'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
|
| 1309 |
+
self::DEBUG_LOWLEVEL
|
| 1310 |
+
);
|
| 1311 |
+
break;
|
| 1312 |
+
}
|
| 1313 |
+
|
| 1314 |
+
//Deliberate noise suppression - errors are handled afterwards
|
| 1315 |
+
$str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
|
| 1316 |
+
$this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
|
| 1317 |
+
$data .= $str;
|
| 1318 |
+
//If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
|
| 1319 |
+
//or 4th character is a space or a line break char, we are done reading, break the loop.
|
| 1320 |
+
//String array access is a significant micro-optimisation over strlen
|
| 1321 |
+
if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
|
| 1322 |
+
break;
|
| 1323 |
+
}
|
| 1324 |
+
//Timed-out? Log and break
|
| 1325 |
+
$info = stream_get_meta_data($this->smtp_conn);
|
| 1326 |
+
if ($info['timed_out']) {
|
| 1327 |
+
$this->edebug(
|
| 1328 |
+
'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
|
| 1329 |
+
self::DEBUG_LOWLEVEL
|
| 1330 |
+
);
|
| 1331 |
+
break;
|
| 1332 |
+
}
|
| 1333 |
+
//Now check if reads took too long
|
| 1334 |
+
if ($endtime && time() > $endtime) {
|
| 1335 |
+
$this->edebug(
|
| 1336 |
+
'SMTP -> get_lines(): timelimit reached (' .
|
| 1337 |
+
$this->Timelimit . ' sec)',
|
| 1338 |
+
self::DEBUG_LOWLEVEL
|
| 1339 |
+
);
|
| 1340 |
+
break;
|
| 1341 |
+
}
|
| 1342 |
+
}
|
| 1343 |
+
|
| 1344 |
+
return $data;
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
/**
|
| 1348 |
+
* Enable or disable VERP address generation.
|
| 1349 |
+
*
|
| 1350 |
+
* @param bool $enabled
|
| 1351 |
+
*/
|
| 1352 |
+
public function setVerp($enabled = false)
|
| 1353 |
+
{
|
| 1354 |
+
$this->do_verp = $enabled;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
/**
|
| 1358 |
+
* Get VERP address generation mode.
|
| 1359 |
+
*
|
| 1360 |
+
* @return bool
|
| 1361 |
+
*/
|
| 1362 |
+
public function getVerp()
|
| 1363 |
+
{
|
| 1364 |
+
return $this->do_verp;
|
| 1365 |
+
}
|
| 1366 |
+
|
| 1367 |
+
/**
|
| 1368 |
+
* Set error messages and codes.
|
| 1369 |
+
*
|
| 1370 |
+
* @param string $message The error message
|
| 1371 |
+
* @param string $detail Further detail on the error
|
| 1372 |
+
* @param string $smtp_code An associated SMTP error code
|
| 1373 |
+
* @param string $smtp_code_ex Extended SMTP code
|
| 1374 |
+
*/
|
| 1375 |
+
protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
|
| 1376 |
+
{
|
| 1377 |
+
$this->error = [
|
| 1378 |
+
'error' => $message,
|
| 1379 |
+
'detail' => $detail,
|
| 1380 |
+
'smtp_code' => $smtp_code,
|
| 1381 |
+
'smtp_code_ex' => $smtp_code_ex,
|
| 1382 |
+
];
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
/**
|
| 1386 |
+
* Set debug output method.
|
| 1387 |
+
*
|
| 1388 |
+
* @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
|
| 1389 |
+
*/
|
| 1390 |
+
public function setDebugOutput($method = 'echo')
|
| 1391 |
+
{
|
| 1392 |
+
$this->Debugoutput = $method;
|
| 1393 |
+
}
|
| 1394 |
+
|
| 1395 |
+
/**
|
| 1396 |
+
* Get debug output method.
|
| 1397 |
+
*
|
| 1398 |
+
* @return string
|
| 1399 |
+
*/
|
| 1400 |
+
public function getDebugOutput()
|
| 1401 |
+
{
|
| 1402 |
+
return $this->Debugoutput;
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
/**
|
| 1406 |
+
* Set debug output level.
|
| 1407 |
+
*
|
| 1408 |
+
* @param int $level
|
| 1409 |
+
*/
|
| 1410 |
+
public function setDebugLevel($level = 0)
|
| 1411 |
+
{
|
| 1412 |
+
$this->do_debug = $level;
|
| 1413 |
+
}
|
| 1414 |
+
|
| 1415 |
+
/**
|
| 1416 |
+
* Get debug output level.
|
| 1417 |
+
*
|
| 1418 |
+
* @return int
|
| 1419 |
+
*/
|
| 1420 |
+
public function getDebugLevel()
|
| 1421 |
+
{
|
| 1422 |
+
return $this->do_debug;
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
/**
|
| 1426 |
+
* Set SMTP timeout.
|
| 1427 |
+
*
|
| 1428 |
+
* @param int $timeout The timeout duration in seconds
|
| 1429 |
+
*/
|
| 1430 |
+
public function setTimeout($timeout = 0)
|
| 1431 |
+
{
|
| 1432 |
+
$this->Timeout = $timeout;
|
| 1433 |
+
}
|
| 1434 |
+
|
| 1435 |
+
/**
|
| 1436 |
+
* Get SMTP timeout.
|
| 1437 |
+
*
|
| 1438 |
+
* @return int
|
| 1439 |
+
*/
|
| 1440 |
+
public function getTimeout()
|
| 1441 |
+
{
|
| 1442 |
+
return $this->Timeout;
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
/**
|
| 1446 |
+
* Reports an error number and string.
|
| 1447 |
+
*
|
| 1448 |
+
* @param int $errno The error number returned by PHP
|
| 1449 |
+
* @param string $errmsg The error message returned by PHP
|
| 1450 |
+
* @param string $errfile The file the error occurred in
|
| 1451 |
+
* @param int $errline The line number the error occurred on
|
| 1452 |
+
*/
|
| 1453 |
+
protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
|
| 1454 |
+
{
|
| 1455 |
+
$notice = 'Connection failed.';
|
| 1456 |
+
$this->setError(
|
| 1457 |
+
$notice,
|
| 1458 |
+
$errmsg,
|
| 1459 |
+
(string) $errno
|
| 1460 |
+
);
|
| 1461 |
+
$this->edebug(
|
| 1462 |
+
"$notice Error #$errno: $errmsg [$errfile line $errline]",
|
| 1463 |
+
self::DEBUG_CONNECTION
|
| 1464 |
+
);
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
/**
|
| 1468 |
+
* Extract and return the ID of the last SMTP transaction based on
|
| 1469 |
+
* a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
|
| 1470 |
+
* Relies on the host providing the ID in response to a DATA command.
|
| 1471 |
+
* If no reply has been received yet, it will return null.
|
| 1472 |
+
* If no pattern was matched, it will return false.
|
| 1473 |
+
*
|
| 1474 |
+
* @return bool|string|null
|
| 1475 |
+
*/
|
| 1476 |
+
protected function recordLastTransactionID()
|
| 1477 |
+
{
|
| 1478 |
+
$reply = $this->getLastReply();
|
| 1479 |
+
|
| 1480 |
+
if (empty($reply)) {
|
| 1481 |
+
$this->last_smtp_transaction_id = null;
|
| 1482 |
+
} else {
|
| 1483 |
+
$this->last_smtp_transaction_id = false;
|
| 1484 |
+
foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
|
| 1485 |
+
$matches = [];
|
| 1486 |
+
if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
|
| 1487 |
+
$this->last_smtp_transaction_id = trim($matches[1]);
|
| 1488 |
+
break;
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
return $this->last_smtp_transaction_id;
|
| 1494 |
+
}
|
| 1495 |
+
|
| 1496 |
+
/**
|
| 1497 |
+
* Get the queue/transaction ID of the last SMTP transaction
|
| 1498 |
+
* If no reply has been received yet, it will return null.
|
| 1499 |
+
* If no pattern was matched, it will return false.
|
| 1500 |
+
*
|
| 1501 |
+
* @return bool|string|null
|
| 1502 |
+
*
|
| 1503 |
+
* @see recordLastTransactionID()
|
| 1504 |
+
*/
|
| 1505 |
+
public function getLastTransactionID()
|
| 1506 |
+
{
|
| 1507 |
+
return $this->last_smtp_transaction_id;
|
| 1508 |
+
}
|
| 1509 |
+
}
|
PROJECT_OVERVIEW.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Martechsol Chat Assistant - Project Overview
|
| 2 |
+
|
| 3 |
+
This document explains how your RAG (Retrieval-Augmented Generation) chatbot works in simple terms.
|
| 4 |
+
|
| 5 |
+
## 1. The "Brain" (The AI Model)
|
| 6 |
+
The assistant uses the **Llama 3.1 8B** model (specifically `llama-3.1-8b-instant`).
|
| 7 |
+
- **Why this model?** It is extremely fast, smart, and efficient at understanding context and following instructions.
|
| 8 |
+
- **Provider**: It is powered by **Groq**, which allows the model to reply almost instantly.
|
| 9 |
+
|
| 10 |
+
## 2. How it Works (The "RAG" Process)
|
| 11 |
+
Instead of just relying on general knowledge, this bot "reads" your documents to give specific answers.
|
| 12 |
+
1. **Reading**: It looks at your files in the `docs/` folder (PDFs and Text files).
|
| 13 |
+
2. **Memorizing**: It breaks the text into small chunks and converts them into mathematical "vectors" (using the `bge-small-en-v1.5` model).
|
| 14 |
+
3. **Searching**: When you ask a question, it quickly searches its "memory" (the **FAISS** index) to find the most relevant parts of your documents.
|
| 15 |
+
4. **Answering**: It sends your question along with those relevant document parts to the Llama AI, which then writes a human-like reply.
|
| 16 |
+
|
| 17 |
+
## 3. The Architecture
|
| 18 |
+
### Frontend (The Face)
|
| 19 |
+
- **Gradio**: This is the clean, chat-like interface you see. It is hosted on **Hugging Face Spaces**.
|
| 20 |
+
- **WordPress Addon**: A custom HTML/CSS/JS wrapper that lets you embed the chat as a beautiful floating button on your website.
|
| 21 |
+
|
| 22 |
+
### Backend (The Engine)
|
| 23 |
+
- **FastAPI**: A high-performance Python framework that connects everything. It manages the messages, handles the document search, and talks to the AI provider.
|
| 24 |
+
- **Uvicorn**: The lightning-fast server that runs the FastAPI code.
|
| 25 |
+
|
| 26 |
+
## 4. Key Features
|
| 27 |
+
- **Humanistic Replies**: The bot is programmed to be polite, conversational, and professional.
|
| 28 |
+
- **Context-Aware**: It remembers the last few messages in the conversation so you can ask follow-up questions.
|
| 29 |
+
- **Greeting Support**: It can handle "Hi" and "Hello" naturally before getting down to business.
|
| 30 |
+
- **Safe & Grounded**: It is instructed to only answer based on your documents to prevent "hallucinations" (making things up).
|
README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Fast RAG Chatbot
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
storage: true
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# High-Performance RAG Chatbot (FastAPI + FAISS)
|
| 12 |
+
|
| 13 |
+
Production-style document QA chatbot using:
|
| 14 |
+
- FastAPI API service
|
| 15 |
+
- FAISS vector search
|
| 16 |
+
- SentenceTransformer embeddings (`BAAI/bge-small-en-v1.5` by default)
|
| 17 |
+
- Groq (preferred) or Hugging Face LLM APIs
|
| 18 |
+
- Optional Gradio chat UI
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
- Loads `.pdf` and `.txt` files from `docs/`
|
| 23 |
+
- Cleans extracted text and chunks into semantic windows
|
| 24 |
+
- Chunk size: 420 tokens (word-level approximation)
|
| 25 |
+
- Overlap: 80 tokens
|
| 26 |
+
- Builds FAISS index and saves it locally
|
| 27 |
+
- Re-indexes only when docs change (fingerprint-based cache)
|
| 28 |
+
- Retrieves top-k relevant chunks only (default k=4)
|
| 29 |
+
- Strict anti-hallucination prompt
|
| 30 |
+
- Health endpoint with docs/index status
|
| 31 |
+
- Retrieval logging (source + similarity score)
|
| 32 |
+
- CORS controls for website integration
|
| 33 |
+
- Optional API key auth for `/chat`
|
| 34 |
+
- In-memory rate limiting per client IP
|
| 35 |
+
- Query embedding cache for repeated questions
|
| 36 |
+
- Docker + docker-compose deployment
|
| 37 |
+
|
| 38 |
+
## Project Structure
|
| 39 |
+
|
| 40 |
+
`app/main.py` - FastAPI app and endpoints
|
| 41 |
+
`app/services/document_loader.py` - PDF/TXT ingestion and cleaning
|
| 42 |
+
`app/services/chunker.py` - token-window chunking
|
| 43 |
+
`app/services/embeddings.py` - embedding model wrapper
|
| 44 |
+
`app/services/vector_store.py` - FAISS index and retrieval
|
| 45 |
+
`app/services/llm.py` - Groq/HF LLM clients and prompt
|
| 46 |
+
`app/services/rag_pipeline.py` - end-to-end chat flow
|
| 47 |
+
`app/ui_gradio.py` - optional web chat UI
|
| 48 |
+
|
| 49 |
+
## Setup
|
| 50 |
+
|
| 51 |
+
1. Install dependencies:
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
pip install -r requirements.txt
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
2. Configure environment:
|
| 58 |
+
|
| 59 |
+
```bash
|
| 60 |
+
copy .env.example .env
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
Then set your keys in `.env`:
|
| 64 |
+
- `GROQ_API_KEY` (if using Groq)
|
| 65 |
+
- `HF_API_KEY` (if using Hugging Face)
|
| 66 |
+
- Optional:
|
| 67 |
+
- `API_KEY` for request auth (send as `x-api-key`)
|
| 68 |
+
- `CORS_ALLOW_ORIGINS` as comma-separated origins
|
| 69 |
+
- `RATE_LIMIT_REQUESTS` and `RATE_LIMIT_WINDOW_SECONDS`
|
| 70 |
+
|
| 71 |
+
3. Add documents:
|
| 72 |
+
- Put your `.pdf` and `.txt` files in `docs/`
|
| 73 |
+
|
| 74 |
+
## Run API
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
## Endpoints
|
| 81 |
+
|
| 82 |
+
### `GET /health`
|
| 83 |
+
Returns status and index readiness.
|
| 84 |
+
|
| 85 |
+
### `POST /chat`
|
| 86 |
+
|
| 87 |
+
Request:
|
| 88 |
+
```json
|
| 89 |
+
{
|
| 90 |
+
"message": "What are the key points?",
|
| 91 |
+
"history": []
|
| 92 |
+
}
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Response:
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"reply": "Answer based on retrieved context.",
|
| 99 |
+
"retrieved_chunks": [
|
| 100 |
+
{
|
| 101 |
+
"id": "...",
|
| 102 |
+
"source": "...",
|
| 103 |
+
"text": "...",
|
| 104 |
+
"score": 0.83
|
| 105 |
+
}
|
| 106 |
+
]
|
| 107 |
+
}
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## Optional UI
|
| 111 |
+
|
| 112 |
+
Start API first, then:
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
python -m app.ui_gradio
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
By default, Gradio now runs in direct RAG mode (no localhost API dependency).
|
| 119 |
+
If you set `RAG_API_URL`, it will call that external FastAPI endpoint instead.
|
| 120 |
+
|
| 121 |
+
## Deployment Notes
|
| 122 |
+
|
| 123 |
+
- Works as backend for websites (REST API is frontend-agnostic)
|
| 124 |
+
- Persist `data/index/` volume in production
|
| 125 |
+
- Prefer Groq provider for low latency
|
| 126 |
+
- Keep `top_k` small (3-5) for speed and lower prompt tokens
|
| 127 |
+
- Protect `/chat` with `API_KEY` in production
|
| 128 |
+
- Set strict `CORS_ALLOW_ORIGINS` instead of `*`
|
| 129 |
+
|
| 130 |
+
## Docker Deployment
|
| 131 |
+
|
| 132 |
+
Build and run:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
docker compose up --build -d
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
Health check:
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
curl http://localhost:8000/health
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
Chat call with API key:
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
curl -X POST http://localhost:8000/chat ^
|
| 148 |
+
-H "Content-Type: application/json" ^
|
| 149 |
+
-H "x-api-key: YOUR_API_KEY" ^
|
| 150 |
+
-d "{\"message\":\"What does the handbook say about leave policy?\",\"history\":[]}"
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## Hugging Face Spaces (Recommended: Gradio Space)
|
| 154 |
+
|
| 155 |
+
Use these settings in your Space:
|
| 156 |
+
- **SDK**: Gradio
|
| 157 |
+
- **App file**: `app.py`
|
| 158 |
+
- **Python version**: 3.10+ (3.11 recommended)
|
| 159 |
+
|
| 160 |
+
Add Space Secrets:
|
| 161 |
+
- `GROQ_API_KEY` (or `HF_API_KEY`)
|
| 162 |
+
- Optional: `LLM_PROVIDER`, `GROQ_MODEL`, `HF_MODEL`, `TOP_K`
|
| 163 |
+
|
| 164 |
+
Upload project files (excluding `.env`) and include your knowledge files inside `docs/`.
|
app.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from app.main import app
|
| 3 |
+
|
| 4 |
+
# Hugging Face Spaces (sdk: gradio) looks for `app` variable
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
uvicorn.run("app.main:app", host="0.0.0.0", port=7860)
|
app/__init__.py
ADDED
|
File without changes
|
app/admin/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# app/admin/__init__.py
|
app/admin/router.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app/admin/router.py
|
| 3 |
+
───────────────────
|
| 4 |
+
Secure FastAPI router for the Admin Panel.
|
| 5 |
+
|
| 6 |
+
Security design:
|
| 7 |
+
• All /admin/* routes require HTTP Basic Auth.
|
| 8 |
+
• Credentials are compared using secrets.compare_digest() to prevent
|
| 9 |
+
timing-based attacks — NOT a simple == comparison.
|
| 10 |
+
• Wrong credentials always return 401 with WWW-Authenticate header.
|
| 11 |
+
• Admin routes are completely isolated from the main app and Gradio UI.
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
GET /admin → Serves the premium admin.html SPA
|
| 15 |
+
GET /api/admin/sessions → Returns all session summaries (JSON)
|
| 16 |
+
GET /api/admin/sessions/{session_id} → Returns full session transcript
|
| 17 |
+
DELETE /api/admin/sessions/{session_id} → Deletes a session
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import secrets
|
| 21 |
+
import logging
|
| 22 |
+
import asyncio
|
| 23 |
+
import time
|
| 24 |
+
import random
|
| 25 |
+
import smtplib
|
| 26 |
+
from email.mime.text import MIMEText
|
| 27 |
+
from email.mime.multipart import MIMEMultipart
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Dict
|
| 30 |
+
|
| 31 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 32 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 33 |
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
| 34 |
+
|
| 35 |
+
from app.services.session_store import get_all_sessions, get_session, delete_session, _list_json_files, _read_json
|
| 36 |
+
from app.core.config import get_settings
|
| 37 |
+
import pandas as pd
|
| 38 |
+
import io
|
| 39 |
+
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 40 |
+
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
settings = get_settings()
|
| 43 |
+
|
| 44 |
+
# ── Credentials (stored as constants — in production, move to env vars) ───────
|
| 45 |
+
ADMIN_USERNAME = "martech_admin"
|
| 46 |
+
ADMIN_PASSWORD = "martech_admin_303"
|
| 47 |
+
|
| 48 |
+
# ── Security scheme ───────────────────────────────────────────────────────────
|
| 49 |
+
security = HTTPBasic()
|
| 50 |
+
|
| 51 |
+
# ── Router ────────────────────────────────────────────────────────────────────
|
| 52 |
+
admin_router = APIRouter(tags=["admin"])
|
| 53 |
+
|
| 54 |
+
# ── OTP Store (In-memory for simplicity) ──────────────────────────────────────
|
| 55 |
+
# Format: {username: {"code": "123456", "expires": timestamp}}
|
| 56 |
+
_otp_store: Dict[str, dict] = {}
|
| 57 |
+
OTP_EXPIRY_SECONDS = 300 # 5 minutes
|
| 58 |
+
|
| 59 |
+
# ── Path to the admin HTML template ──────────────────────────────────────────
|
| 60 |
+
_ADMIN_HTML_PATH = Path(__file__).parent / "templates" / "admin.html"
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# ── Auth dependency ───────────────────────────────────────────────────────────
|
| 64 |
+
def verify_admin(credentials: HTTPBasicCredentials = Depends(security)) -> str:
|
| 65 |
+
"""
|
| 66 |
+
Verifies HTTP Basic credentials using constant-time comparison.
|
| 67 |
+
Raises 401 for any mismatch. Returns the username on success.
|
| 68 |
+
"""
|
| 69 |
+
correct_username = secrets.compare_digest(
|
| 70 |
+
credentials.username.encode("utf-8"),
|
| 71 |
+
ADMIN_USERNAME.encode("utf-8"),
|
| 72 |
+
)
|
| 73 |
+
correct_password = secrets.compare_digest(
|
| 74 |
+
credentials.password.encode("utf-8"),
|
| 75 |
+
ADMIN_PASSWORD.encode("utf-8"),
|
| 76 |
+
)
|
| 77 |
+
if not (correct_username and correct_password):
|
| 78 |
+
logger.warning(
|
| 79 |
+
"Admin auth failed for user: '%s'", credentials.username
|
| 80 |
+
)
|
| 81 |
+
raise HTTPException(
|
| 82 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 83 |
+
detail="Invalid admin credentials.",
|
| 84 |
+
)
|
| 85 |
+
return credentials.username
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# ── OTP Endpoints ─────────────────────────────────────────────────────────────
|
| 89 |
+
|
| 90 |
+
from pydantic import BaseModel
|
| 91 |
+
|
| 92 |
+
class LoginRequest(BaseModel):
|
| 93 |
+
username: str
|
| 94 |
+
password: str
|
| 95 |
+
|
| 96 |
+
class OTPVerifyRequest(BaseModel):
|
| 97 |
+
username: str
|
| 98 |
+
password: str
|
| 99 |
+
code: str
|
| 100 |
+
|
| 101 |
+
@admin_router.post("/api/admin/request-otp", include_in_schema=False)
|
| 102 |
+
async def request_otp(req: LoginRequest):
|
| 103 |
+
"""
|
| 104 |
+
Verifies password and generates a 6-digit OTP.
|
| 105 |
+
Sends the OTP to the configured admin email.
|
| 106 |
+
"""
|
| 107 |
+
if not (secrets.compare_digest(req.username, ADMIN_USERNAME) and
|
| 108 |
+
secrets.compare_digest(req.password, ADMIN_PASSWORD)):
|
| 109 |
+
raise HTTPException(status_code=401, detail="Invalid credentials.")
|
| 110 |
+
|
| 111 |
+
# Generate 6-digit code
|
| 112 |
+
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
| 113 |
+
_otp_store[req.username] = {
|
| 114 |
+
"code": code,
|
| 115 |
+
"expires": time.time() + OTP_EXPIRY_SECONDS
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
# Attempt to send email
|
| 119 |
+
email_sent = False
|
| 120 |
+
if settings.smtp_user and settings.smtp_pass:
|
| 121 |
+
try:
|
| 122 |
+
# Use to_thread to avoid blocking the event loop during SMTP network I/O
|
| 123 |
+
await asyncio.to_thread(send_otp_email, settings.admin_email, code)
|
| 124 |
+
email_sent = True
|
| 125 |
+
logger.info("OTP email sent to %s", settings.admin_email)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error("Failed to send OTP email: %s", e)
|
| 128 |
+
else:
|
| 129 |
+
logger.warning("SMTP credentials not configured. OTP will only be in logs.")
|
| 130 |
+
|
| 131 |
+
# FALLBACK/LOG: Always log to console so the user is never locked out
|
| 132 |
+
print("\n" + "="*50)
|
| 133 |
+
print(f" ADMIN OTP FOR {req.username.upper()}: {code}")
|
| 134 |
+
print(f" SENDING TO: {settings.admin_email} ({'SUCCESS' if email_sent else 'FAILED/SKIPPED'})")
|
| 135 |
+
print(" EXPIRES IN 5 MINUTES")
|
| 136 |
+
print("="*50 + "\n")
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
"message": "OTP sent successfully.",
|
| 140 |
+
"email_status": "sent" if email_sent else "logged_to_console_only"
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
import socket
|
| 145 |
+
|
| 146 |
+
def send_otp_email(to_email: str, otp_code: str):
|
| 147 |
+
"""Sends a professional OTP email via SMTP. Run in thread."""
|
| 148 |
+
msg = MIMEMultipart()
|
| 149 |
+
msg['From'] = settings.smtp_user
|
| 150 |
+
msg['To'] = to_email
|
| 151 |
+
msg['Subject'] = f"Your Admin Access Code: {otp_code}"
|
| 152 |
+
|
| 153 |
+
body = f"""
|
| 154 |
+
<html>
|
| 155 |
+
<body style="font-family: sans-serif; padding: 20px; color: #333;">
|
| 156 |
+
<h2 style="color: #3b82f6;">Martechsol Admin Panel</h2>
|
| 157 |
+
<p>Your one-time security code is:</p>
|
| 158 |
+
<div style="font-size: 32px; font-weight: bold; letter-spacing: 5px; padding: 15px; background: #f3f4f6; border-radius: 8px; display: inline-block; margin: 10px 0;">
|
| 159 |
+
{otp_code}
|
| 160 |
+
</div>
|
| 161 |
+
<p style="color: #666; font-size: 14px;">This code will expire in 5 minutes.</p>
|
| 162 |
+
<hr style="border: 0; border-top: 1px solid #eee; margin-top: 20px;">
|
| 163 |
+
<p style="font-size: 12px; color: #999;">If you did not request this code, please secure your account.</p>
|
| 164 |
+
</body>
|
| 165 |
+
</html>
|
| 166 |
+
"""
|
| 167 |
+
msg.attach(MIMEText(body, 'html'))
|
| 168 |
+
|
| 169 |
+
# Force IPv4 resolution to avoid "Network unreachable" IPv6 issues
|
| 170 |
+
try:
|
| 171 |
+
ais = socket.getaddrinfo(settings.smtp_server, settings.smtp_port, socket.AF_INET)
|
| 172 |
+
target_ip = ais[0][4][0]
|
| 173 |
+
logger.info("Resolved %s to IPv4: %s", settings.smtp_server, target_ip)
|
| 174 |
+
except Exception as e:
|
| 175 |
+
logger.error("DNS Resolution failed: %s", e)
|
| 176 |
+
target_ip = settings.smtp_server
|
| 177 |
+
|
| 178 |
+
logger.info("Attempting SMTP_SSL connection to %s:%d (via %s)",
|
| 179 |
+
settings.smtp_server, settings.smtp_port, target_ip)
|
| 180 |
+
|
| 181 |
+
# Using the IP directly can sometimes bypass DNS/routing quirks
|
| 182 |
+
with smtplib.SMTP_SSL(target_ip, settings.smtp_port, timeout=15) as server:
|
| 183 |
+
server.login(settings.smtp_user, settings.smtp_pass)
|
| 184 |
+
server.send_message(msg)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
@admin_router.post("/api/admin/verify-otp", include_in_schema=False)
|
| 188 |
+
async def verify_otp(req: OTPVerifyRequest):
|
| 189 |
+
"""Verifies both the password and the OTP code."""
|
| 190 |
+
# 1. Verify password first
|
| 191 |
+
if not (secrets.compare_digest(req.username, ADMIN_USERNAME) and
|
| 192 |
+
secrets.compare_digest(req.password, ADMIN_PASSWORD)):
|
| 193 |
+
raise HTTPException(status_code=401, detail="Invalid credentials.")
|
| 194 |
+
|
| 195 |
+
# 2. Verify OTP
|
| 196 |
+
stored = _otp_store.get(req.username)
|
| 197 |
+
if not stored:
|
| 198 |
+
raise HTTPException(status_code=400, detail="No OTP requested for this user.")
|
| 199 |
+
|
| 200 |
+
if time.time() > stored["expires"]:
|
| 201 |
+
del _otp_store[req.username]
|
| 202 |
+
raise HTTPException(status_code=400, detail="OTP has expired. Please request a new one.")
|
| 203 |
+
|
| 204 |
+
if not secrets.compare_digest(req.code, stored["code"]):
|
| 205 |
+
raise HTTPException(status_code=401, detail="Invalid OTP code.")
|
| 206 |
+
|
| 207 |
+
# Success — clear OTP and return
|
| 208 |
+
del _otp_store[req.username]
|
| 209 |
+
return {"message": "OTP verified."}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# ── Routes ────────────────────────────────────────────────────────────────────
|
| 213 |
+
|
| 214 |
+
@admin_router.get("/admin", response_class=HTMLResponse, include_in_schema=False)
|
| 215 |
+
async def admin_panel():
|
| 216 |
+
"""Serves the admin panel SPA."""
|
| 217 |
+
try:
|
| 218 |
+
html_content = _ADMIN_HTML_PATH.read_text(encoding="utf-8")
|
| 219 |
+
return HTMLResponse(content=html_content, status_code=200)
|
| 220 |
+
except FileNotFoundError:
|
| 221 |
+
logger.error("admin.html template not found at %s", _ADMIN_HTML_PATH)
|
| 222 |
+
raise HTTPException(
|
| 223 |
+
status_code=500,
|
| 224 |
+
detail="Admin panel template is missing. Please redeploy.",
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@admin_router.get("/api/admin/sessions", include_in_schema=False)
|
| 229 |
+
async def list_sessions(username: str = Depends(verify_admin)):
|
| 230 |
+
"""Returns all session summaries sorted by most recent activity."""
|
| 231 |
+
sessions = await get_all_sessions()
|
| 232 |
+
return JSONResponse(content={"sessions": sessions, "total": len(sessions)})
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
@admin_router.get("/api/admin/sessions/{session_id}", include_in_schema=False)
|
| 236 |
+
async def get_session_detail(
|
| 237 |
+
session_id: str, username: str = Depends(verify_admin)
|
| 238 |
+
):
|
| 239 |
+
"""Returns the full Q&A transcript for a specific session."""
|
| 240 |
+
# Basic input sanitisation — session_id should only be a UUID-like string
|
| 241 |
+
if not session_id.replace("-", "").replace("_", "").isalnum():
|
| 242 |
+
raise HTTPException(status_code=400, detail="Invalid session ID format.")
|
| 243 |
+
|
| 244 |
+
data = await get_session(session_id)
|
| 245 |
+
if not data:
|
| 246 |
+
raise HTTPException(
|
| 247 |
+
status_code=404,
|
| 248 |
+
detail=f"Session '{session_id}' not found.",
|
| 249 |
+
)
|
| 250 |
+
return JSONResponse(content=data)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@admin_router.delete("/api/admin/sessions/{session_id}", include_in_schema=False)
|
| 254 |
+
async def remove_session(
|
| 255 |
+
session_id: str, username: str = Depends(verify_admin)
|
| 256 |
+
):
|
| 257 |
+
"""Deletes a session file permanently."""
|
| 258 |
+
if not session_id.replace("-", "").replace("_", "").isalnum():
|
| 259 |
+
raise HTTPException(status_code=400, detail="Invalid session ID format.")
|
| 260 |
+
|
| 261 |
+
success = await delete_session(session_id)
|
| 262 |
+
if not success:
|
| 263 |
+
raise HTTPException(status_code=500, detail="Failed to delete session.")
|
| 264 |
+
return JSONResponse(content={"deleted": session_id})
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
# ── Export Endpoints ─────────────────────────────────────────────────────────
|
| 268 |
+
|
| 269 |
+
@admin_router.get("/api/admin/export/session/{session_id}", include_in_schema=False)
|
| 270 |
+
async def export_session(
|
| 271 |
+
session_id: str, format: str = "excel", username: str = Depends(verify_admin)
|
| 272 |
+
):
|
| 273 |
+
"""Exports a single session's transcript in Excel or JSON format."""
|
| 274 |
+
data = await get_session(session_id)
|
| 275 |
+
if not data:
|
| 276 |
+
raise HTTPException(status_code=404, detail="Session not found.")
|
| 277 |
+
|
| 278 |
+
messages = data.get("messages", [])
|
| 279 |
+
export_data = []
|
| 280 |
+
for msg in messages:
|
| 281 |
+
export_data.append({
|
| 282 |
+
"Timestamp": msg.get("timestamp", ""),
|
| 283 |
+
"Question": msg.get("question", ""),
|
| 284 |
+
"Answer": msg.get("answer", "")
|
| 285 |
+
})
|
| 286 |
+
|
| 287 |
+
if format.lower() == "json":
|
| 288 |
+
return JSONResponse(
|
| 289 |
+
content=export_data,
|
| 290 |
+
headers={"Content-Disposition": f"attachment; filename=session_{session_id}.json"}
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# Excel Export
|
| 294 |
+
df = pd.DataFrame(export_data)
|
| 295 |
+
output = io.BytesIO()
|
| 296 |
+
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
| 297 |
+
df.to_excel(writer, index=False, sheet_name='Transcript')
|
| 298 |
+
|
| 299 |
+
output.seek(0)
|
| 300 |
+
return StreamingResponse(
|
| 301 |
+
output,
|
| 302 |
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 303 |
+
headers={"Content-Disposition": f"attachment; filename=session_{session_id}.xlsx"}
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
@admin_router.get("/api/admin/export/all", include_in_schema=False)
|
| 308 |
+
async def export_all_sessions(
|
| 309 |
+
format: str = "excel", username: str = Depends(verify_admin)
|
| 310 |
+
):
|
| 311 |
+
"""Exports all sessions' transcripts in a single Excel or JSON file."""
|
| 312 |
+
files = await asyncio.to_thread(_list_json_files)
|
| 313 |
+
all_messages = []
|
| 314 |
+
|
| 315 |
+
for fpath in files:
|
| 316 |
+
try:
|
| 317 |
+
data = await asyncio.to_thread(_read_json, fpath)
|
| 318 |
+
session_id = data.get("session_id", fpath.stem)
|
| 319 |
+
for msg in data.get("messages", []):
|
| 320 |
+
all_messages.append({
|
| 321 |
+
"Session ID": session_id,
|
| 322 |
+
"Timestamp": msg.get("timestamp", ""),
|
| 323 |
+
"Question": msg.get("question", ""),
|
| 324 |
+
"Answer": msg.get("answer", "")
|
| 325 |
+
})
|
| 326 |
+
except Exception as e:
|
| 327 |
+
logger.warning("Failed to read %s for export: %s", fpath, e)
|
| 328 |
+
|
| 329 |
+
if format.lower() == "json":
|
| 330 |
+
return JSONResponse(
|
| 331 |
+
content=all_messages,
|
| 332 |
+
headers={"Content-Disposition": "attachment; filename=all_chats_export.json"}
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
# Excel Export
|
| 336 |
+
df = pd.DataFrame(all_messages)
|
| 337 |
+
output = io.BytesIO()
|
| 338 |
+
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
| 339 |
+
df.to_excel(writer, index=False, sheet_name='All Chats')
|
| 340 |
+
|
| 341 |
+
output.seek(0)
|
| 342 |
+
return StreamingResponse(
|
| 343 |
+
output,
|
| 344 |
+
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 345 |
+
headers={"Content-Disposition": "attachment; filename=all_chats_export.xlsx"}
|
| 346 |
+
)
|
app/admin/templates/admin.html
ADDED
|
@@ -0,0 +1,1143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
| 7 |
+
<title>Martechsol — Admin Panel</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
| 10 |
+
<style>
|
| 11 |
+
*,
|
| 12 |
+
*::before,
|
| 13 |
+
*::after {
|
| 14 |
+
box-sizing: border-box;
|
| 15 |
+
margin: 0;
|
| 16 |
+
padding: 0
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
:root {
|
| 20 |
+
--bg: #0a0d14;
|
| 21 |
+
--surface: #111827;
|
| 22 |
+
--surface2: #1a2235;
|
| 23 |
+
--surface3: #1f2a3d;
|
| 24 |
+
--border: #1e2d45;
|
| 25 |
+
--accent: #3b82f6;
|
| 26 |
+
--accent2: #6366f1;
|
| 27 |
+
--green: #10b981;
|
| 28 |
+
--red: #ef4444;
|
| 29 |
+
--yellow: #f59e0b;
|
| 30 |
+
--text: #f1f5f9;
|
| 31 |
+
--text2: #94a3b8;
|
| 32 |
+
--text3: #64748b;
|
| 33 |
+
--radius: 12px;
|
| 34 |
+
--shadow: 0 4px 24px rgba(0, 0, 0, .4);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
html,
|
| 38 |
+
body {
|
| 39 |
+
height: 100%;
|
| 40 |
+
font-family: 'Inter', sans-serif;
|
| 41 |
+
background: var(--bg);
|
| 42 |
+
color: var(--text);
|
| 43 |
+
overflow: hidden
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* ── Login Screen ── */
|
| 47 |
+
#login-screen {
|
| 48 |
+
position: fixed;
|
| 49 |
+
inset: 0;
|
| 50 |
+
display: flex;
|
| 51 |
+
align-items: center;
|
| 52 |
+
justify-content: center;
|
| 53 |
+
background: radial-gradient(ellipse at 50% 0%, #1a2a4a 0%, var(--bg) 70%);
|
| 54 |
+
z-index: 9999;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.login-card {
|
| 58 |
+
width: 380px;
|
| 59 |
+
background: var(--surface);
|
| 60 |
+
border: 1px solid var(--border);
|
| 61 |
+
border-radius: 20px;
|
| 62 |
+
padding: 44px 40px;
|
| 63 |
+
box-shadow: var(--shadow);
|
| 64 |
+
animation: fadeUp .5s ease;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.login-logo {
|
| 68 |
+
text-align: center;
|
| 69 |
+
margin-bottom: 28px
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.login-logo svg {
|
| 73 |
+
width: 52px;
|
| 74 |
+
height: 52px
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.login-logo h1 {
|
| 78 |
+
font-size: 22px;
|
| 79 |
+
font-weight: 700;
|
| 80 |
+
margin-top: 12px;
|
| 81 |
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
| 82 |
+
-webkit-background-clip: text;
|
| 83 |
+
-webkit-text-fill-color: transparent
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.login-logo p {
|
| 87 |
+
font-size: 13px;
|
| 88 |
+
color: var(--text3);
|
| 89 |
+
margin-top: 4px
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.form-group {
|
| 93 |
+
margin-bottom: 16px
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.form-group label {
|
| 97 |
+
display: block;
|
| 98 |
+
font-size: 12px;
|
| 99 |
+
font-weight: 600;
|
| 100 |
+
color: var(--text2);
|
| 101 |
+
text-transform: uppercase;
|
| 102 |
+
letter-spacing: .06em;
|
| 103 |
+
margin-bottom: 6px
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.form-group input {
|
| 107 |
+
width: 100%;
|
| 108 |
+
padding: 11px 14px;
|
| 109 |
+
background: var(--surface2);
|
| 110 |
+
border: 1px solid var(--border);
|
| 111 |
+
border-radius: 8px;
|
| 112 |
+
color: var(--text);
|
| 113 |
+
font-size: 14px;
|
| 114 |
+
font-family: inherit;
|
| 115 |
+
outline: none;
|
| 116 |
+
transition: border-color .2s;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.form-group input:focus {
|
| 120 |
+
border-color: var(--accent)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.btn-login {
|
| 124 |
+
width: 100%;
|
| 125 |
+
padding: 12px;
|
| 126 |
+
border: none;
|
| 127 |
+
border-radius: 8px;
|
| 128 |
+
cursor: pointer;
|
| 129 |
+
font-size: 14px;
|
| 130 |
+
font-weight: 600;
|
| 131 |
+
font-family: inherit;
|
| 132 |
+
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
| 133 |
+
color: #fff;
|
| 134 |
+
transition: opacity .2s, transform .15s;
|
| 135 |
+
margin-top: 4px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.btn-login:hover {
|
| 139 |
+
opacity: .9;
|
| 140 |
+
transform: translateY(-1px)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.login-error {
|
| 144 |
+
display: none;
|
| 145 |
+
background: rgba(239, 68, 68, .12);
|
| 146 |
+
border: 1px solid rgba(239, 68, 68, .3);
|
| 147 |
+
border-radius: 8px;
|
| 148 |
+
padding: 10px 14px;
|
| 149 |
+
font-size: 13px;
|
| 150 |
+
color: #fca5a5;
|
| 151 |
+
margin-top: 14px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
/* ── App Shell ── */
|
| 155 |
+
#app {
|
| 156 |
+
display: none;
|
| 157 |
+
height: 100vh;
|
| 158 |
+
display: grid;
|
| 159 |
+
grid-template-columns: 280px 1fr;
|
| 160 |
+
grid-template-rows: 60px 1fr
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.topbar {
|
| 164 |
+
grid-column: 1/-1;
|
| 165 |
+
display: flex;
|
| 166 |
+
align-items: center;
|
| 167 |
+
justify-content: space-between;
|
| 168 |
+
padding: 0 24px;
|
| 169 |
+
background: var(--surface);
|
| 170 |
+
border-bottom: 1px solid var(--border);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.topbar-brand {
|
| 174 |
+
display: flex;
|
| 175 |
+
align-items: center;
|
| 176 |
+
gap: 10px;
|
| 177 |
+
font-weight: 700;
|
| 178 |
+
font-size: 16px
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.topbar-brand .dot {
|
| 182 |
+
width: 8px;
|
| 183 |
+
height: 8px;
|
| 184 |
+
background: var(--green);
|
| 185 |
+
border-radius: 50%;
|
| 186 |
+
box-shadow: 0 0 8px var(--green);
|
| 187 |
+
animation: pulse 2s infinite
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.topbar-right {
|
| 191 |
+
display: flex;
|
| 192 |
+
align-items: center;
|
| 193 |
+
gap: 14px
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.badge {
|
| 197 |
+
font-size: 11px;
|
| 198 |
+
padding: 3px 10px;
|
| 199 |
+
border-radius: 20px;
|
| 200 |
+
background: rgba(59, 130, 246, .15);
|
| 201 |
+
color: var(--accent);
|
| 202 |
+
border: 1px solid rgba(59, 130, 246, .25)
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.btn-logout {
|
| 206 |
+
padding: 7px 16px;
|
| 207 |
+
border: 1px solid var(--border);
|
| 208 |
+
border-radius: 8px;
|
| 209 |
+
background: transparent;
|
| 210 |
+
color: var(--text2);
|
| 211 |
+
font-size: 13px;
|
| 212 |
+
font-family: inherit;
|
| 213 |
+
cursor: pointer;
|
| 214 |
+
transition: all .2s;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.btn-logout:hover {
|
| 218 |
+
border-color: var(--red);
|
| 219 |
+
color: var(--red)
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* ── Sidebar ── */
|
| 223 |
+
.sidebar {
|
| 224 |
+
background: var(--surface);
|
| 225 |
+
border-right: 1px solid var(--border);
|
| 226 |
+
overflow: hidden;
|
| 227 |
+
display: flex;
|
| 228 |
+
flex-direction: column;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.sidebar-head {
|
| 232 |
+
padding: 16px 20px;
|
| 233 |
+
border-bottom: 1px solid var(--border);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.sidebar-head h2 {
|
| 237 |
+
font-size: 12px;
|
| 238 |
+
font-weight: 600;
|
| 239 |
+
text-transform: uppercase;
|
| 240 |
+
letter-spacing: .08em;
|
| 241 |
+
color: var(--text3)
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.sidebar-stats {
|
| 245 |
+
display: flex;
|
| 246 |
+
gap: 16px;
|
| 247 |
+
margin-top: 10px
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.stat {
|
| 251 |
+
text-align: center;
|
| 252 |
+
flex: 1;
|
| 253 |
+
background: var(--surface2);
|
| 254 |
+
border-radius: 8px;
|
| 255 |
+
padding: 8px 4px
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.stat-val {
|
| 259 |
+
font-size: 20px;
|
| 260 |
+
font-weight: 700;
|
| 261 |
+
color: var(--accent)
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.stat-lbl {
|
| 265 |
+
font-size: 10px;
|
| 266 |
+
color: var(--text3);
|
| 267 |
+
margin-top: 1px
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.sidebar-actions {
|
| 271 |
+
padding: 12px 16px;
|
| 272 |
+
display: flex;
|
| 273 |
+
gap: 8px;
|
| 274 |
+
border-bottom: 1px solid var(--border)
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.btn-export {
|
| 278 |
+
flex: 1;
|
| 279 |
+
padding: 8px;
|
| 280 |
+
border: 1px solid var(--border);
|
| 281 |
+
border-radius: 8px;
|
| 282 |
+
background: var(--surface2);
|
| 283 |
+
color: var(--text2);
|
| 284 |
+
font-size: 12px;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
cursor: pointer;
|
| 287 |
+
display: flex;
|
| 288 |
+
align-items: center;
|
| 289 |
+
justify-content: center;
|
| 290 |
+
gap: 6px;
|
| 291 |
+
transition: all .2s;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.btn-export:hover {
|
| 295 |
+
border-color: var(--accent);
|
| 296 |
+
color: var(--accent);
|
| 297 |
+
background: rgba(59, 130, 246, 0.05)
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.export-dropdown {
|
| 301 |
+
position: relative;
|
| 302 |
+
display: inline-block;
|
| 303 |
+
flex: 1
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.export-menu {
|
| 307 |
+
position: absolute;
|
| 308 |
+
top: 100%;
|
| 309 |
+
left: 0;
|
| 310 |
+
right: 0;
|
| 311 |
+
background: var(--surface3);
|
| 312 |
+
border: 1px solid var(--border);
|
| 313 |
+
border-radius: 8px;
|
| 314 |
+
margin-top: 4px;
|
| 315 |
+
display: none;
|
| 316 |
+
z-index: 100;
|
| 317 |
+
box-shadow: var(--shadow);
|
| 318 |
+
overflow: hidden;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.export-menu button {
|
| 322 |
+
width: 100%;
|
| 323 |
+
padding: 10px;
|
| 324 |
+
border: none;
|
| 325 |
+
background: none;
|
| 326 |
+
color: var(--text2);
|
| 327 |
+
font-size: 12px;
|
| 328 |
+
text-align: left;
|
| 329 |
+
cursor: pointer;
|
| 330 |
+
transition: all .2s;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.export-menu button:hover {
|
| 334 |
+
background: var(--surface2);
|
| 335 |
+
color: var(--text)
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.export-dropdown:hover .export-menu {
|
| 339 |
+
display: block
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.search-wrap {
|
| 343 |
+
padding: 12px 16px;
|
| 344 |
+
border-bottom: 1px solid var(--border)
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.search-wrap input {
|
| 348 |
+
width: 100%;
|
| 349 |
+
padding: 8px 12px;
|
| 350 |
+
background: var(--surface2);
|
| 351 |
+
border: 1px solid var(--border);
|
| 352 |
+
border-radius: 8px;
|
| 353 |
+
color: var(--text);
|
| 354 |
+
font-size: 13px;
|
| 355 |
+
font-family: inherit;
|
| 356 |
+
outline: none;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.search-wrap input:focus {
|
| 360 |
+
border-color: var(--accent)
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.session-list {
|
| 364 |
+
flex: 1;
|
| 365 |
+
overflow-y: auto;
|
| 366 |
+
padding: 8px
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.session-list::-webkit-scrollbar {
|
| 370 |
+
width: 4px
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.session-list::-webkit-scrollbar-track {
|
| 374 |
+
background: transparent
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.session-list::-webkit-scrollbar-thumb {
|
| 378 |
+
background: var(--border);
|
| 379 |
+
border-radius: 4px
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.session-item {
|
| 383 |
+
padding: 12px 14px;
|
| 384 |
+
border-radius: 10px;
|
| 385 |
+
cursor: pointer;
|
| 386 |
+
margin-bottom: 4px;
|
| 387 |
+
border: 1px solid transparent;
|
| 388 |
+
transition: all .2s;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.session-item:hover {
|
| 392 |
+
background: var(--surface2);
|
| 393 |
+
border-color: var(--border)
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.session-item.active {
|
| 397 |
+
background: rgba(59, 130, 246, .1);
|
| 398 |
+
border-color: rgba(59, 130, 246, .35)
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.session-item-id {
|
| 402 |
+
font-size: 12px;
|
| 403 |
+
font-weight: 600;
|
| 404 |
+
color: var(--text);
|
| 405 |
+
white-space: nowrap;
|
| 406 |
+
overflow: hidden;
|
| 407 |
+
text-overflow: ellipsis
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.session-item-meta {
|
| 411 |
+
display: flex;
|
| 412 |
+
justify-content: space-between;
|
| 413 |
+
margin-top: 4px
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.session-item-time {
|
| 417 |
+
font-size: 11px;
|
| 418 |
+
color: var(--text3)
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.session-item-count {
|
| 422 |
+
font-size: 11px;
|
| 423 |
+
background: var(--surface3);
|
| 424 |
+
padding: 1px 7px;
|
| 425 |
+
border-radius: 20px;
|
| 426 |
+
color: var(--text2)
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.no-sessions {
|
| 430 |
+
text-align: center;
|
| 431 |
+
padding: 40px 20px;
|
| 432 |
+
color: var(--text3);
|
| 433 |
+
font-size: 13px
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
/* ── Main Content ── */
|
| 437 |
+
.main {
|
| 438 |
+
overflow: hidden;
|
| 439 |
+
display: flex;
|
| 440 |
+
flex-direction: column;
|
| 441 |
+
background: var(--bg)
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.main-placeholder {
|
| 445 |
+
flex: 1;
|
| 446 |
+
display: flex;
|
| 447 |
+
flex-direction: column;
|
| 448 |
+
align-items: center;
|
| 449 |
+
justify-content: center;
|
| 450 |
+
color: var(--text3);
|
| 451 |
+
gap: 14px;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.main-placeholder svg {
|
| 455 |
+
width: 56px;
|
| 456 |
+
height: 56px;
|
| 457 |
+
opacity: .3
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.main-placeholder p {
|
| 461 |
+
font-size: 14px
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.chat-view {
|
| 465 |
+
flex: 1;
|
| 466 |
+
display: flex;
|
| 467 |
+
flex-direction: column;
|
| 468 |
+
overflow: hidden
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.chat-header {
|
| 472 |
+
padding: 16px 24px;
|
| 473 |
+
background: var(--surface);
|
| 474 |
+
border-bottom: 1px solid var(--border);
|
| 475 |
+
display: flex;
|
| 476 |
+
align-items: center;
|
| 477 |
+
justify-content: space-between;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.chat-header-info h3 {
|
| 481 |
+
font-size: 14px;
|
| 482 |
+
font-weight: 600
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.chat-header-info p {
|
| 486 |
+
font-size: 12px;
|
| 487 |
+
color: var(--text3);
|
| 488 |
+
margin-top: 2px
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.chat-header-actions {
|
| 492 |
+
display: flex;
|
| 493 |
+
gap: 8px
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.btn-del {
|
| 497 |
+
padding: 6px 14px;
|
| 498 |
+
border: 1px solid rgba(239, 68, 68, .3);
|
| 499 |
+
border-radius: 8px;
|
| 500 |
+
background: rgba(239, 68, 68, .08);
|
| 501 |
+
color: #fca5a5;
|
| 502 |
+
font-size: 12px;
|
| 503 |
+
font-family: inherit;
|
| 504 |
+
cursor: pointer;
|
| 505 |
+
transition: all .2s;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.btn-del:hover {
|
| 509 |
+
background: rgba(239, 68, 68, .2)
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.messages-area {
|
| 513 |
+
flex: 1;
|
| 514 |
+
overflow-y: auto;
|
| 515 |
+
padding: 24px;
|
| 516 |
+
display: flex;
|
| 517 |
+
flex-direction: column;
|
| 518 |
+
gap: 16px
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.messages-area::-webkit-scrollbar {
|
| 522 |
+
width: 4px
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.messages-area::-webkit-scrollbar-thumb {
|
| 526 |
+
background: var(--border);
|
| 527 |
+
border-radius: 4px
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.msg-block {
|
| 531 |
+
display: flex;
|
| 532 |
+
flex-direction: column;
|
| 533 |
+
gap: 8px
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.msg-label {
|
| 537 |
+
font-size: 10px;
|
| 538 |
+
font-weight: 700;
|
| 539 |
+
text-transform: uppercase;
|
| 540 |
+
letter-spacing: .08em;
|
| 541 |
+
display: flex;
|
| 542 |
+
align-items: center;
|
| 543 |
+
gap: 6px
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.msg-label.q {
|
| 547 |
+
color: var(--accent)
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.msg-label.a {
|
| 551 |
+
color: var(--green)
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.msg-bubble {
|
| 555 |
+
padding: 13px 16px;
|
| 556 |
+
border-radius: 10px;
|
| 557 |
+
font-size: 14px;
|
| 558 |
+
line-height: 1.65;
|
| 559 |
+
white-space: pre-wrap;
|
| 560 |
+
word-break: break-word;
|
| 561 |
+
max-width: 100%;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.msg-bubble.q {
|
| 565 |
+
background: rgba(59, 130, 246, .1);
|
| 566 |
+
border: 1px solid rgba(59, 130, 246, .2);
|
| 567 |
+
color: var(--text)
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.msg-bubble.a {
|
| 571 |
+
background: rgba(16, 185, 129, .08);
|
| 572 |
+
border: 1px solid rgba(16, 185, 129, .2);
|
| 573 |
+
color: var(--text)
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.msg-ts {
|
| 577 |
+
font-size: 11px;
|
| 578 |
+
color: var(--text3);
|
| 579 |
+
margin-top: 2px
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.msg-num {
|
| 583 |
+
font-size: 11px;
|
| 584 |
+
background: var(--surface2);
|
| 585 |
+
padding: 1px 8px;
|
| 586 |
+
border-radius: 20px;
|
| 587 |
+
color: var(--text3)
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
.spinner {
|
| 591 |
+
width: 32px;
|
| 592 |
+
height: 32px;
|
| 593 |
+
border: 3px solid var(--border);
|
| 594 |
+
border-top-color: var(--accent);
|
| 595 |
+
border-radius: 50%;
|
| 596 |
+
animation: spin .7s linear infinite;
|
| 597 |
+
margin: auto;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.toast {
|
| 601 |
+
position: fixed;
|
| 602 |
+
bottom: 24px;
|
| 603 |
+
right: 24px;
|
| 604 |
+
padding: 12px 20px;
|
| 605 |
+
background: var(--surface2);
|
| 606 |
+
border: 1px solid var(--border);
|
| 607 |
+
border-radius: 10px;
|
| 608 |
+
font-size: 13px;
|
| 609 |
+
color: var(--text);
|
| 610 |
+
box-shadow: var(--shadow);
|
| 611 |
+
transform: translateY(20px);
|
| 612 |
+
opacity: 0;
|
| 613 |
+
transition: all .3s;
|
| 614 |
+
pointer-events: none;
|
| 615 |
+
z-index: 9998;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.toast.show {
|
| 619 |
+
transform: translateY(0);
|
| 620 |
+
opacity: 1
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
.toast.success {
|
| 624 |
+
border-color: rgba(16, 185, 129, .4);
|
| 625 |
+
color: #6ee7b7
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
.toast.error {
|
| 629 |
+
border-color: rgba(239, 68, 68, .4);
|
| 630 |
+
color: #fca5a5
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
/* ── Animations ── */
|
| 634 |
+
@keyframes fadeUp {
|
| 635 |
+
from {
|
| 636 |
+
opacity: 0;
|
| 637 |
+
transform: translateY(18px)
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
to {
|
| 641 |
+
opacity: 1;
|
| 642 |
+
transform: translateY(0)
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
@keyframes pulse {
|
| 647 |
+
|
| 648 |
+
0%,
|
| 649 |
+
100% {
|
| 650 |
+
opacity: 1
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
50% {
|
| 654 |
+
opacity: .4
|
| 655 |
+
}
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
@keyframes spin {
|
| 659 |
+
to {
|
| 660 |
+
transform: rotate(360deg)
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
</style>
|
| 664 |
+
</head>
|
| 665 |
+
|
| 666 |
+
<body>
|
| 667 |
+
|
| 668 |
+
<!-- LOGIN SCREEN -->
|
| 669 |
+
<div id="login-screen">
|
| 670 |
+
<div class="login-card">
|
| 671 |
+
<div class="login-logo">
|
| 672 |
+
<svg viewBox="0 0 52 52" fill="none">
|
| 673 |
+
<rect width="52" height="52" rx="14" fill="url(#lg)" />
|
| 674 |
+
<path d="M16 26h20M26 16v20" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
|
| 675 |
+
<defs>
|
| 676 |
+
<linearGradient id="lg" x1="0" y1="0" x2="52" y2="52">
|
| 677 |
+
<stop stop-color="#3b82f6" />
|
| 678 |
+
<stop offset="1" stop-color="#6366f1" />
|
| 679 |
+
</linearGradient>
|
| 680 |
+
</defs>
|
| 681 |
+
</svg>
|
| 682 |
+
<h1>Martechsol Admin</h1>
|
| 683 |
+
<p>Secure Chat Analytics Dashboard</p>
|
| 684 |
+
</div>
|
| 685 |
+
<!-- Stage 1: Username & Password -->
|
| 686 |
+
<div id="login-stage-1">
|
| 687 |
+
<div class="form-group">
|
| 688 |
+
<label>Username</label>
|
| 689 |
+
<input type="text" id="inp-user" placeholder="Enter username" autocomplete="username" />
|
| 690 |
+
</div>
|
| 691 |
+
<div class="form-group">
|
| 692 |
+
<label>Password</label>
|
| 693 |
+
<input type="password" id="inp-pass" placeholder="Enter password" autocomplete="current-password" />
|
| 694 |
+
</div>
|
| 695 |
+
<button class="btn-login" id="btn-login">Sign In</button>
|
| 696 |
+
</div>
|
| 697 |
+
|
| 698 |
+
<!-- Stage 2: OTP (Hidden by default) -->
|
| 699 |
+
<div id="login-stage-2" style="display:none">
|
| 700 |
+
<div class="form-group">
|
| 701 |
+
<label>One-Time Password (OTP)</label>
|
| 702 |
+
<input type="text" id="inp-otp" placeholder="6-digit code" maxlength="6"
|
| 703 |
+
style="text-align:center;letter-spacing:0.5em;font-size:18px;font-weight:700" />
|
| 704 |
+
<p id="otp-hint" style="font-size:11px;color:var(--text3);margin-top:8px;text-align:center">Code sent to your
|
| 705 |
+
email</p>
|
| 706 |
+
</div>
|
| 707 |
+
<button class="btn-login" id="btn-verify">Verify & Enter</button>
|
| 708 |
+
<button class="btn-logout" id="btn-back" style="width:100%;margin-top:10px;border:none">Back to Login</button>
|
| 709 |
+
</div>
|
| 710 |
+
|
| 711 |
+
<div class="login-error" id="login-error">Invalid credentials. Please try again.</div>
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
|
| 715 |
+
<!-- APP SHELL (hidden until login) -->
|
| 716 |
+
<div id="app" style="display:none">
|
| 717 |
+
<!-- Topbar -->
|
| 718 |
+
<header class="topbar">
|
| 719 |
+
<div class="topbar-brand">
|
| 720 |
+
<div class="dot"></div>
|
| 721 |
+
Martechsol Admin Panel
|
| 722 |
+
</div>
|
| 723 |
+
<div class="topbar-right">
|
| 724 |
+
<span class="badge" id="total-badge">Loading…</span>
|
| 725 |
+
<button class="btn-logout" id="btn-logout">Sign Out</button>
|
| 726 |
+
</div>
|
| 727 |
+
</header>
|
| 728 |
+
|
| 729 |
+
<!-- Sidebar -->
|
| 730 |
+
<aside class="sidebar">
|
| 731 |
+
<div class="sidebar-head">
|
| 732 |
+
<h2>Sessions</h2>
|
| 733 |
+
<div class="sidebar-stats">
|
| 734 |
+
<div class="stat">
|
| 735 |
+
<div class="stat-val" id="stat-total">—</div>
|
| 736 |
+
<div class="stat-lbl">Total</div>
|
| 737 |
+
</div>
|
| 738 |
+
<div class="stat">
|
| 739 |
+
<div class="stat-val" id="stat-msgs">—</div>
|
| 740 |
+
<div class="stat-lbl">Messages</div>
|
| 741 |
+
</div>
|
| 742 |
+
<div class="stat">
|
| 743 |
+
<div class="stat-val" id="stat-today">—</div>
|
| 744 |
+
<div class="stat-lbl">Today</div>
|
| 745 |
+
</div>
|
| 746 |
+
</div>
|
| 747 |
+
</div>
|
| 748 |
+
<div class="sidebar-actions">
|
| 749 |
+
<div class="export-dropdown">
|
| 750 |
+
<button class="btn-export">
|
| 751 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 752 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
| 753 |
+
</svg>
|
| 754 |
+
Export All
|
| 755 |
+
</button>
|
| 756 |
+
<div class="export-menu">
|
| 757 |
+
<button id="btn-export-all-excel">Excel (.xlsx)</button>
|
| 758 |
+
<button id="btn-export-all-json">JSON (.json)</button>
|
| 759 |
+
</div>
|
| 760 |
+
</div>
|
| 761 |
+
</div>
|
| 762 |
+
<div class="search-wrap">
|
| 763 |
+
<input type="text" id="search-inp" placeholder="Search sessions…" />
|
| 764 |
+
</div>
|
| 765 |
+
<div class="session-list" id="session-list">
|
| 766 |
+
<div class="no-sessions">Loading sessions…</div>
|
| 767 |
+
</div>
|
| 768 |
+
</aside>
|
| 769 |
+
|
| 770 |
+
<!-- Main -->
|
| 771 |
+
<main class="main" id="main-area">
|
| 772 |
+
<div class="main-placeholder" id="placeholder">
|
| 773 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 774 |
+
<path
|
| 775 |
+
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
| 776 |
+
stroke-linecap="round" stroke-linejoin="round" />
|
| 777 |
+
</svg>
|
| 778 |
+
<p>Select a session from the sidebar to view the transcript</p>
|
| 779 |
+
</div>
|
| 780 |
+
<div class="chat-view" id="chat-view" style="display:none">
|
| 781 |
+
<div class="chat-header">
|
| 782 |
+
<div class="chat-header-info">
|
| 783 |
+
<h3 id="view-session-id">Session ID</h3>
|
| 784 |
+
<p id="view-session-user" style="font-weight:600;color:var(--accent);margin-top:2px"></p>
|
| 785 |
+
<p id="view-session-meta">Loading…</p>
|
| 786 |
+
</div>
|
| 787 |
+
<div class="chat-header-actions">
|
| 788 |
+
<div class="export-dropdown">
|
| 789 |
+
<button class="btn-export" style="padding: 6px 12px;">
|
| 790 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 791 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
|
| 792 |
+
</svg>
|
| 793 |
+
Export Session
|
| 794 |
+
</button>
|
| 795 |
+
<div class="export-menu" style="right:0;left:auto;min-width:120px">
|
| 796 |
+
<button id="btn-export-session-excel">Excel (.xlsx)</button>
|
| 797 |
+
<button id="btn-export-session-json">JSON (.json)</button>
|
| 798 |
+
</div>
|
| 799 |
+
</div>
|
| 800 |
+
<button class="btn-del" id="btn-delete-session">Delete Session</button>
|
| 801 |
+
</div>
|
| 802 |
+
</div>
|
| 803 |
+
<div class="messages-area" id="messages-area"></div>
|
| 804 |
+
</div>
|
| 805 |
+
</main>
|
| 806 |
+
</div>
|
| 807 |
+
|
| 808 |
+
<div class="toast" id="toast"></div>
|
| 809 |
+
|
| 810 |
+
<script>
|
| 811 |
+
(function () {
|
| 812 |
+
const CREDS = { user: 'martech_admin', pass: 'martech_admin_303' };
|
| 813 |
+
let _creds = null;
|
| 814 |
+
let _allSessions = [];
|
| 815 |
+
let _activeId = null;
|
| 816 |
+
|
| 817 |
+
const STORAGE_KEY = 'martech_admin_creds';
|
| 818 |
+
|
| 819 |
+
// ── Helpers ──────────────────────────────────────────────
|
| 820 |
+
function $(id) { return document.getElementById(id) }
|
| 821 |
+
function authHeader() { return 'Basic ' + btoa(_creds.user + ':' + _creds.pass) }
|
| 822 |
+
|
| 823 |
+
function showToast(msg, type = '') {
|
| 824 |
+
const t = $('toast');
|
| 825 |
+
t.textContent = msg; t.className = 'toast ' + (type || '');
|
| 826 |
+
t.classList.add('show');
|
| 827 |
+
setTimeout(() => t.classList.remove('show'), 3000);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
function fmtDate(iso) {
|
| 831 |
+
if (!iso) return '—';
|
| 832 |
+
const d = new Date(iso);
|
| 833 |
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
function shortId(id) {
|
| 837 |
+
if (!id) return '—';
|
| 838 |
+
return id.length > 16 ? id.slice(0, 8) + '…' + id.slice(-6) : id;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
function todayCount(sessions) {
|
| 842 |
+
const today = new Date().toDateString();
|
| 843 |
+
return sessions.filter(s => {
|
| 844 |
+
try { return new Date(s.created_at).toDateString() === today } catch { return false }
|
| 845 |
+
}).length;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
// ── Login ─────────────────────────────────────────────────
|
| 849 |
+
async function doLogin() {
|
| 850 |
+
const u = $('inp-user').value.trim();
|
| 851 |
+
const p = $('inp-pass').value;
|
| 852 |
+
if (!u || !p) return;
|
| 853 |
+
|
| 854 |
+
$('btn-login').textContent = 'Authenticating…';
|
| 855 |
+
$('btn-login').disabled = true;
|
| 856 |
+
$('login-error').style.display = 'none';
|
| 857 |
+
|
| 858 |
+
try {
|
| 859 |
+
const r = await fetch('/api/admin/request-otp', {
|
| 860 |
+
method: 'POST',
|
| 861 |
+
headers: { 'Content-Type': 'application/json' },
|
| 862 |
+
body: JSON.stringify({ username: u, password: p })
|
| 863 |
+
});
|
| 864 |
+
|
| 865 |
+
if (r.ok) {
|
| 866 |
+
const data = await r.json();
|
| 867 |
+
// Switch to OTP stage
|
| 868 |
+
$('login-stage-1').style.display = 'none';
|
| 869 |
+
$('login-stage-2').style.display = 'block';
|
| 870 |
+
if (data.email_status === 'logged_to_console_only') {
|
| 871 |
+
$('otp-hint').textContent = 'SMTP not configured. Check server logs.';
|
| 872 |
+
$('otp-hint').style.color = 'var(--yellow)';
|
| 873 |
+
} else {
|
| 874 |
+
$('otp-hint').textContent = 'Code sent to your registered email.';
|
| 875 |
+
$('otp-hint').style.color = 'var(--text3)';
|
| 876 |
+
}
|
| 877 |
+
$('inp-otp').focus();
|
| 878 |
+
} else {
|
| 879 |
+
const err = await r.json();
|
| 880 |
+
$('login-error').textContent = err.detail || 'Login failed.';
|
| 881 |
+
$('login-error').style.display = 'block';
|
| 882 |
+
}
|
| 883 |
+
} catch (e) {
|
| 884 |
+
$('login-error').textContent = 'Network error: ' + e.message;
|
| 885 |
+
$('login-error').style.display = 'block';
|
| 886 |
+
} finally {
|
| 887 |
+
$('btn-login').textContent = 'Sign In';
|
| 888 |
+
$('btn-login').disabled = false;
|
| 889 |
+
}
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
async function doVerifyOtp() {
|
| 893 |
+
const u = $('inp-user').value.trim();
|
| 894 |
+
const p = $('inp-pass').value;
|
| 895 |
+
const otp = $('inp-otp').value.trim();
|
| 896 |
+
if (!otp) return;
|
| 897 |
+
|
| 898 |
+
$('btn-verify').textContent = 'Verifying…';
|
| 899 |
+
$('btn-verify').disabled = true;
|
| 900 |
+
$('login-error').style.display = 'none';
|
| 901 |
+
|
| 902 |
+
try {
|
| 903 |
+
const r = await fetch('/api/admin/verify-otp', {
|
| 904 |
+
method: 'POST',
|
| 905 |
+
headers: { 'Content-Type': 'application/json' },
|
| 906 |
+
body: JSON.stringify({ username: u, password: p, code: otp })
|
| 907 |
+
});
|
| 908 |
+
|
| 909 |
+
if (r.ok) {
|
| 910 |
+
_creds = { user: u, pass: p, otp_verified: true };
|
| 911 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(_creds));
|
| 912 |
+
$('login-screen').style.display = 'none';
|
| 913 |
+
$('app').style.display = 'grid';
|
| 914 |
+
loadSessions();
|
| 915 |
+
showToast('Login successful', 'success');
|
| 916 |
+
} else {
|
| 917 |
+
const err = await r.json();
|
| 918 |
+
$('login-error').textContent = err.detail || 'Invalid OTP.';
|
| 919 |
+
$('login-error').style.display = 'block';
|
| 920 |
+
$('inp-otp').value = '';
|
| 921 |
+
}
|
| 922 |
+
} catch (e) {
|
| 923 |
+
$('login-error').textContent = 'Network error: ' + e.message;
|
| 924 |
+
$('login-error').style.display = 'block';
|
| 925 |
+
} finally {
|
| 926 |
+
$('btn-verify').textContent = 'Verify & Enter';
|
| 927 |
+
$('btn-verify').disabled = false;
|
| 928 |
+
}
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
$('btn-login').addEventListener('click', doLogin);
|
| 932 |
+
$('btn-verify').addEventListener('click', doVerifyOtp);
|
| 933 |
+
$('btn-back').addEventListener('click', () => {
|
| 934 |
+
$('login-stage-2').style.display = 'none';
|
| 935 |
+
$('login-stage-1').style.display = 'block';
|
| 936 |
+
$('login-error').style.display = 'none';
|
| 937 |
+
$('inp-otp').value = '';
|
| 938 |
+
});
|
| 939 |
+
|
| 940 |
+
[$('inp-user'), $('inp-pass')].forEach(el => el.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin() }));
|
| 941 |
+
$('inp-otp').addEventListener('keydown', e => { if (e.key === 'Enter') doVerifyOtp() });
|
| 942 |
+
|
| 943 |
+
$('btn-logout').addEventListener('click', () => {
|
| 944 |
+
_creds = null; _allSessions = []; _activeId = null;
|
| 945 |
+
localStorage.removeItem(STORAGE_KEY);
|
| 946 |
+
$('app').style.display = 'none';
|
| 947 |
+
$('login-screen').style.display = 'flex';
|
| 948 |
+
$('login-stage-2').style.display = 'none';
|
| 949 |
+
$('login-stage-1').style.display = 'block';
|
| 950 |
+
$('inp-pass').value = '';
|
| 951 |
+
$('inp-otp').value = '';
|
| 952 |
+
$('login-error').style.display = 'none';
|
| 953 |
+
});
|
| 954 |
+
|
| 955 |
+
// ── Auto-Login ────────────────────────────────────────────
|
| 956 |
+
(function init() {
|
| 957 |
+
const stored = localStorage.getItem(STORAGE_KEY);
|
| 958 |
+
if (stored) {
|
| 959 |
+
try {
|
| 960 |
+
const parsed = JSON.parse(stored);
|
| 961 |
+
if (parsed.user === CREDS.user && parsed.pass === CREDS.pass && parsed.otp_verified) {
|
| 962 |
+
_creds = parsed;
|
| 963 |
+
$('login-screen').style.display = 'none';
|
| 964 |
+
$('app').style.display = 'grid';
|
| 965 |
+
loadSessions();
|
| 966 |
+
} else {
|
| 967 |
+
// Force re-login if OTP was not verified
|
| 968 |
+
localStorage.removeItem(STORAGE_KEY);
|
| 969 |
+
}
|
| 970 |
+
} catch (e) {
|
| 971 |
+
localStorage.removeItem(STORAGE_KEY);
|
| 972 |
+
}
|
| 973 |
+
}
|
| 974 |
+
})();
|
| 975 |
+
|
| 976 |
+
// ── Sessions list ─────────────────────────────────────────
|
| 977 |
+
async function loadSessions() {
|
| 978 |
+
$('session-list').innerHTML = '<div class="no-sessions"><div class="spinner"></div></div>';
|
| 979 |
+
try {
|
| 980 |
+
const r = await fetch('/api/admin/sessions', { headers: { Authorization: authHeader() } });
|
| 981 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 982 |
+
const data = await r.json();
|
| 983 |
+
_allSessions = data.sessions || [];
|
| 984 |
+
renderSidebar(_allSessions);
|
| 985 |
+
updateStats(_allSessions);
|
| 986 |
+
} catch (e) {
|
| 987 |
+
$('session-list').innerHTML = '<div class="no-sessions">Failed to load sessions.</div>';
|
| 988 |
+
showToast('Could not fetch sessions: ' + e.message, 'error');
|
| 989 |
+
}
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
function updateStats(sessions) {
|
| 993 |
+
const totalMsgs = sessions.reduce((a, s) => a + (s.message_count || 0), 0);
|
| 994 |
+
$('stat-total').textContent = sessions.length;
|
| 995 |
+
$('stat-msgs').textContent = totalMsgs;
|
| 996 |
+
$('stat-today').textContent = todayCount(sessions);
|
| 997 |
+
$('total-badge').textContent = sessions.length + ' Sessions';
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
function renderSidebar(sessions) {
|
| 1001 |
+
if (!sessions.length) {
|
| 1002 |
+
$('session-list').innerHTML = '<div class="no-sessions">No sessions yet.</div>';
|
| 1003 |
+
return;
|
| 1004 |
+
}
|
| 1005 |
+
$('session-list').innerHTML = sessions.map(s => `
|
| 1006 |
+
<div class="session-item${s.session_id === _activeId ? ' active' : ''}"
|
| 1007 |
+
data-id="${s.session_id}">
|
| 1008 |
+
<div class="session-item-id" title="${s.user_name || s.session_id}">${s.user_name || shortId(s.session_id)}</div>
|
| 1009 |
+
<div class="session-item-meta">
|
| 1010 |
+
<span class="session-item-time">${fmtDate(s.last_active)}</span>
|
| 1011 |
+
<span class="session-item-count">${s.message_count || 0} msg${s.message_count !== 1 ? 's' : ''}</span>
|
| 1012 |
+
</div>
|
| 1013 |
+
</div>
|
| 1014 |
+
`).join('');
|
| 1015 |
+
document.querySelectorAll('.session-item').forEach(el => {
|
| 1016 |
+
el.addEventListener('click', () => openSession(el.dataset.id));
|
| 1017 |
+
});
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
+
$('search-inp').addEventListener('input', e => {
|
| 1021 |
+
const q = e.target.value.toLowerCase();
|
| 1022 |
+
const filtered = _allSessions.filter(s => s.session_id.toLowerCase().includes(q));
|
| 1023 |
+
renderSidebar(filtered);
|
| 1024 |
+
});
|
| 1025 |
+
|
| 1026 |
+
// ── Session detail ────────────────────────────────────────
|
| 1027 |
+
async function openSession(id) {
|
| 1028 |
+
_activeId = id;
|
| 1029 |
+
document.querySelectorAll('.session-item').forEach(el => {
|
| 1030 |
+
el.classList.toggle('active', el.dataset.id === id);
|
| 1031 |
+
});
|
| 1032 |
+
$('placeholder').style.display = 'none';
|
| 1033 |
+
$('chat-view').style.display = 'flex';
|
| 1034 |
+
$('view-session-id').textContent = shortId(id);
|
| 1035 |
+
$('view-session-user').textContent = '';
|
| 1036 |
+
$('view-session-meta').textContent = 'Loading…';
|
| 1037 |
+
$('messages-area').innerHTML = '<div class="spinner" style="margin-top:60px"></div>';
|
| 1038 |
+
|
| 1039 |
+
try {
|
| 1040 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(id),
|
| 1041 |
+
{ headers: { Authorization: authHeader() } });
|
| 1042 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 1043 |
+
const data = await r.json();
|
| 1044 |
+
renderSession(data);
|
| 1045 |
+
} catch (e) {
|
| 1046 |
+
$('messages-area').innerHTML = '<div style="color:var(--red);padding:20px">Failed to load session: ' + e.message + '</div>';
|
| 1047 |
+
}
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
function renderSession(data) {
|
| 1051 |
+
$('view-session-user').textContent = data.user_name || 'Guest User';
|
| 1052 |
+
$('view-session-meta').textContent =
|
| 1053 |
+
'Created: ' + fmtDate(data.created_at) + ' | Last active: ' + fmtDate(data.last_active) +
|
| 1054 |
+
' | ' + (data.message_count || 0) + ' messages';
|
| 1055 |
+
|
| 1056 |
+
const msgs = data.messages || [];
|
| 1057 |
+
if (!msgs.length) {
|
| 1058 |
+
$('messages-area').innerHTML = '<div style="color:var(--text3);text-align:center;margin-top:40px">No messages in this session.</div>';
|
| 1059 |
+
return;
|
| 1060 |
+
}
|
| 1061 |
+
$('messages-area').innerHTML = msgs.map(m => `
|
| 1062 |
+
<div class="msg-block">
|
| 1063 |
+
<div style="display:flex;align-items:center;gap:8px">
|
| 1064 |
+
<span class="msg-label q">
|
| 1065 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
|
| 1066 |
+
User Question
|
| 1067 |
+
</span>
|
| 1068 |
+
<span class="msg-num">#${m.id || '—'}</span>
|
| 1069 |
+
<span class="msg-ts">${fmtDate(m.timestamp)}</span>
|
| 1070 |
+
</div>
|
| 1071 |
+
<div class="msg-bubble q">${escHtml(m.question)}</div>
|
| 1072 |
+
<div class="msg-label a" style="margin-top:6px">
|
| 1073 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
|
| 1074 |
+
Assistant Answer
|
| 1075 |
+
</div>
|
| 1076 |
+
<div class="msg-bubble a">${escHtml(m.answer)}</div>
|
| 1077 |
+
</div>
|
| 1078 |
+
`).join('');
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
function escHtml(str) {
|
| 1082 |
+
return String(str || '')
|
| 1083 |
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
| 1084 |
+
.replace(/"/g, '"');
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
// ── Delete session ────────────────────────────────────────
|
| 1088 |
+
$('btn-delete-session').addEventListener('click', async () => {
|
| 1089 |
+
if (!_activeId) return;
|
| 1090 |
+
if (!confirm('Delete session ' + shortId(_activeId) + '? This cannot be undone.')) return;
|
| 1091 |
+
try {
|
| 1092 |
+
const r = await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId),
|
| 1093 |
+
{ method: 'DELETE', headers: { Authorization: authHeader() } });
|
| 1094 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 1095 |
+
showToast('Session deleted.', 'success');
|
| 1096 |
+
_allSessions = _allSessions.filter(s => s.session_id !== _activeId);
|
| 1097 |
+
_activeId = null;
|
| 1098 |
+
$('chat-view').style.display = 'none';
|
| 1099 |
+
$('placeholder').style.display = 'flex';
|
| 1100 |
+
renderSidebar(_allSessions);
|
| 1101 |
+
updateStats(_allSessions);
|
| 1102 |
+
} catch (e) { showToast('Delete failed: ' + e.message, 'error') }
|
| 1103 |
+
});
|
| 1104 |
+
|
| 1105 |
+
// ── Exports ───────────────────────────────────────────────
|
| 1106 |
+
async function downloadExport(url, filename) {
|
| 1107 |
+
try {
|
| 1108 |
+
const r = await fetch(url, { headers: { Authorization: authHeader() } });
|
| 1109 |
+
if (!r.ok) throw new Error('Export failed');
|
| 1110 |
+
const blob = await r.blob();
|
| 1111 |
+
const link = document.createElement('a');
|
| 1112 |
+
link.href = window.URL.createObjectURL(blob);
|
| 1113 |
+
link.download = filename;
|
| 1114 |
+
link.click();
|
| 1115 |
+
showToast('Export successful', 'success');
|
| 1116 |
+
} catch (e) {
|
| 1117 |
+
showToast('Export failed: ' + e.message, 'error');
|
| 1118 |
+
}
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
$('btn-export-all-excel').addEventListener('click', () => {
|
| 1122 |
+
downloadExport('/api/admin/export/all?format=excel', 'all_chats_export.xlsx');
|
| 1123 |
+
});
|
| 1124 |
+
|
| 1125 |
+
$('btn-export-all-json').addEventListener('click', () => {
|
| 1126 |
+
downloadExport('/api/admin/export/all?format=json', 'all_chats_export.json');
|
| 1127 |
+
});
|
| 1128 |
+
|
| 1129 |
+
$('btn-export-session-excel').addEventListener('click', () => {
|
| 1130 |
+
if (!_activeId) return;
|
| 1131 |
+
downloadExport(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=excel`, `session_${_activeId}.xlsx`);
|
| 1132 |
+
});
|
| 1133 |
+
|
| 1134 |
+
$('btn-export-session-json').addEventListener('click', () => {
|
| 1135 |
+
if (!_activeId) return;
|
| 1136 |
+
downloadExport(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=json`, `session_${_activeId}.json`);
|
| 1137 |
+
});
|
| 1138 |
+
|
| 1139 |
+
})();
|
| 1140 |
+
</script>
|
| 1141 |
+
</body>
|
| 1142 |
+
|
| 1143 |
+
</html>
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/schemas.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ChatRequest(BaseModel):
|
| 7 |
+
message: str = Field(..., min_length=1)
|
| 8 |
+
history: List[Dict[str, str]] = Field(default_factory=list)
|
| 9 |
+
session_id: str | None = Field(default=None, description="Unique session identifier for chat persistence")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ChatResponse(BaseModel):
|
| 13 |
+
reply: str
|
| 14 |
+
retrieved_chunks: List[Dict[str, Any]] = Field(default_factory=list)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class HealthResponse(BaseModel):
|
| 18 |
+
status: str
|
| 19 |
+
docs_loaded: bool
|
| 20 |
+
index_ready: bool
|
| 21 |
+
chunk_count: int
|
| 22 |
+
index_path: str
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SessionUpdateNameRequest(BaseModel):
|
| 26 |
+
session_id: str
|
| 27 |
+
user_name: str
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/config.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import lru_cache
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from pydantic import Field, model_validator
|
| 4 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Settings(BaseSettings):
|
| 8 |
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
| 9 |
+
|
| 10 |
+
app_name: str = "Fast RAG Chatbot"
|
| 11 |
+
llm_provider: str = Field(default="groq", alias="LLM_PROVIDER")
|
| 12 |
+
groq_api_key: str = Field(default="", alias="GROQ_API_KEY")
|
| 13 |
+
groq_model: str = Field(default="llama-3.1-8b-instant", alias="GROQ_MODEL")
|
| 14 |
+
hf_api_key: str = Field(default="", alias="HF_API_KEY")
|
| 15 |
+
hf_model: str = Field(default="meta-llama/Llama-3.1-8B-Instruct", alias="HF_MODEL")
|
| 16 |
+
embedding_model: str = Field(default="BAAI/bge-small-en-v1.5", alias="EMBEDDING_MODEL")
|
| 17 |
+
reranker_model: str = Field(default="cross-encoder/ms-marco-MiniLM-L-6-v2", alias="RERANKER_MODEL")
|
| 18 |
+
docs_dir: Path = Field(default=Path("docs"), alias="DOCS_DIR")
|
| 19 |
+
@model_validator(mode='after')
|
| 20 |
+
def set_persistent_paths(self) -> 'Settings':
|
| 21 |
+
# If we are on Hugging Face (detected by /data volume),
|
| 22 |
+
# force use of /data for persistence regardless of other settings.
|
| 23 |
+
if Path("/data").exists():
|
| 24 |
+
self.index_dir = Path("/data/index")
|
| 25 |
+
self.sessions_dir = Path("/data/sessions")
|
| 26 |
+
return self
|
| 27 |
+
|
| 28 |
+
index_dir: Path = Field(default=Path("data/index"), alias="INDEX_DIR")
|
| 29 |
+
sessions_dir: Path = Field(default=Path("data/sessions"), alias="SESSIONS_DIR")
|
| 30 |
+
chunk_size_tokens: int = 200 # Smaller chunks for higher precision
|
| 31 |
+
chunk_overlap_tokens: int = 50
|
| 32 |
+
top_k: int = Field(default=15, alias="TOP_K") # More for reranking
|
| 33 |
+
max_context_chunks: int = 6 # Allow more context for ambiguity detection
|
| 34 |
+
request_timeout_s: float = 20.0
|
| 35 |
+
cors_allow_origins: str = Field(default="*", alias="CORS_ALLOW_ORIGINS")
|
| 36 |
+
api_key: str = Field(default="", alias="API_KEY")
|
| 37 |
+
rate_limit_requests: int = Field(default=60, alias="RATE_LIMIT_REQUESTS")
|
| 38 |
+
rate_limit_window_seconds: int = Field(default=60, alias="RATE_LIMIT_WINDOW_SECONDS")
|
| 39 |
+
|
| 40 |
+
# SMTP Settings for OTP
|
| 41 |
+
smtp_server: str = Field(default="smtp.gmail.com", alias="SMTP_SERVER")
|
| 42 |
+
smtp_port: int = Field(default=465, alias="SMTP_PORT")
|
| 43 |
+
smtp_user: str = Field(default="", alias="SMTP_USER")
|
| 44 |
+
smtp_pass: str = Field(default="", alias="SMTP_PASS")
|
| 45 |
+
admin_email: str = Field(default="randomjoedown@gmail.com", alias="ADMIN_EMAIL")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@lru_cache
|
| 49 |
+
def get_settings() -> Settings:
|
| 50 |
+
return Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI, HTTPException, Header, Request
|
| 4 |
+
from fastapi.responses import RedirectResponse
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.staticfiles import StaticFiles
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
from app.api.schemas import ChatRequest, ChatResponse, HealthResponse, SessionUpdateNameRequest
|
| 10 |
+
from app.core.config import get_settings
|
| 11 |
+
from app.services.embeddings import EmbeddingService
|
| 12 |
+
from app.services.llm import LLMService
|
| 13 |
+
from app.services.rag_pipeline import RAGPipeline
|
| 14 |
+
from app.services.rate_limiter import InMemoryRateLimiter
|
| 15 |
+
from app.services.vector_store import FaissVectorStore
|
| 16 |
+
from app.services.reranker import RerankerService
|
| 17 |
+
from app.ui_gradio import demo
|
| 18 |
+
from app.admin.router import admin_router
|
| 19 |
+
from app.services import session_store as _ss
|
| 20 |
+
import asyncio
|
| 21 |
+
import gradio as gr
|
| 22 |
+
|
| 23 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s - %(message)s")
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
settings = get_settings()
|
| 27 |
+
embedding_service = EmbeddingService(settings.embedding_model)
|
| 28 |
+
vector_store = FaissVectorStore(
|
| 29 |
+
embedding_service=embedding_service,
|
| 30 |
+
docs_dir=settings.docs_dir,
|
| 31 |
+
index_dir=settings.index_dir,
|
| 32 |
+
chunk_size_tokens=settings.chunk_size_tokens,
|
| 33 |
+
chunk_overlap_tokens=settings.chunk_overlap_tokens,
|
| 34 |
+
)
|
| 35 |
+
llm_service = LLMService(
|
| 36 |
+
provider=settings.llm_provider,
|
| 37 |
+
groq_api_key=settings.groq_api_key,
|
| 38 |
+
groq_model=settings.groq_model,
|
| 39 |
+
hf_api_key=settings.hf_api_key,
|
| 40 |
+
hf_model=settings.hf_model,
|
| 41 |
+
timeout_s=settings.request_timeout_s,
|
| 42 |
+
)
|
| 43 |
+
reranker_service = RerankerService(settings.reranker_model)
|
| 44 |
+
pipeline = RAGPipeline(
|
| 45 |
+
vector_store=vector_store,
|
| 46 |
+
llm_service=llm_service,
|
| 47 |
+
reranker=reranker_service,
|
| 48 |
+
top_k=settings.top_k,
|
| 49 |
+
max_context_chunks=settings.max_context_chunks
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# ── FastAPI App ───────────────────────────────────────────────────────────────
|
| 53 |
+
fastapi_app = FastAPI(title=settings.app_name)
|
| 54 |
+
|
| 55 |
+
allow_origins = [o.strip() for o in settings.cors_allow_origins.split(",") if o.strip()]
|
| 56 |
+
fastapi_app.add_middleware(
|
| 57 |
+
CORSMiddleware,
|
| 58 |
+
allow_origins=["*"], # Explicitly allow all for testing
|
| 59 |
+
allow_credentials=True,
|
| 60 |
+
allow_methods=["*"],
|
| 61 |
+
allow_headers=["*"],
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
rate_limiter = InMemoryRateLimiter(settings.rate_limit_requests, settings.rate_limit_window_seconds)
|
| 65 |
+
|
| 66 |
+
# ── Admin Panel (mounted on FastAPI before Gradio wrapping) ───────────────────
|
| 67 |
+
fastapi_app.include_router(admin_router)
|
| 68 |
+
|
| 69 |
+
# ── Static Files ──────────────────────────────────────────────────────────────
|
| 70 |
+
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
| 71 |
+
if os.path.exists(static_dir):
|
| 72 |
+
fastapi_app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 73 |
+
|
| 74 |
+
@fastapi_app.get("/widget", include_in_schema=False)
|
| 75 |
+
async def get_widget():
|
| 76 |
+
"""Serves the chat widget HTML with explicit CORS headers."""
|
| 77 |
+
from fastapi.responses import FileResponse
|
| 78 |
+
path = os.path.join(static_dir, "addon.html")
|
| 79 |
+
if not os.path.exists(path):
|
| 80 |
+
raise HTTPException(status_code=404, detail="Widget not found")
|
| 81 |
+
return FileResponse(
|
| 82 |
+
path,
|
| 83 |
+
headers={
|
| 84 |
+
"Access-Control-Allow-Origin": "*",
|
| 85 |
+
"Cache-Control": "no-cache, no-store, must-revalidate"
|
| 86 |
+
}
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@fastapi_app.on_event("startup")
|
| 91 |
+
def startup_event() -> None:
|
| 92 |
+
logger.info("─── RAG SERVICE STARTUP ───")
|
| 93 |
+
logger.info("Index Directory: %s", settings.index_dir)
|
| 94 |
+
logger.info("Sessions Directory: %s", settings.sessions_dir)
|
| 95 |
+
|
| 96 |
+
# Verify persistence mount
|
| 97 |
+
if str(settings.index_dir).startswith("/data"):
|
| 98 |
+
if os.path.exists("/data"):
|
| 99 |
+
logger.info("✅ Persistent storage /data detected and in use.")
|
| 100 |
+
else:
|
| 101 |
+
logger.warning("⚠️ /data requested but NOT found on filesystem!")
|
| 102 |
+
else:
|
| 103 |
+
logger.warning("⚠️ Using ephemeral storage (data will be lost on rebuild).")
|
| 104 |
+
|
| 105 |
+
logger.info("Loading/building FAISS index...")
|
| 106 |
+
vector_store.build_or_load()
|
| 107 |
+
logger.info("RAG service started with %s chunks", len(vector_store.metadata))
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@fastapi_app.get("/", include_in_schema=False)
|
| 111 |
+
async def root():
|
| 112 |
+
"""Prevent direct browser access to the root URL."""
|
| 113 |
+
raise HTTPException(
|
| 114 |
+
status_code=403,
|
| 115 |
+
detail="Direct access to this resource is restricted. Please use the authorized interface."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
@fastapi_app.get("/health", response_model=HealthResponse)
|
| 120 |
+
def health() -> HealthResponse:
|
| 121 |
+
h = vector_store.health()
|
| 122 |
+
return HealthResponse(status="ok", **h)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@fastapi_app.post("/api/chat", response_model=ChatResponse)
|
| 126 |
+
async def chat(
|
| 127 |
+
payload: ChatRequest,
|
| 128 |
+
request: Request,
|
| 129 |
+
x_api_key: str | None = Header(default=None),
|
| 130 |
+
) -> ChatResponse:
|
| 131 |
+
if not payload.message.strip():
|
| 132 |
+
raise HTTPException(status_code=400, detail="message must not be empty")
|
| 133 |
+
if settings.api_key and x_api_key != settings.api_key:
|
| 134 |
+
raise HTTPException(status_code=401, detail="unauthorized")
|
| 135 |
+
|
| 136 |
+
client_ip = request.client.host if request.client else "unknown"
|
| 137 |
+
if not rate_limiter.allow(client_ip):
|
| 138 |
+
raise HTTPException(status_code=429, detail="rate limit exceeded")
|
| 139 |
+
|
| 140 |
+
logger.info("Incoming chat request: msg='%s', sid='%s'", payload.message[:30], payload.session_id)
|
| 141 |
+
|
| 142 |
+
# Fetch user_name if available for personalization
|
| 143 |
+
user_name = None
|
| 144 |
+
if payload.session_id:
|
| 145 |
+
sess_data = await _ss.get_session(payload.session_id)
|
| 146 |
+
user_name = sess_data.get("user_name")
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
result = await pipeline.chat(payload.message, payload.history, user_name=user_name)
|
| 150 |
+
|
| 151 |
+
# ── Fire-and-forget: persist to session store in background ──
|
| 152 |
+
if payload.session_id:
|
| 153 |
+
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, result["reply"]))
|
| 154 |
+
|
| 155 |
+
return ChatResponse(**result)
|
| 156 |
+
except Exception as e:
|
| 157 |
+
import httpx
|
| 158 |
+
import traceback
|
| 159 |
+
logger.error(f"Error in chat endpoint: {str(e)}\n{traceback.format_exc()}")
|
| 160 |
+
|
| 161 |
+
error_msg = "⚠️ Oops! Something went wrong."
|
| 162 |
+
if isinstance(e, httpx.HTTPStatusError):
|
| 163 |
+
if e.response.status_code == 429:
|
| 164 |
+
error_msg = "⚠️ Rate limit reached. Please slow down a bit!"
|
| 165 |
+
else:
|
| 166 |
+
error_msg = "⚠️ I encountered an error. Please try again in a few seconds."
|
| 167 |
+
|
| 168 |
+
return ChatResponse(reply=error_msg, retrieved_chunks=[])
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@fastapi_app.post("/api/session/update-name")
|
| 172 |
+
async def update_session_user_name(payload: SessionUpdateNameRequest):
|
| 173 |
+
"""Updates the user name and renames the session file to match the name."""
|
| 174 |
+
new_id = await _ss.rename_session(payload.session_id, payload.user_name)
|
| 175 |
+
return {"status": "ok", "new_session_id": new_id}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
@fastapi_app.get("/api/session/history/{session_id}")
|
| 179 |
+
async def get_session_history(session_id: str):
|
| 180 |
+
"""Returns the chat history for a session formatted for the frontend."""
|
| 181 |
+
data = await _ss.get_session(session_id)
|
| 182 |
+
if not data:
|
| 183 |
+
return {"history": []}
|
| 184 |
+
|
| 185 |
+
history = []
|
| 186 |
+
for msg in data.get("messages", []):
|
| 187 |
+
history.append({"role": "user", "content": msg["question"]})
|
| 188 |
+
history.append({"role": "assistant", "content": msg["answer"]})
|
| 189 |
+
return {"history": history, "user_name": data.get("user_name", "")}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ── Mount Gradio at /ui — MUST be last, wraps the FastAPI app ─────────────────
|
| 193 |
+
# All FastAPI routes (including /admin) are already registered above and are
|
| 194 |
+
# preserved inside the Gradio-wrapped Starlette app.
|
| 195 |
+
app = gr.mount_gradio_app(fastapi_app, demo, path="/ui")
|
app/services/__init__.py
ADDED
|
File without changes
|
app/services/chunker.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def _tokenize(text: str) -> List[str]:
|
| 5 |
+
return text.split()
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def chunk_documents(
|
| 9 |
+
documents: List[Dict[str, str]],
|
| 10 |
+
chunk_size_tokens: int = 420,
|
| 11 |
+
chunk_overlap_tokens: int = 80,
|
| 12 |
+
) -> List[Dict[str, str]]:
|
| 13 |
+
chunks: List[Dict[str, str]] = []
|
| 14 |
+
step = max(1, chunk_size_tokens - chunk_overlap_tokens)
|
| 15 |
+
|
| 16 |
+
for doc in documents:
|
| 17 |
+
tokens = _tokenize(doc["text"])
|
| 18 |
+
if not tokens:
|
| 19 |
+
continue
|
| 20 |
+
|
| 21 |
+
i = 0
|
| 22 |
+
chunk_id = 0
|
| 23 |
+
while i < len(tokens):
|
| 24 |
+
window = tokens[i : i + chunk_size_tokens]
|
| 25 |
+
if not window:
|
| 26 |
+
break
|
| 27 |
+
|
| 28 |
+
chunk_text = " ".join(window)
|
| 29 |
+
chunks.append(
|
| 30 |
+
{
|
| 31 |
+
"id": f"{doc['source']}::chunk_{chunk_id}",
|
| 32 |
+
"source": doc["source"],
|
| 33 |
+
"text": chunk_text,
|
| 34 |
+
}
|
| 35 |
+
)
|
| 36 |
+
chunk_id += 1
|
| 37 |
+
i += step
|
| 38 |
+
|
| 39 |
+
return chunks
|
app/services/document_loader.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
from pypdf import PdfReader
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _clean_text(text: str) -> str:
|
| 8 |
+
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
| 9 |
+
merged = " ".join(lines)
|
| 10 |
+
return " ".join(merged.split())
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_documents(docs_dir: Path) -> List[dict]:
|
| 14 |
+
docs: List[dict] = []
|
| 15 |
+
if not docs_dir.exists():
|
| 16 |
+
return docs
|
| 17 |
+
|
| 18 |
+
for file_path in sorted(docs_dir.rglob("*")):
|
| 19 |
+
if not file_path.is_file():
|
| 20 |
+
continue
|
| 21 |
+
|
| 22 |
+
suffix = file_path.suffix.lower()
|
| 23 |
+
if suffix == ".txt":
|
| 24 |
+
raw = file_path.read_text(encoding="utf-8", errors="ignore")
|
| 25 |
+
cleaned = _clean_text(raw)
|
| 26 |
+
if cleaned:
|
| 27 |
+
docs.append({"source": str(file_path), "text": cleaned})
|
| 28 |
+
elif suffix == ".pdf":
|
| 29 |
+
reader = PdfReader(str(file_path))
|
| 30 |
+
pages = []
|
| 31 |
+
for page in reader.pages:
|
| 32 |
+
pages.append(page.extract_text() or "")
|
| 33 |
+
cleaned = _clean_text("\n".join(pages))
|
| 34 |
+
if cleaned:
|
| 35 |
+
docs.append({"source": str(file_path), "text": cleaned})
|
| 36 |
+
|
| 37 |
+
return docs
|
app/services/embeddings.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EmbeddingService:
|
| 9 |
+
def __init__(self, model_name: str) -> None:
|
| 10 |
+
self.model = SentenceTransformer(model_name)
|
| 11 |
+
|
| 12 |
+
def encode(self, texts: List[str]) -> np.ndarray:
|
| 13 |
+
vectors = self.model.encode(
|
| 14 |
+
texts,
|
| 15 |
+
normalize_embeddings=True,
|
| 16 |
+
show_progress_bar=False,
|
| 17 |
+
convert_to_numpy=True,
|
| 18 |
+
)
|
| 19 |
+
return np.asarray(vectors, dtype=np.float32)
|
| 20 |
+
|
| 21 |
+
@lru_cache(maxsize=2048)
|
| 22 |
+
def encode_query_cached(self, text: str) -> bytes:
|
| 23 |
+
vec = self.model.encode(
|
| 24 |
+
[text],
|
| 25 |
+
normalize_embeddings=True,
|
| 26 |
+
show_progress_bar=False,
|
| 27 |
+
convert_to_numpy=True,
|
| 28 |
+
)
|
| 29 |
+
arr = np.asarray(vec, dtype=np.float32)
|
| 30 |
+
return arr.tobytes()
|
| 31 |
+
|
| 32 |
+
def encode_query(self, text: str) -> np.ndarray:
|
| 33 |
+
buf = self.encode_query_cached(text)
|
| 34 |
+
return np.frombuffer(buf, dtype=np.float32).reshape(1, -1)
|
app/services/llm.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
import httpx
|
| 3 |
+
|
| 4 |
+
# MASTER SYSTEM PROMPT - Consolidating all user feedback for "Perfection"
|
| 5 |
+
SYSTEM_PROMPT = """You are Martechsol Assistant, a peak-intelligence expert.
|
| 6 |
+
|
| 7 |
+
AMBIGUITY & CLARIFICATION PROTOCOL:
|
| 8 |
+
1. DETECT MULTIPLE TOPICS: If the "Expert Data" contains different types of information for the same keyword (e.g., "office timing" vs "salary timing"), and the user's question is general (e.g., "what is the timing?"), DO NOT answer yet.
|
| 9 |
+
2. ASK FIRST: Instead of answering, briefly state that there are multiple types of [Topic] and ask: "Are you asking about [Topic A]? Or perhaps [Topic B]?"
|
| 10 |
+
3. SPECIFIC ANSWERS: If the user confirms a topic (e.g., "Yes, office timings") or asks a specific question initially, provide a single, sophisticated summary (max 25 words).
|
| 11 |
+
4. UNKNOWN INFO: If the requested information is not in the "Expert Data", say: "I don't have that specific information."
|
| 12 |
+
|
| 13 |
+
SYNTHESIS PROTOCOL:
|
| 14 |
+
1. PEAK BREVITY: Max 25 words per response.
|
| 15 |
+
2. NO LISTS: Never use bullet points.
|
| 16 |
+
3. NO SOURCES: Never mention "Expert Data", "documents", or "handbook".
|
| 17 |
+
4. IDENTITY: You are a professional Martechsol assistant."""
|
| 18 |
+
|
| 19 |
+
def _build_context(chunks: List[Dict[str, str]]) -> str:
|
| 20 |
+
"""Combines chunks into a raw string without technical headers."""
|
| 21 |
+
if not chunks:
|
| 22 |
+
return ""
|
| 23 |
+
return "\n\n".join([c['text'] for c in chunks])
|
| 24 |
+
|
| 25 |
+
class LLMService:
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
provider: str,
|
| 29 |
+
groq_api_key: str,
|
| 30 |
+
groq_model: str,
|
| 31 |
+
hf_api_key: str,
|
| 32 |
+
hf_model: str,
|
| 33 |
+
timeout_s: float = 20.0,
|
| 34 |
+
) -> None:
|
| 35 |
+
self.provider = provider.lower().strip()
|
| 36 |
+
self.groq_api_key = groq_api_key
|
| 37 |
+
self.groq_model = groq_model
|
| 38 |
+
self.hf_api_key = hf_api_key
|
| 39 |
+
self.hf_model = hf_model
|
| 40 |
+
self.timeout_s = timeout_s
|
| 41 |
+
|
| 42 |
+
async def refine_query(self, query: str, history: List[Dict[str, str]]) -> str:
|
| 43 |
+
"""Expands a short query into a descriptive search term for better retrieval."""
|
| 44 |
+
if len(query.split()) > 5:
|
| 45 |
+
return query # Long enough
|
| 46 |
+
|
| 47 |
+
prompt = f"""Given the user query and history, generate a single, broad search query that will find ALL relevant policy sections in a company handbook.
|
| 48 |
+
|
| 49 |
+
If the query is general (like "timing"), ensure the search query includes keywords for all possible interpretations (e.g., office, salary, shifts).
|
| 50 |
+
|
| 51 |
+
Examples:
|
| 52 |
+
- "timing" -> "Martechsol timings: office hours, shift schedules, workweek, salary processing dates, and deadlines."
|
| 53 |
+
- "leave" -> "Martechsol leave policies: sick leave, annual leave, casual leave, and leave approval process."
|
| 54 |
+
|
| 55 |
+
User Query: {query}
|
| 56 |
+
Broad Search Query:"""
|
| 57 |
+
|
| 58 |
+
# Using a very low temperature for precision
|
| 59 |
+
messages = [{"role": "system", "content": "You are a search expert. Output only the expanded query."},
|
| 60 |
+
{"role": "user", "content": prompt}]
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
expanded = await self._call_groq(prompt, [], "You are a search expert. Output only the expanded query.")
|
| 64 |
+
return expanded.strip().strip('"')
|
| 65 |
+
except:
|
| 66 |
+
return query # Fallback to original
|
| 67 |
+
|
| 68 |
+
async def answer(
|
| 69 |
+
self,
|
| 70 |
+
question: str,
|
| 71 |
+
chunks: List[Dict[str, str]],
|
| 72 |
+
history: List[Dict[str, str]],
|
| 73 |
+
user_name: str = None
|
| 74 |
+
) -> str:
|
| 75 |
+
# 6 messages (3 turns) provides perfect balance of memory vs. token cost
|
| 76 |
+
pruned_history = history[-6:]
|
| 77 |
+
context = _build_context(chunks)
|
| 78 |
+
|
| 79 |
+
system_msg = SYSTEM_PROMPT
|
| 80 |
+
if user_name and user_name != "default name":
|
| 81 |
+
system_msg += f"\n\nYou are currently talking to {user_name}."
|
| 82 |
+
|
| 83 |
+
# Build the prompt without technical markers
|
| 84 |
+
if not context:
|
| 85 |
+
user_prompt = question
|
| 86 |
+
else:
|
| 87 |
+
user_prompt = f"Expert Data: {context}\n\nQuestion: {question}"
|
| 88 |
+
|
| 89 |
+
return await self._call_groq(user_prompt, pruned_history, system_msg)
|
| 90 |
+
|
| 91 |
+
async def _call_groq(self, user_prompt: str, history: List[Dict[str, str]], system_msg: str) -> str:
|
| 92 |
+
if not self.groq_api_key:
|
| 93 |
+
return "Expert access required."
|
| 94 |
+
|
| 95 |
+
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 96 |
+
headers = {
|
| 97 |
+
"Authorization": f"Bearer {self.groq_api_key}",
|
| 98 |
+
"Content-Type": "application/json"
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
messages = [{"role": "system", "content": system_msg}]
|
| 102 |
+
for msg in history:
|
| 103 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 104 |
+
messages.append({"role": "user", "content": user_prompt})
|
| 105 |
+
|
| 106 |
+
payload = {
|
| 107 |
+
"model": self.groq_model,
|
| 108 |
+
"temperature": 0.1, # Strict precision
|
| 109 |
+
"max_tokens": 150, # Large buffer to ensure the summary is never cut off
|
| 110 |
+
"messages": messages,
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
async with httpx.AsyncClient(timeout=self.timeout_s) as client:
|
| 114 |
+
resp = await client.post(url, headers=headers, json=payload)
|
| 115 |
+
resp.raise_for_status()
|
| 116 |
+
data = resp.json()
|
| 117 |
+
return data["choices"][0]["message"]["content"].strip()
|
app/services/rag_pipeline.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
|
| 4 |
+
from app.services.llm import LLMService
|
| 5 |
+
from app.services.vector_store import FaissVectorStore
|
| 6 |
+
from app.services.reranker import RerankerService
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
RELEVANCE_THRESHOLD = 0.45 # FAISS threshold
|
| 11 |
+
RERANK_THRESHOLD = 0.1 # Cross-encoder threshold (usually lower for cross-encoders)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class RAGPipeline:
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
vector_store: FaissVectorStore,
|
| 18 |
+
llm_service: LLMService,
|
| 19 |
+
reranker: RerankerService,
|
| 20 |
+
top_k: int,
|
| 21 |
+
max_context_chunks: int
|
| 22 |
+
) -> None:
|
| 23 |
+
self.vector_store = vector_store
|
| 24 |
+
self.llm_service = llm_service
|
| 25 |
+
self.reranker = reranker
|
| 26 |
+
self.top_k = top_k
|
| 27 |
+
self.max_context_chunks = max_context_chunks
|
| 28 |
+
|
| 29 |
+
async def chat(self, message: str, history: List[Dict[str, str]], user_name: str = None) -> Dict[str, object]:
|
| 30 |
+
# Step 0: Query Refinement for "Deep Intelligence"
|
| 31 |
+
search_query = await self.llm_service.refine_query(message, history)
|
| 32 |
+
logger.info("Original query: '%s' -> Refined search query: '%s'", message, search_query)
|
| 33 |
+
|
| 34 |
+
# Step 1: Broad search in vector store using refined query
|
| 35 |
+
all_chunks = self.vector_store.search(search_query, top_k=self.top_k)
|
| 36 |
+
|
| 37 |
+
# Filter chunks by initial relevance to save processing
|
| 38 |
+
initial_chunks = [c for c in all_chunks if c["score"] >= RELEVANCE_THRESHOLD]
|
| 39 |
+
|
| 40 |
+
if not initial_chunks:
|
| 41 |
+
logger.info("No chunks passed initial relevance threshold for: %s", message)
|
| 42 |
+
reply = await self.llm_service.answer(question=message, chunks=[], history=history, user_name=user_name)
|
| 43 |
+
return {"reply": reply, "retrieved_chunks": []}
|
| 44 |
+
|
| 45 |
+
# Step 2: Deep reranking for "Peak Intelligence"
|
| 46 |
+
reranked_chunks = self.reranker.rerank(message, initial_chunks, top_n=self.max_context_chunks)
|
| 47 |
+
|
| 48 |
+
# Filter by rerank score
|
| 49 |
+
final_chunks = [c for c in reranked_chunks if c.get("rerank_score", 0) > RERANK_THRESHOLD]
|
| 50 |
+
|
| 51 |
+
logger.info("Retrieved %s -> Filtered %s -> Reranked %s for query: %s",
|
| 52 |
+
len(all_chunks), len(initial_chunks), len(final_chunks), message)
|
| 53 |
+
|
| 54 |
+
for c in final_chunks:
|
| 55 |
+
logger.info("Chunk rerank_score=%.4f source=%s", c["rerank_score"], c["source"])
|
| 56 |
+
|
| 57 |
+
reply = await self.llm_service.answer(question=message, chunks=final_chunks, history=history, user_name=user_name)
|
| 58 |
+
return {"reply": reply, "retrieved_chunks": final_chunks}
|
app/services/rate_limiter.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from collections import defaultdict, deque
|
| 3 |
+
from typing import Deque, Dict
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class InMemoryRateLimiter:
|
| 7 |
+
def __init__(self, max_requests: int, window_seconds: int) -> None:
|
| 8 |
+
self.max_requests = max_requests
|
| 9 |
+
self.window_seconds = window_seconds
|
| 10 |
+
self._hits: Dict[str, Deque[float]] = defaultdict(deque)
|
| 11 |
+
|
| 12 |
+
def allow(self, key: str) -> bool:
|
| 13 |
+
now = time.time()
|
| 14 |
+
window_start = now - self.window_seconds
|
| 15 |
+
q = self._hits[key]
|
| 16 |
+
|
| 17 |
+
while q and q[0] < window_start:
|
| 18 |
+
q.popleft()
|
| 19 |
+
|
| 20 |
+
if len(q) >= self.max_requests:
|
| 21 |
+
return False
|
| 22 |
+
|
| 23 |
+
q.append(now)
|
| 24 |
+
return True
|
app/services/reranker.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict
|
| 2 |
+
import numpy as np
|
| 3 |
+
from sentence_transformers import CrossEncoder
|
| 4 |
+
|
| 5 |
+
class RerankerService:
|
| 6 |
+
def __init__(self, model_name: str) -> None:
|
| 7 |
+
self.model = CrossEncoder(model_name)
|
| 8 |
+
|
| 9 |
+
def rerank(self, query: str, chunks: List[Dict[str, str]], top_n: int = 4) -> List[Dict[str, str]]:
|
| 10 |
+
if not chunks:
|
| 11 |
+
return []
|
| 12 |
+
|
| 13 |
+
# Prepare pairs for cross-encoder
|
| 14 |
+
pairs = [[query, chunk["text"]] for chunk in chunks]
|
| 15 |
+
|
| 16 |
+
# Get scores
|
| 17 |
+
scores = self.model.predict(pairs)
|
| 18 |
+
|
| 19 |
+
# Add scores to chunks and sort
|
| 20 |
+
for i, chunk in enumerate(chunks):
|
| 21 |
+
chunk["rerank_score"] = float(scores[i])
|
| 22 |
+
|
| 23 |
+
# Sort by rerank_score descending
|
| 24 |
+
sorted_chunks = sorted(chunks, key=lambda x: x["rerank_score"], reverse=True)
|
| 25 |
+
|
| 26 |
+
return sorted_chunks[:top_n]
|
app/services/session_store.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
session_store.py
|
| 3 |
+
────────────────
|
| 4 |
+
Async, fire-and-forget JSON-based chat session storage.
|
| 5 |
+
Each browser session gets its own JSON file in data/sessions/.
|
| 6 |
+
|
| 7 |
+
Rules:
|
| 8 |
+
• NEVER raises exceptions that propagate upward — any failure is silently logged.
|
| 9 |
+
• ALL disk I/O is done with asyncio to avoid blocking the event loop.
|
| 10 |
+
• Thread-safe: uses an asyncio.Lock per session_id to avoid race conditions.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import asyncio
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
from datetime import datetime, timezone
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Dict
|
| 20 |
+
|
| 21 |
+
from app.core.config import get_settings
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
settings = get_settings()
|
| 24 |
+
|
| 25 |
+
# ── Directory where session JSON files are stored ───────────────────────────
|
| 26 |
+
def _get_sessions_dir() -> Path:
|
| 27 |
+
"""Returns the current sessions directory from settings."""
|
| 28 |
+
return settings.sessions_dir
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ── Per-session locks to prevent concurrent write corruption ─────────────────
|
| 32 |
+
_session_locks: Dict[str, asyncio.Lock] = {}
|
| 33 |
+
_locks_meta_lock = asyncio.Lock() # protects the _session_locks dict itself
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _get_session_path(session_id: str) -> Path:
|
| 37 |
+
"""Returns the absolute path for a session's JSON file."""
|
| 38 |
+
return _get_sessions_dir() / f"{session_id}.json"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _now_iso() -> str:
|
| 42 |
+
"""Returns the current UTC time as an ISO-8601 string."""
|
| 43 |
+
return datetime.now(timezone.utc).isoformat()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
async def _get_lock(session_id: str) -> asyncio.Lock:
|
| 47 |
+
"""Gets or creates a per-session asyncio.Lock in a thread-safe manner."""
|
| 48 |
+
async with _locks_meta_lock:
|
| 49 |
+
if session_id not in _session_locks:
|
| 50 |
+
_session_locks[session_id] = asyncio.Lock()
|
| 51 |
+
return _session_locks[session_id]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _ensure_sessions_dir() -> None:
|
| 55 |
+
"""Creates the sessions directory if it does not exist. Sync, called once."""
|
| 56 |
+
try:
|
| 57 |
+
_get_sessions_dir().mkdir(parents=True, exist_ok=True)
|
| 58 |
+
except OSError as exc:
|
| 59 |
+
logger.error("session_store: cannot create sessions dir: %s", exc)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# Call once at import time so the directory is always ready.
|
| 63 |
+
_ensure_sessions_dir()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
async def ensure_session(session_id: str) -> None:
|
| 67 |
+
"""
|
| 68 |
+
Creates the session JSON file if it doesn't exist yet.
|
| 69 |
+
Safe to call on every page load — does nothing if file exists.
|
| 70 |
+
"""
|
| 71 |
+
path = _get_session_path(session_id)
|
| 72 |
+
if path.exists():
|
| 73 |
+
return
|
| 74 |
+
lock = await _get_lock(session_id)
|
| 75 |
+
async with lock:
|
| 76 |
+
# Double-check after acquiring lock
|
| 77 |
+
if path.exists():
|
| 78 |
+
return
|
| 79 |
+
skeleton = {
|
| 80 |
+
"session_id": session_id,
|
| 81 |
+
"user_name": "default name",
|
| 82 |
+
"created_at": _now_iso(),
|
| 83 |
+
"last_active": _now_iso(),
|
| 84 |
+
"message_count": 0,
|
| 85 |
+
"messages": [],
|
| 86 |
+
}
|
| 87 |
+
try:
|
| 88 |
+
await asyncio.to_thread(_write_json, path, skeleton)
|
| 89 |
+
logger.info("session_store: created session %s", session_id)
|
| 90 |
+
except Exception as exc:
|
| 91 |
+
logger.error("session_store: failed to create session %s: %s", session_id, exc)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
async def save_message(session_id: str, question: str, answer: str) -> None:
|
| 95 |
+
"""
|
| 96 |
+
Appends a Question+Answer pair with timestamps to the session's JSON file.
|
| 97 |
+
Fire-and-forget safe — caller should wrap in asyncio.create_task().
|
| 98 |
+
"""
|
| 99 |
+
path = _get_session_path(session_id)
|
| 100 |
+
logger.info("session_store: save_message called for session_id=%s, path=%s", session_id, path)
|
| 101 |
+
lock = await _get_lock(session_id)
|
| 102 |
+
async with lock:
|
| 103 |
+
try:
|
| 104 |
+
# Load current data
|
| 105 |
+
if path.exists():
|
| 106 |
+
data = await asyncio.to_thread(_read_json, path)
|
| 107 |
+
else:
|
| 108 |
+
# Session file missing — recreate it gracefully
|
| 109 |
+
data = {
|
| 110 |
+
"session_id": session_id,
|
| 111 |
+
"user_name": "default name",
|
| 112 |
+
"created_at": _now_iso(),
|
| 113 |
+
"last_active": _now_iso(),
|
| 114 |
+
"message_count": 0,
|
| 115 |
+
"messages": [],
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
timestamp = _now_iso()
|
| 119 |
+
data["messages"].append({
|
| 120 |
+
"id": data["message_count"] + 1,
|
| 121 |
+
"timestamp": timestamp,
|
| 122 |
+
"question": question,
|
| 123 |
+
"answer": answer,
|
| 124 |
+
})
|
| 125 |
+
data["message_count"] += 1
|
| 126 |
+
data["last_active"] = timestamp
|
| 127 |
+
|
| 128 |
+
await asyncio.to_thread(_write_json, path, data)
|
| 129 |
+
logger.info("session_store: successfully saved message to %s", path)
|
| 130 |
+
except Exception as exc:
|
| 131 |
+
logger.error(
|
| 132 |
+
"session_store: failed to save message for session %s: %s",
|
| 133 |
+
session_id,
|
| 134 |
+
exc,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
async def get_all_sessions() -> list:
|
| 139 |
+
"""
|
| 140 |
+
Returns a list of all session summaries (metadata only, no full messages).
|
| 141 |
+
Used by the Admin Panel sidebar.
|
| 142 |
+
"""
|
| 143 |
+
try:
|
| 144 |
+
files = await asyncio.to_thread(_list_json_files)
|
| 145 |
+
sessions = []
|
| 146 |
+
for fpath in files:
|
| 147 |
+
try:
|
| 148 |
+
data = await asyncio.to_thread(_read_json, fpath)
|
| 149 |
+
sessions.append({
|
| 150 |
+
"session_id": data.get("session_id", fpath.stem),
|
| 151 |
+
"user_name": data.get("user_name", ""),
|
| 152 |
+
"created_at": data.get("created_at", ""),
|
| 153 |
+
"last_active": data.get("last_active", ""),
|
| 154 |
+
"message_count": data.get("message_count", 0),
|
| 155 |
+
})
|
| 156 |
+
except Exception as exc:
|
| 157 |
+
logger.warning("session_store: could not read %s: %s", fpath, exc)
|
| 158 |
+
# Sort newest first
|
| 159 |
+
sessions.sort(key=lambda s: s.get("last_active", ""), reverse=True)
|
| 160 |
+
return sessions
|
| 161 |
+
except Exception as exc:
|
| 162 |
+
logger.error("session_store: get_all_sessions failed: %s", exc)
|
| 163 |
+
return []
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
async def get_session(session_id: str) -> dict:
|
| 167 |
+
"""
|
| 168 |
+
Returns the full session data (including all messages) for the Admin Panel.
|
| 169 |
+
Returns an empty dict if the session does not exist.
|
| 170 |
+
"""
|
| 171 |
+
path = _get_session_path(session_id)
|
| 172 |
+
try:
|
| 173 |
+
if not path.exists():
|
| 174 |
+
return {}
|
| 175 |
+
return await asyncio.to_thread(_read_json, path)
|
| 176 |
+
except Exception as exc:
|
| 177 |
+
logger.error("session_store: get_session(%s) failed: %s", session_id, exc)
|
| 178 |
+
return {}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
async def delete_session(session_id: str) -> bool:
|
| 182 |
+
"""
|
| 183 |
+
Deletes a session file. Returns True on success, False on failure.
|
| 184 |
+
Used by Admin Panel if needed in the future.
|
| 185 |
+
"""
|
| 186 |
+
path = _get_session_path(session_id)
|
| 187 |
+
lock = await _get_lock(session_id)
|
| 188 |
+
async with lock:
|
| 189 |
+
try:
|
| 190 |
+
if path.exists():
|
| 191 |
+
await asyncio.to_thread(os.remove, path)
|
| 192 |
+
return True
|
| 193 |
+
except Exception as exc:
|
| 194 |
+
logger.error("session_store: delete_session(%s) failed: %s", session_id, exc)
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
async def update_session_name(session_id: str, user_name: str) -> None:
|
| 199 |
+
"""Updates the user_name field in the session JSON file."""
|
| 200 |
+
path = _get_session_path(session_id)
|
| 201 |
+
lock = await _get_lock(session_id)
|
| 202 |
+
async with lock:
|
| 203 |
+
try:
|
| 204 |
+
if path.exists():
|
| 205 |
+
data = await asyncio.to_thread(_read_json, path)
|
| 206 |
+
data["user_name"] = user_name
|
| 207 |
+
await asyncio.to_thread(_write_json, path, data)
|
| 208 |
+
logger.info("session_store: updated name for session %s to %s", session_id, user_name)
|
| 209 |
+
else:
|
| 210 |
+
# If session doesn't exist yet, we can't update it, but we could create it?
|
| 211 |
+
# Usually ensure_session is called first.
|
| 212 |
+
logger.warning("session_store: attempt to update name for non-existent session %s", session_id)
|
| 213 |
+
except Exception as exc:
|
| 214 |
+
logger.error("session_store: failed to update name for session %s: %s", session_id, exc)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
async def rename_session(old_id: str, new_name: str) -> str:
|
| 218 |
+
"""
|
| 219 |
+
Renames a session file from old_id to a name based on new_name.
|
| 220 |
+
Returns the new session_id used.
|
| 221 |
+
"""
|
| 222 |
+
import re
|
| 223 |
+
# 1. Sanitize the name for use as a filename
|
| 224 |
+
clean_name = re.sub(r'[^\w\s-]', '', new_name).strip().replace(' ', '_')
|
| 225 |
+
if not clean_name:
|
| 226 |
+
clean_name = "User"
|
| 227 |
+
|
| 228 |
+
new_id = clean_name
|
| 229 |
+
old_path = _get_session_path(old_id)
|
| 230 |
+
new_path = _get_session_path(new_id)
|
| 231 |
+
|
| 232 |
+
# Ensure uniqueness: if John_Doe exists, try John_Doe_1, etc.
|
| 233 |
+
counter = 1
|
| 234 |
+
while new_path.exists() and new_id != old_id:
|
| 235 |
+
new_id = f"{clean_name}_{counter}"
|
| 236 |
+
new_path = _get_session_path(new_id)
|
| 237 |
+
counter += 1
|
| 238 |
+
|
| 239 |
+
if new_id == old_id:
|
| 240 |
+
return old_id
|
| 241 |
+
|
| 242 |
+
# Lock both IDs to prevent race conditions during move
|
| 243 |
+
lock_old = await _get_lock(old_id)
|
| 244 |
+
lock_new = await _get_lock(new_id)
|
| 245 |
+
|
| 246 |
+
async with lock_old:
|
| 247 |
+
async with lock_new:
|
| 248 |
+
try:
|
| 249 |
+
if old_path.exists():
|
| 250 |
+
data = await asyncio.to_thread(_read_json, old_path)
|
| 251 |
+
data["session_id"] = new_id
|
| 252 |
+
data["user_name"] = new_name
|
| 253 |
+
|
| 254 |
+
# Write to new path and delete old
|
| 255 |
+
await asyncio.to_thread(_write_json, new_path, data)
|
| 256 |
+
await asyncio.to_thread(os.remove, old_path)
|
| 257 |
+
|
| 258 |
+
logger.info("session_store: renamed session %s -> %s", old_id, new_id)
|
| 259 |
+
return new_id
|
| 260 |
+
else:
|
| 261 |
+
logger.warning("session_store: cannot rename non-existent session %s", old_id)
|
| 262 |
+
return old_id
|
| 263 |
+
except Exception as exc:
|
| 264 |
+
logger.error("session_store: failed to rename session %s: %s", old_id, exc)
|
| 265 |
+
return old_id
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# ── Sync helper functions (run via asyncio.to_thread) ────────────────────────
|
| 269 |
+
|
| 270 |
+
def _write_json(path: Path, data: dict) -> None:
|
| 271 |
+
"""Atomically writes JSON to a file using a temp file + rename pattern."""
|
| 272 |
+
tmp_path = path.with_suffix(".tmp")
|
| 273 |
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
| 274 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 275 |
+
os.replace(tmp_path, path) # Atomic on POSIX, best-effort on Windows
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _read_json(path: Path) -> dict:
|
| 279 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 280 |
+
return json.load(f)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def _list_json_files() -> list:
|
| 284 |
+
return list(_get_sessions_dir().glob("*.json"))
|
app/services/vector_store.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Dict, List, Tuple
|
| 5 |
+
|
| 6 |
+
import faiss
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from app.services.chunker import chunk_documents
|
| 10 |
+
from app.services.document_loader import load_documents
|
| 11 |
+
from app.services.embeddings import EmbeddingService
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class FaissVectorStore:
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
embedding_service: EmbeddingService,
|
| 18 |
+
docs_dir: Path,
|
| 19 |
+
index_dir: Path,
|
| 20 |
+
chunk_size_tokens: int,
|
| 21 |
+
chunk_overlap_tokens: int,
|
| 22 |
+
) -> None:
|
| 23 |
+
self.embedding_service = embedding_service
|
| 24 |
+
self.docs_dir = docs_dir
|
| 25 |
+
self.index_dir = index_dir
|
| 26 |
+
self.chunk_size_tokens = chunk_size_tokens
|
| 27 |
+
self.chunk_overlap_tokens = chunk_overlap_tokens
|
| 28 |
+
|
| 29 |
+
self.faiss_index_path = index_dir / "faiss.index"
|
| 30 |
+
self.metadata_path = index_dir / "metadata.json"
|
| 31 |
+
self.state_path = index_dir / "state.json"
|
| 32 |
+
|
| 33 |
+
self.index = None
|
| 34 |
+
self.metadata: List[Dict[str, str]] = []
|
| 35 |
+
self.docs_loaded = False
|
| 36 |
+
self.last_retrieved: List[Dict[str, str]] = []
|
| 37 |
+
|
| 38 |
+
def _compute_docs_fingerprint(self) -> str:
|
| 39 |
+
hasher = hashlib.sha256()
|
| 40 |
+
# Include chunk settings in fingerprint so changing them triggers re-index
|
| 41 |
+
hasher.update(str(self.chunk_size_tokens).encode("utf-8"))
|
| 42 |
+
hasher.update(str(self.chunk_overlap_tokens).encode("utf-8"))
|
| 43 |
+
|
| 44 |
+
if not self.docs_dir.exists():
|
| 45 |
+
return "no_docs"
|
| 46 |
+
for path in sorted(self.docs_dir.rglob("*")):
|
| 47 |
+
if path.is_file() and path.suffix.lower() in {".txt", ".pdf"}:
|
| 48 |
+
stat = path.stat()
|
| 49 |
+
hasher.update(str(path).encode("utf-8"))
|
| 50 |
+
hasher.update(str(stat.st_mtime_ns).encode("utf-8"))
|
| 51 |
+
hasher.update(str(stat.st_size).encode("utf-8"))
|
| 52 |
+
return hasher.hexdigest()
|
| 53 |
+
|
| 54 |
+
def _can_use_cached_index(self, fingerprint: str) -> bool:
|
| 55 |
+
if not (self.faiss_index_path.exists() and self.metadata_path.exists() and self.state_path.exists()):
|
| 56 |
+
return False
|
| 57 |
+
try:
|
| 58 |
+
state = json.loads(self.state_path.read_text(encoding="utf-8"))
|
| 59 |
+
return state.get("docs_fingerprint") == fingerprint
|
| 60 |
+
except Exception:
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
def build_or_load(self) -> None:
|
| 64 |
+
self.index_dir.mkdir(parents=True, exist_ok=True)
|
| 65 |
+
fingerprint = self._compute_docs_fingerprint()
|
| 66 |
+
|
| 67 |
+
if self._can_use_cached_index(fingerprint):
|
| 68 |
+
self.index = faiss.read_index(str(self.faiss_index_path))
|
| 69 |
+
self.metadata = json.loads(self.metadata_path.read_text(encoding="utf-8"))
|
| 70 |
+
self.docs_loaded = len(self.metadata) > 0
|
| 71 |
+
return
|
| 72 |
+
|
| 73 |
+
docs = load_documents(self.docs_dir)
|
| 74 |
+
chunks = chunk_documents(docs, self.chunk_size_tokens, self.chunk_overlap_tokens)
|
| 75 |
+
if not chunks:
|
| 76 |
+
self.index = None
|
| 77 |
+
self.metadata = []
|
| 78 |
+
self.docs_loaded = False
|
| 79 |
+
return
|
| 80 |
+
|
| 81 |
+
vectors = self.embedding_service.encode([c["text"] for c in chunks])
|
| 82 |
+
dim = vectors.shape[1]
|
| 83 |
+
index = faiss.IndexFlatIP(dim)
|
| 84 |
+
index.add(vectors)
|
| 85 |
+
|
| 86 |
+
self.index = index
|
| 87 |
+
self.metadata = chunks
|
| 88 |
+
self.docs_loaded = True
|
| 89 |
+
|
| 90 |
+
faiss.write_index(index, str(self.faiss_index_path))
|
| 91 |
+
self.metadata_path.write_text(json.dumps(chunks, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 92 |
+
self.state_path.write_text(
|
| 93 |
+
json.dumps({"docs_fingerprint": fingerprint, "chunk_count": len(chunks)}, indent=2),
|
| 94 |
+
encoding="utf-8",
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
def search(self, query: str, top_k: int = 4) -> List[Dict[str, str]]:
|
| 98 |
+
if self.index is None or not self.metadata:
|
| 99 |
+
self.last_retrieved = []
|
| 100 |
+
return []
|
| 101 |
+
|
| 102 |
+
query_vec = self.embedding_service.encode_query(query)
|
| 103 |
+
scores, indices = self.index.search(np.asarray(query_vec, dtype=np.float32), top_k)
|
| 104 |
+
|
| 105 |
+
results: List[Dict[str, str]] = []
|
| 106 |
+
for score, idx in zip(scores[0], indices[0]):
|
| 107 |
+
if idx < 0 or idx >= len(self.metadata):
|
| 108 |
+
continue
|
| 109 |
+
chunk = self.metadata[idx]
|
| 110 |
+
results.append(
|
| 111 |
+
{
|
| 112 |
+
"id": chunk["id"],
|
| 113 |
+
"source": chunk["source"],
|
| 114 |
+
"text": chunk["text"],
|
| 115 |
+
"score": float(score),
|
| 116 |
+
}
|
| 117 |
+
)
|
| 118 |
+
self.last_retrieved = results
|
| 119 |
+
return results
|
| 120 |
+
|
| 121 |
+
def health(self) -> Dict[str, object]:
|
| 122 |
+
return {
|
| 123 |
+
"docs_loaded": self.docs_loaded,
|
| 124 |
+
"index_ready": self.index is not None,
|
| 125 |
+
"chunk_count": len(self.metadata),
|
| 126 |
+
"index_path": str(self.faiss_index_path),
|
| 127 |
+
}
|
app/ui_gradio.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import random
|
| 5 |
+
import string
|
| 6 |
+
import traceback
|
| 7 |
+
import uuid
|
| 8 |
+
from typing import Dict, List, Tuple
|
| 9 |
+
|
| 10 |
+
from app.services import session_store as _ss
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import httpx
|
| 14 |
+
|
| 15 |
+
from app.core.config import get_settings
|
| 16 |
+
from app.services.embeddings import EmbeddingService
|
| 17 |
+
from app.services.llm import LLMService
|
| 18 |
+
from app.services.rag_pipeline import RAGPipeline
|
| 19 |
+
from app.services.vector_store import FaissVectorStore
|
| 20 |
+
from app.services.reranker import RerankerService
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
settings = get_settings()
|
| 24 |
+
API_URL = os.getenv("RAG_API_URL", "").strip()
|
| 25 |
+
|
| 26 |
+
# Initialize services
|
| 27 |
+
embedding_service = EmbeddingService(settings.embedding_model)
|
| 28 |
+
vector_store = FaissVectorStore(
|
| 29 |
+
embedding_service=embedding_service,
|
| 30 |
+
docs_dir=settings.docs_dir,
|
| 31 |
+
index_dir=settings.index_dir,
|
| 32 |
+
chunk_size_tokens=settings.chunk_size_tokens,
|
| 33 |
+
chunk_overlap_tokens=settings.chunk_overlap_tokens,
|
| 34 |
+
)
|
| 35 |
+
llm_service = LLMService(
|
| 36 |
+
provider=settings.llm_provider,
|
| 37 |
+
groq_api_key=settings.groq_api_key,
|
| 38 |
+
groq_model=settings.groq_model,
|
| 39 |
+
hf_api_key=settings.hf_api_key,
|
| 40 |
+
hf_model=settings.hf_model,
|
| 41 |
+
timeout_s=settings.request_timeout_s,
|
| 42 |
+
)
|
| 43 |
+
reranker_service = RerankerService(settings.reranker_model)
|
| 44 |
+
pipeline = RAGPipeline(
|
| 45 |
+
vector_store=vector_store,
|
| 46 |
+
llm_service=llm_service,
|
| 47 |
+
reranker=reranker_service,
|
| 48 |
+
top_k=settings.top_k,
|
| 49 |
+
max_context_chunks=settings.max_context_chunks
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Ensure index is ready
|
| 53 |
+
vector_store.build_or_load()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ── Keyboard layout for realistic typos ──────────────────────────────
|
| 57 |
+
NEARBY_KEYS = {
|
| 58 |
+
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 59 |
+
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 60 |
+
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 61 |
+
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 62 |
+
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 63 |
+
'z': 'xas',
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
TYPO_CHANCE = 0.07 # 7% chance per word
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _nearby_char(ch: str) -> str:
|
| 70 |
+
"""Return a plausible neighbouring key for the given character."""
|
| 71 |
+
lower = ch.lower()
|
| 72 |
+
if lower in NEARBY_KEYS:
|
| 73 |
+
replacement = random.choice(NEARBY_KEYS[lower])
|
| 74 |
+
return replacement.upper() if ch.isupper() else replacement
|
| 75 |
+
return random.choice(string.ascii_lowercase)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _to_history(messages: List[Dict[str, str]]) -> list:
|
| 79 |
+
"""Returns the history as a list of dicts (already in this format for Gradio 5)."""
|
| 80 |
+
return messages
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
async def _chat_via_api(message: str, history: List[Dict[str, str]]) -> str:
|
| 84 |
+
payload = {"message": message, "history": history}
|
| 85 |
+
headers = {"Content-Type": "application/json"}
|
| 86 |
+
if settings.api_key:
|
| 87 |
+
headers["x-api-key"] = settings.api_key
|
| 88 |
+
|
| 89 |
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 90 |
+
resp = await client.post(API_URL, json=payload, headers=headers)
|
| 91 |
+
if resp.status_code == 429:
|
| 92 |
+
return "⚠️ I'm receiving too many requests right now. Please wait a moment before sending another message."
|
| 93 |
+
resp.raise_for_status()
|
| 94 |
+
return resp.json()["reply"]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
async def _get_reply(message: str, chat_history: List[Dict[str, str]]) -> str:
|
| 98 |
+
"""Fetch the full reply from the LLM (API or direct pipeline)."""
|
| 99 |
+
if API_URL:
|
| 100 |
+
return await _chat_via_api(message, chat_history)
|
| 101 |
+
result = await pipeline.chat(message=message, history=chat_history)
|
| 102 |
+
return result["reply"]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
async def chat_fn(message: str, chat_history: List[Dict[str, str]], session_id: str = ""):
|
| 106 |
+
"""Streaming generator: yields word-by-word with human-like timing & typos."""
|
| 107 |
+
if not message or not message.strip():
|
| 108 |
+
yield gr.update(interactive=True), chat_history
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
original_history = chat_history.copy()
|
| 112 |
+
chat_history = chat_history + [
|
| 113 |
+
{"role": "user", "content": message},
|
| 114 |
+
{"role": "assistant", "content": ""}
|
| 115 |
+
]
|
| 116 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
reply = await _get_reply(message, original_history)
|
| 120 |
+
except httpx.HTTPStatusError as e:
|
| 121 |
+
logger.error(f"HTTP Error: {e.response.status_code} - {e.response.text}")
|
| 122 |
+
reply = "⚠️ Rate limit reached. Please slow down a bit!" if e.response.status_code == 429 \
|
| 123 |
+
else "⚠️ I encountered an error. Please try again in a few seconds."
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
|
| 126 |
+
reply = f"⚠️ Oops! Something went wrong."
|
| 127 |
+
|
| 128 |
+
# ── Humanistic letter-by-letter typing with occasional typos ──────
|
| 129 |
+
displayed = ""
|
| 130 |
+
|
| 131 |
+
# We'll process word by word to decide on typos, but type letter by letter
|
| 132 |
+
words = reply.split(" ")
|
| 133 |
+
for i, word in enumerate(words):
|
| 134 |
+
separator = " " if i > 0 else ""
|
| 135 |
+
|
| 136 |
+
# Decide if this word gets a typo
|
| 137 |
+
do_typo = (
|
| 138 |
+
i > 0
|
| 139 |
+
and len(word) > 4
|
| 140 |
+
and random.random() < TYPO_CHANCE
|
| 141 |
+
and word.isalpha()
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
if do_typo:
|
| 145 |
+
# Pick a random position to make a mistake
|
| 146 |
+
typo_pos = random.randint(2, len(word) - 1)
|
| 147 |
+
|
| 148 |
+
# 1. Type the word correctly up to the typo position
|
| 149 |
+
for char in (separator + word[:typo_pos]):
|
| 150 |
+
displayed += char
|
| 151 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 152 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 153 |
+
await asyncio.sleep(random.uniform(0.03, 0.07))
|
| 154 |
+
|
| 155 |
+
# 2. Type the wrong character
|
| 156 |
+
wrong_char = _nearby_char(word[typo_pos])
|
| 157 |
+
displayed += wrong_char
|
| 158 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 159 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 160 |
+
await asyncio.sleep(random.uniform(0.15, 0.3)) # Pause after error
|
| 161 |
+
|
| 162 |
+
# 3. Realize and Backspace
|
| 163 |
+
await asyncio.sleep(random.uniform(0.2, 0.4))
|
| 164 |
+
displayed = displayed[:-1]
|
| 165 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 166 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 167 |
+
await asyncio.sleep(random.uniform(0.1, 0.2))
|
| 168 |
+
|
| 169 |
+
# 4. Type the correct character and the rest of the word
|
| 170 |
+
for char in word[typo_pos:]:
|
| 171 |
+
displayed += char
|
| 172 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 173 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 174 |
+
await asyncio.sleep(random.uniform(0.04, 0.08))
|
| 175 |
+
else:
|
| 176 |
+
# Type the whole word (including separator) letter by letter
|
| 177 |
+
for char in (separator + word):
|
| 178 |
+
displayed += char
|
| 179 |
+
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 180 |
+
yield gr.update(value="", interactive=False), chat_history
|
| 181 |
+
|
| 182 |
+
# Faster typing for letters, slight pause for space
|
| 183 |
+
if char == " ":
|
| 184 |
+
await asyncio.sleep(random.uniform(0.08, 0.15))
|
| 185 |
+
else:
|
| 186 |
+
await asyncio.sleep(random.uniform(0.02, 0.06))
|
| 187 |
+
|
| 188 |
+
# Punctuation pauses (at the end of words)
|
| 189 |
+
if word and word[-1] in ".!?":
|
| 190 |
+
await asyncio.sleep(random.uniform(0.4, 0.8))
|
| 191 |
+
elif word and word[-1] in ",:;":
|
| 192 |
+
await asyncio.sleep(random.uniform(0.2, 0.4))
|
| 193 |
+
|
| 194 |
+
# ── Fire-and-forget: persist to session store (never blocks the UI) ───
|
| 195 |
+
if session_id:
|
| 196 |
+
asyncio.create_task(_ss.save_message(session_id, message, displayed))
|
| 197 |
+
|
| 198 |
+
# Re-enable the input box at the end
|
| 199 |
+
yield gr.update(interactive=True), chat_history
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
custom_css = """
|
| 203 |
+
/* Aggressively force height: auto and min-height: 40px on the chatbot and flex containers */
|
| 204 |
+
.gradio-container, .flex, .block, #chatbot-window {
|
| 205 |
+
height: auto !important;
|
| 206 |
+
min-height: 40px !important;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* Ensure the chatbot doesn't overflow the viewport if it gets too full */
|
| 210 |
+
#chatbot-window {
|
| 211 |
+
max-height: 70vh !important;
|
| 212 |
+
border: none !important;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* Aggressively hide the footer, including HF injected footers if inside the container */
|
| 216 |
+
footer, .footer, footer * {
|
| 217 |
+
display: none !important;
|
| 218 |
+
visibility: hidden !important;
|
| 219 |
+
height: 0 !important;
|
| 220 |
+
margin: 0 !important;
|
| 221 |
+
padding: 0 !important;
|
| 222 |
+
}
|
| 223 |
+
"""
|
| 224 |
+
|
| 225 |
+
# JS: runs immediately on page load — generates/loads session_id from localStorage
|
| 226 |
+
# and restores previous chat history. Completely non-blocking.
|
| 227 |
+
_SESSION_JS = """
|
| 228 |
+
async () => {
|
| 229 |
+
// ── Session ID: generate once, persist forever in localStorage ──
|
| 230 |
+
let sid = localStorage.getItem('martech_session_id');
|
| 231 |
+
if (!sid) {
|
| 232 |
+
sid = 'sess-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 233 |
+
localStorage.setItem('martech_session_id', sid);
|
| 234 |
+
}
|
| 235 |
+
// Write the session_id into the hidden Gradio textbox
|
| 236 |
+
const el = document.getElementById('session-id-box')?.querySelector('textarea');
|
| 237 |
+
if (el) { el.value = sid; el.dispatchEvent(new Event('input', {bubbles:true})); }
|
| 238 |
+
return sid;
|
| 239 |
+
}
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
with gr.Blocks(title="Martechsol Assistant", theme=gr.themes.Soft(), css=custom_css) as demo:
|
| 243 |
+
gr.Markdown("# Martechsol Assistant")
|
| 244 |
+
gr.Markdown("Welcome! How can I help you today?")
|
| 245 |
+
|
| 246 |
+
chatbot = gr.Chatbot(show_label=False, elem_id="chatbot-window")
|
| 247 |
+
|
| 248 |
+
with gr.Row():
|
| 249 |
+
msg = gr.Textbox(
|
| 250 |
+
placeholder="Type your question here...",
|
| 251 |
+
show_label=False,
|
| 252 |
+
scale=9
|
| 253 |
+
)
|
| 254 |
+
send = gr.Button("Send", variant="primary", scale=1)
|
| 255 |
+
|
| 256 |
+
clear = gr.Button("Clear Chat History")
|
| 257 |
+
|
| 258 |
+
# Hidden textbox holds the session_id — invisible to the user
|
| 259 |
+
session_id = gr.Textbox(
|
| 260 |
+
value="",
|
| 261 |
+
visible=False,
|
| 262 |
+
elem_id="session-id-box",
|
| 263 |
+
label="session_id",
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# On load: run JS to set session_id from localStorage (non-blocking)
|
| 267 |
+
demo.load(fn=None, inputs=None, outputs=session_id, js=_SESSION_JS)
|
| 268 |
+
|
| 269 |
+
# Wire up the events — streaming generator for live typing effect (UNCHANGED)
|
| 270 |
+
send.click(chat_fn, inputs=[msg, chatbot, session_id], outputs=[msg, chatbot])
|
| 271 |
+
msg.submit(chat_fn, inputs=[msg, chatbot, session_id], outputs=[msg, chatbot])
|
| 272 |
+
clear.click(lambda: [], None, chatbot)
|
| 273 |
+
|
| 274 |
+
if __name__ == "__main__":
|
| 275 |
+
demo.launch(server_name="0.0.0.0", server_port=7860, show_api=False)
|
data/index/.gitkeep
ADDED
|
File without changes
|
data/sessions/.gitkeep
ADDED
|
File without changes
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
services:
|
| 3 |
+
rag-api:
|
| 4 |
+
build: .
|
| 5 |
+
container_name: rag-api
|
| 6 |
+
ports:
|
| 7 |
+
- "8000:8000"
|
| 8 |
+
env_file:
|
| 9 |
+
- .env
|
| 10 |
+
volumes:
|
| 11 |
+
- ./docs:/app/docs
|
| 12 |
+
- ./data/index:/app/data/index
|
| 13 |
+
- ./data/sessions:/app/data/sessions
|
| 14 |
+
restart: unless-stopped
|
docs/MartechSol_Employee_HandBook (7) (1).pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a48b14cc28d7892ae03a5dfe06a244832e2807ce178ca1f1acdbc58d6af4e8ef
|
| 3 |
+
size 3844502
|
docs/MartechSol_Employee_Handbook_Extracted.txt
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Employee Handbook
|
| 2 |
+
Last Modified: September 1st, 2024
|
| 3 |
+
|
| 4 |
+
Contents
|
| 5 |
+
Introduction ....................................................................................................... 4
|
| 6 |
+
Workplace Orientation ....................................................................................... 4
|
| 7 |
+
Employment Status .......................................................................................... 5
|
| 8 |
+
Employment At Will .......................................................................................... 5
|
| 9 |
+
Employee Responsibilities ........................................................................................ 5
|
| 10 |
+
Workweek and Workday .................................................................................... 5
|
| 11 |
+
Time Clocks and Time Cards ............................................................................... 5
|
| 12 |
+
General Attendance Requirements ....................................................................... 6
|
| 13 |
+
Work from Home .............................................................................................. 6
|
| 14 |
+
Problem Resolution ........................................................................................... 7
|
| 15 |
+
Personal Appearance ......................................................................................... 7
|
| 16 |
+
Health Safety .................................................................................................. 7
|
| 17 |
+
Employee Relationships ..................................................................................... 8
|
| 18 |
+
Policy Against Harassment ................................................................................. 8
|
| 19 |
+
Activity Permission ........................................................................................... 9
|
| 20 |
+
Confidentiality ................................................................................................ 9
|
| 21 |
+
Intangible Property ......................................................................................... 10
|
| 22 |
+
Restraint of Trade, Non-Compete, and Cooling Off Period ....................................... 11
|
| 23 |
+
Solicitation ................................................................................................... 11
|
| 24 |
+
Cell Phone Usage ........................................................................................... 12
|
| 25 |
+
Computer, E-Mail and Internet Policy ................................................................. 12
|
| 26 |
+
Tobacco Use .................................................................................................. 13
|
| 27 |
+
Grievance Procedure ....................................................................................... 13
|
| 28 |
+
Copyright Violation ......................................................................................... 13
|
| 29 |
+
Data Theft .................................................................................................... 13
|
| 30 |
+
Separation from the Company .......................................................................... 14
|
| 31 |
+
Exit Interviews .............................................................................................. 14
|
| 32 |
+
Final Settlement ............................................................................................. 14
|
| 33 |
+
Return of Property .......................................................................................... 15
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
Acceptability ................................................................................................. 15
|
| 37 |
+
Work Rules ................................................................................................... 15
|
| 38 |
+
Level 1 Actions .............................................................................................. 16
|
| 39 |
+
Level 2 Actions .............................................................................................. 16
|
| 40 |
+
Level 3 Actions .............................................................................................. 17
|
| 41 |
+
Disciplinary Procedures ..................................................................................... 18
|
| 42 |
+
Employee Benefits ................................................................................................. 18
|
| 43 |
+
Referral Program ............................................................................................ 18
|
| 44 |
+
Game Room .................................................................................................. 18
|
| 45 |
+
Holidays and Holiday Pay ................................................................................. 18
|
| 46 |
+
Pay Period and Paychecks ................................................................................ 19
|
| 47 |
+
Leaves ......................................................................................................... 19
|
| 48 |
+
Sick Leaves ................................................................................................... 19
|
| 49 |
+
Casual Leaves ............................................................................................... 20
|
| 50 |
+
Annual Leaves ............................................................................................... 21
|
| 51 |
+
Hajj Leave .................................................................................................... 22
|
| 52 |
+
Maternity Leave ............................................................................................. 23
|
| 53 |
+
Paternity Leave .............................................................................................. 23
|
| 54 |
+
Bereavement Leave ........................................................................................ 24
|
| 55 |
+
Unauthorized Leaves ....................................................................................... 24
|
| 56 |
+
Unapproved Absence Without Pay ..................................................................... 25
|
| 57 |
+
Leave Encashment ......................................................................................... 25
|
| 58 |
+
Other Benefits ..................................................................................................... 25
|
| 59 |
+
Loans & Advance Salary .................................................................................. 25
|
| 60 |
+
Maternity (Pregnancy) Benefit .......................................................................... 26
|
| 61 |
+
Company Maintained Vehicle ............................................................................ 27
|
| 62 |
+
Healthcare Insurance – For Self & Spouse ............................................................ 27
|
| 63 |
+
EOBI: .......................................................................................................... 30
|
| 64 |
+
Provident Fund: ............................................................................................. 30
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
Introduction
|
| 68 |
+
We are providing you this employee handbook to introduce you to our policies. The handbook will
|
| 69 |
+
describe some of the more important employee benefits and obligations.
|
| 70 |
+
Our business is constantly changing, and therefore MartechSol reserves the right to change all of our
|
| 71 |
+
policies, including those covered in this handbook, at any time. You will be informed of any changes
|
| 72 |
+
in policy through posting on portal or through other appropriate means. If you are uncertain about
|
| 73 |
+
any policy or procedure, please check with your Supervising Authority.
|
| 74 |
+
Workplace Orientation
|
| 75 |
+
Orientati
|
| 76 |
+
on Training and Initial Evaluation Period
|
| 77 |
+
All employees will receive orientation training at the beginning of employment with MartechSol. This
|
| 78 |
+
period of orientation will include familiarization with Logicose’s work policies, disciplinary policies, job
|
| 79 |
+
duties, and reporting responsibilities.
|
| 80 |
+
Upon completion of orientation, all employees will undergo evaluation during the first ninety (90)
|
| 81 |
+
calendar days of employment. If performance is unsatisfactory for any reason during this period of time,
|
| 82 |
+
the employee may be immediately dismissed from employment. At the end of this 90-day period, the
|
| 83 |
+
employee’s Supervising Authority will determine if the employee’s performance warrants continued
|
| 84 |
+
employment.
|
| 85 |
+
If the employee’s performance is not up to the Supervising Authority’s expectations in terms of quality of
|
| 86 |
+
work, output, behavior, or any other performance indicator, the employee will not be confirmed for
|
| 87 |
+
further employment at the company after this 90-day period. It is also up to the Supervising Authority to
|
| 88 |
+
end the probation period at any point before 90 days and either confirm or terminate the employee.
|
| 89 |
+
The Supervising Authority is required to make a decision regarding the confirmation of an employee after
|
| 90 |
+
90 days. If the Supervising Authority requires more time to evaluate an employee further, (s)he has the
|
| 91 |
+
right extend the probation period.
|
| 92 |
+
Confirmation
|
| 93 |
+
If performance exceeds expectations, an employee may receive a salary increment upon confirmation.
|
| 94 |
+
The date of confirmation depends on date of joining and performance. Revised salaries for employees
|
| 95 |
+
that are confirmed before the 15th of a given month will be effective from the 1st of that same month. On
|
| 96 |
+
the other hand, revised salaries will be effective from the 1st of the following month for employees that
|
| 97 |
+
are confirmed after the 15th of a given month. For example, if someone is confirmed on the 10th of April,
|
| 98 |
+
the revised salary will be effective from the 1st April, however if they are confirmed on the 16th of April,
|
| 99 |
+
the revised salary will be effective from the 1st of May.
|
| 100 |
+
Casual and sick leaves will also be assigned on a pro-rata basis based on this calculation.
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
Employment Status
|
| 104 |
+
A “regular full-time employee” is any employee who is employed to work on a year-round basis, and who
|
| 105 |
+
is regularly scheduled to work their full shift hours.
|
| 106 |
+
Employment At Will
|
| 107 |
+
All employees are employed on an "at-will" basis. This means that employees are not employed for any
|
| 108 |
+
specific term or period of time, and that the employer may terminate the employment relationship at
|
| 109 |
+
any time for any reason. Similarly, employees may resign and terminate employment with MartechSol
|
| 110 |
+
at any time and for any reason with prior notice of 30-90 days depending on their role within the
|
| 111 |
+
organization.
|
| 112 |
+
N
|
| 113 |
+
o employee has any authority to change the nature of this relationship through any statements,
|
| 114 |
+
representations, or conduct. The at-will relationship may be changed only through written and signed
|
| 115 |
+
authorization of the Supervising Authority.
|
| 116 |
+
Employee Responsibilities
|
| 117 |
+
Workweek and Workday
|
| 118 |
+
The nor
|
| 119 |
+
mal workweek shall be considered full shift hours, six days per week, from 9:00 a.m. to 5:30 p.m.
|
| 120 |
+
The first Saturday of every month is off and the remaining three Saturday’s in each month are six and a
|
| 121 |
+
half hours (half-day). One hour is allocated for lunch/dinner in all shifts.
|
| 122 |
+
Morning Shift: 1:30 pm - 2:30 pm
|
| 123 |
+
Evening Shift: 9:30 pm - 10:30 pm
|
| 124 |
+
Smoking Breaks: There are a maximum of 3 breaks allowed in a workday (other than lunch/dinner
|
| 125 |
+
break). These breaks are not to be longer than 5 minutes.
|
| 126 |
+
If any other break is required, the employee is to contact his or her Supervising Authority for approval.
|
| 127 |
+
Time Clocks and Time Cards
|
| 128 |
+
All empl
|
| 129 |
+
oyees shall use the portal to record their starting time and ending time. For wage payment
|
| 130 |
+
purposes, time worked will be figured to the nearest tenth of an hour.
|
| 131 |
+
Sharing login information (username and password) is strictly prohibited. Each person is to use their own
|
| 132 |
+
login information to time in at work. If any discrepancy is observed in the time in or time out process,
|
| 133 |
+
harsh disciplinary action will be taken against the person or persons involved.
|
| 134 |
+
In case an employee forgets to time in or is unable to time in, they are to submit a trouble ticket to the
|
| 135 |
+
Human Resources department. It is the employee’s responsibility to maintain their time sheet accurately.
|
| 136 |
+
Monthly salary will be calculated according to the time sheet.
|
| 137 |
+
Employees who want to change their work shift should contact their Supervising Authority for approval.
|
| 138 |
+
Necessary paperwork and legitimate reason are required for the shift change process. The decision to
|
| 139 |
+
change the shift of an employee is at the discretion of the Supervising Authority.
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
General Attendance Requirements
|
| 143 |
+
Regular and pro
|
| 144 |
+
mpt attendance of employees is essential to our business. Employees who cannot meet
|
| 145 |
+
our attendance requirements will be subject to counseling and disciplinary action. Excessive absenteeism
|
| 146 |
+
or tardiness will result in termination of employment. The Supervisor has the authority to mark a half
|
| 147 |
+
day, day off, or full day at his/her discretion.
|
| 148 |
+
Late Coming: Late coming is permitted till 20 minutes from your shift start time. If you arrive
|
| 149 |
+
after 20 minutes of your shift start time, then it will be considered as a late arrival. 4 late arrivals
|
| 150 |
+
in a month will be considered as a half day off. In addition, an employee is to inform his/her
|
| 151 |
+
Supervising Authority if they will be more than 30 minutes late to work.
|
| 152 |
+
Early Out: If you need to leave before your shift ends, verbal approval is mandatory from your
|
| 153 |
+
Supervising Authority. There will be no deductions for early out.
|
| 154 |
+
Half Day: Half day will be marked if you arrive 2 hours after your shift start time. If you complete
|
| 155 |
+
less than six and a half hours on any given day, then it will also be considered as a half day off.
|
| 156 |
+
Full Day: Employees who spend less than 2 hours at work on any workday will be marked absent
|
| 157 |
+
that day.
|
| 158 |
+
Employees who are late to work on any given workday are still responsible to complete all the
|
| 159 |
+
work assigned to them. If they are unable to, they should contact their Supervising Authority
|
| 160 |
+
immediately. If an employee arrives at work after 1 hour of their shift start time, by default,
|
| 161 |
+
the 8.5 hours shift has to be completed. If an employee is more than 1 hour late and wants
|
| 162 |
+
to
|
| 163 |
+
leave early (without completing the total (8.5 hr) shift time), he/she has to contact their
|
| 164 |
+
Supervising Authority for approval.
|
| 165 |
+
For
|
| 166 |
+
employees who are unable to complete at least 50% of their work during a workday, the
|
| 167 |
+
Supervising Authority reserves the right to decide the status of the workday (full day, off day,
|
| 168 |
+
half day) is at his or her discretion.
|
| 169 |
+
Work from Home
|
| 170 |
+
In case o
|
| 171 |
+
f emergency (bad weather, temporary disability, unrest in city, etc) when employees cannot
|
| 172 |
+
reach work, they will be requested to work from home. In case an employee has personal valid reason(s)
|
| 173 |
+
as to why they cannot reach work, they can work from home requesting approval from their Supervising
|
| 174 |
+
Authority. The approval of this request is at the Supervising Authority’s discretion.
|
| 175 |
+
If less than 30% of the work has been completed, the day will be counted as a full day off. If 30-70% of
|
| 176 |
+
the work has been completed, it will be counted as a half day. If more than 70% of the work has been
|
| 177 |
+
completed, it will be counted as a full day.
|
| 178 |
+
Work can be submitted no later than 3 hours after the end of the employee’s regular shift timing. If the
|
| 179 |
+
work is not submitted within the given time, it will not be marked as work provided for the same day. If
|
| 180 |
+
an employee faces problems submitting their work within the given duration and wants to submit their
|
| 181 |
+
work later, approval is required from their Supervising Authority.
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
Problem Resolution
|
| 185 |
+
If an emplo
|
| 186 |
+
yee faces difficulty or problems related to anything within the workplace, it is their
|
| 187 |
+
responsibility to submit a trouble ticket through the Portal. There is no need to contact the Supervising
|
| 188 |
+
Authority unless their ticket has not been addressed for more than 24 hours.
|
| 189 |
+
You are requested to use this trouble ticket system for your computer, admin and HR related
|
| 190 |
+
issues. How to use this system:
|
| 191 |
+
1. If you have any computer, admin, or HR related issue, go to the tickets link in the portal and
|
| 192 |
+
select ‘Submit a ticket’.
|
| 193 |
+
2. Select the concerned department for your problem e.g. if your mouse, keyboard, CPU or LCD is
|
| 194 |
+
not working properly, please select Networking. If there is any issue with your attendance or
|
| 195 |
+
payroll, select Human Resources. If there is any issue with fan, AC, kitchen item etc. please select
|
| 196 |
+
Admin department.
|
| 197 |
+
3. Once your trouble ticket is submitted, you may view the status of your ticket from ‘View open
|
| 198 |
+
tickets’ link.
|
| 199 |
+
As soon as your ticket is submitted, the Company will make every possible effort to resolve your issue on
|
| 200 |
+
an urgent basis.
|
| 201 |
+
There is also a messaging feature in this trouble ticket system. If there is any update related to your
|
| 202 |
+
ticket, you will receive a message within your open ticket. Similarly, if you want to send any message
|
| 203 |
+
related to your problem, you can also do that from ‘View open tickets’ section.
|
| 204 |
+
Once your issue is resolved, your ticket will be closed. You may view your closed tickets from ‘View your
|
| 205 |
+
closed tickets’ section.
|
| 206 |
+
This system will help resolve your issues quickly and will allow you to view status of your open tickets.
|
| 207 |
+
Personal Appearance
|
| 208 |
+
A company
|
| 209 |
+
is judged by its employees, as well as by its products and services. All employees are
|
| 210 |
+
expected to maintain good standards of personal neatness, including hair, clothes, and shoes.
|
| 211 |
+
Casual to business-style dress is appropriate. Employees should be neatly groomed and clothes should be
|
| 212 |
+
clean and in good condition. Leisure clothes such as cut-offs or halter tops are not acceptable attire for
|
| 213 |
+
the business office.
|
| 214 |
+
Health Safety
|
| 215 |
+
If an employee has had a contagious illness or serious health issue which may affect the health of other
|
| 216 |
+
employees (e.g. chicken pox, flu), a doctor’s written permission to return to work will be necessary.
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
Employee Relationships
|
| 220 |
+
MartechSol employs a number of people and, as a result, you will be working with people whose
|
| 221 |
+
personalities may differ from yours. It is your responsibility to get along with all of your co-workers. A
|
| 222 |
+
spirit of cooperation shown by all of us will help to accomplish our purpose of providing new
|
| 223 |
+
customers with the best possible service.
|
| 224 |
+
This does not mean that you need to become a personal friend of all your co-workers, but it does mean
|
| 225 |
+
that you should treat your co-workers with respect. Regardless of your personal opinions, your fellow
|
| 226 |
+
employee should be treated exactly as you would like to have that person treat you.
|
| 227 |
+
Senior employees should remember that they were once new employees and therefore should do
|
| 228 |
+
everything possible to aid new employees in acclimating themselves to their new jobs.
|
| 229 |
+
Policy
|
| 230 |
+
Against Harassment
|
| 231 |
+
MartechSol is committed to maintaining a work environment that is free of discrimination and
|
| 232 |
+
harassment. In keeping with this commitment, we will not tolerate harassment of our employees by
|
| 233 |
+
anyone, including any Supervising Authority, co-worker, vendor, client, or customer.
|
| 234 |
+
Harassment consists of unwelcome conduct, whether verbal, physical, or visual, that is based upon a
|
| 235 |
+
person’s protected status, such as sex, color, race, ancestry, religion, national origin, age, physical
|
| 236 |
+
handicap, medical condition, disability, marital status, veteran status, citizenship status, or other
|
| 237 |
+
protected group status.
|
| 238 |
+
MartechSol will not tolerate harassing conduct that affects tangible job benefits, that interferes
|
| 239 |
+
unreasonably with an individual’s work performance, or that creates an intimidating, hostile, or
|
| 240 |
+
offensive working environment.
|
| 241 |
+
Sexual harassment deserves special mention. Unwelcome sexual advances, requests for sexual favors,
|
| 242 |
+
and other physical, verbal, or visual conduct based on sex constitute sexual harassment when (1)
|
| 243 |
+
submission to the conduct is made a term or condition of employment, (2) submission to or rejection of
|
| 244 |
+
the conduct is used as the basis for an employment decision, or (3) the conduct has the purpose or effect
|
| 245 |
+
of unreasonably interfering with an individual’s work performance or creating an intimidating, hostile, or
|
| 246 |
+
offensive working environment. Sexual harassment may include explicit sexual proposition, sexual
|
| 247 |
+
innuendo, suggestive comments, sexually oriented “kidding” or “teasing,” “practical jokes,” jokes about
|
| 248 |
+
gender-specific traits, foul or obscene language or gestures, displays of foul or obscene printed or visual
|
| 249 |
+
material, and physical contact, such as patting, pinching, or brushing against another’s body.
|
| 250 |
+
All employees are responsible for helping to assure we avoid harassment. If you feel that you
|
| 251 |
+
have experienced or witnessed harassment, you should immediately notify your Supervising
|
| 252 |
+
Authority. MartechSol forbids retaliation against anyone who has reported harassment.
|
| 253 |
+
It is our policy to investigate all such complaints thoroughly and promptly. To the fullest extent
|
| 254 |
+
practicable, MartechSol will keep complaints and the terms of their resolution confidential. If an
|
| 255 |
+
investigation confirms that harassment has occurred, MartechSol will take appropriate corrective
|
| 256 |
+
action, including discipline up to and including immediate dismissal from employment.
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
Activity Permission
|
| 260 |
+
MartechSol is concerned with outside activities of employees only where such activities might involve a
|
| 261 |
+
conflict of interest with Logicose’s best interests. A conflict of interest exists if an employee has a
|
| 262 |
+
private financial interest or other relationship outside MartechSol that is detrimental to the best
|
| 263 |
+
interests of MartechSol.
|
| 264 |
+
You shall not without prior written consent of the company, engage directly or indirectly, in any trade,
|
| 265 |
+
business or occupation or any other income generation activity.
|
| 266 |
+
If an employee desires to attend an academic institution during his/her employment, (s)he must receive
|
| 267 |
+
permission from the company by submitting relevant documentation. It is mandatory to fill the Activity
|
| 268 |
+
Permission form for approval.
|
| 269 |
+
An employee should discuss any possible conflict of interest with the HR Department as full disclosure
|
| 270 |
+
and open knowledge are essential. For the employee’s own protection, written approval should be
|
| 271 |
+
obtained in all cases where a possible conflict of interest is involved.
|
| 272 |
+
Other possible conflict of interest situations may include any type of “moonlighting work” or a second
|
| 273 |
+
job, which interferes with the employee’s work for MartechSol, any type of external business
|
| 274 |
+
relationship with MartechSol, any of its competitors or any of its customers, and any type of public
|
| 275 |
+
activity which may affect adversely on MartechSol.
|
| 276 |
+
Confidentiality
|
| 277 |
+
MartechSol is a service provider that creates, completes, and delivers work for clients that do not want
|
| 278 |
+
our involvement disclosed in their projects. The ownership of the work we create for our clients is
|
| 279 |
+
transferred over to them and they hold the copyright to the work delivered. Disclosing such work may
|
| 280 |
+
result in Copyright Infringement, so it is in your, and the company’s best interest, to not share
|
| 281 |
+
involvement in such work.
|
| 282 |
+
Moreover, employees are prohibited from maintaining records of confidential information or intellectual
|
| 283 |
+
property on personal e-mail accounts, computers, hard drives, smart phones, cloud, or online.
|
| 284 |
+
Both during employment with MartechSol and after separation, employees are not permitted to
|
| 285 |
+
disclose to any third party or share any confidential information that is made available to you.
|
| 286 |
+
Confidential information includes, but is not limited to:
|
| 287 |
+
Web copy, eBooks, Press Releases, Reports, Infographics, Articles, Blogs, and/or any copy that was
|
| 288 |
+
created or revised for clients while employed at MartechSol is the property of either the clients or
|
| 289 |
+
MartechSol. Disclosing involvement in such projects to those outside the company, mentioning it
|
| 290 |
+
on your CV or resume, and/or making public your involvement in such projects is prohibited.
|
| 291 |
+
Search Engine Optimization strategy, campaigns, processes, proposals, accounts for posting, list
|
| 292 |
+
of clients, contact information of clients, content created or revised for client is confidential
|
| 293 |
+
information. Mentioning your involvement and/or disclosing such information to those outside
|
| 294 |
+
the company or on your CV or resume is considered breach of policy.
|
| 295 |
+
Client Information, leads, pricing strategy, proposals, account information, portal information,
|
| 296 |
+
samples, and promotional campaigns are the intellectual property of MartechSol. They may
|
| 297 |
+
neither be disclosed nor shared with those outside the organization.
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
Sensitive company information, such as employee contact details, software code, promotional
|
| 301 |
+
campaigns, client list, project(s) details, performance evaluation system, and in-house software.
|
| 302 |
+
Any and all such information is either Logicose’s or its Client’s intellectual property.
|
| 303 |
+
Immediately upon termination of employment, or at any other time upon the company’s request, all
|
| 304 |
+
employees will return to the Company:
|
| 305 |
+
All memoranda, notes and data, and computer software and hardware, records or other
|
| 306 |
+
data/documents compiled by the employee or made available to the employee during the
|
| 307 |
+
employee’s employment with the company concerning the business of the company, its
|
| 308 |
+
affiliates, or their customers.
|
| 309 |
+
All other Confidential Information that is disclosed to you during employment.
|
| 310 |
+
All physical and intellectual property of the company or its affiliates, including without limitation,
|
| 311 |
+
all the content, information, plans, files, records, documents, lists, equipment, supplies,
|
| 312 |
+
promotional materials, keys, vehicles, and similar items and all copies thereof or extracts there
|
| 313 |
+
from.
|
| 314 |
+
Disclosure of such information while employed or after separation from MartechSol is unethical, and
|
| 315 |
+
such actions will be considered intellectual property theft.
|
| 316 |
+
Furthermore, you cannot claim ownership of any project or developed content/material in any manner.
|
| 317 |
+
For example, giving a reference on your CV to a specific website for which you developed content while
|
| 318 |
+
working at the Company is prohibited. Any project or client details relating to your work carried out at
|
| 319 |
+
this company cannot be mentioned on your CV.
|
| 320 |
+
Intangible Property
|
| 321 |
+
During your course of employment with MartechSol, you will come across information that is privy to
|
| 322 |
+
those outside the organization. Such information is the intellectual (intangible) property of the company.
|
| 323 |
+
You may not at any time, during or after employment with MartechSol, have or claim any right, title, or
|
| 324 |
+
interest in any trade name, trademark, patent, copyright, work for hire, or other similar rights belonging
|
| 325 |
+
to or used by the company and shall not have or claim any right, title, or interest in any material or
|
| 326 |
+
matter of any sort prepared for or used in connection with the business or promotion of the company,
|
| 327 |
+
whatever your involvement with such matters may have been, and whether procured, produced,
|
| 328 |
+
prepared, or published in whole or in part by you.
|
| 329 |
+
MartechSol, or its clients, have and will retain the sole and exclusive rights in any and all such trade
|
| 330 |
+
names, trademarks, patents, copyrights, material, and matter unless transferred over to a third party.
|
| 331 |
+
Any and all documents, software, or copy that was created for MartechSol, its clients, or its affiliate
|
| 332 |
+
brand names, is the intellectual property of the company or its clients. You may not claim ownership,
|
| 333 |
+
maintain personal records of, or disclose your involvement in the creation or modification of such
|
| 334 |
+
information, software, or projects.
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
Restraint of Trade, Non-Compete, and Cooling Off Period
|
| 338 |
+
Any current or separated employee is prohibited from incorporating a new company or starting a new
|
| 339 |
+
business in the same industry before the time of at least 24 months.
|
| 340 |
+
Employee shall not, on his or her own behalf or behalf of others, hold any ownership interest in any
|
| 341 |
+
business engaged in the following industries:
|
| 342 |
+
Copywriting
|
| 343 |
+
Online Marketing
|
| 344 |
+
SEO
|
| 345 |
+
SMO
|
| 346 |
+
Ghostwriting
|
| 347 |
+
Industries that MartechSol, or any of its brand names, are currently or in the process of operating in
|
| 348 |
+
Employee agrees and covenants that the use of sensitive and confidential information that is made available,
|
| 349 |
+
in certain circumstances, may cause irreparable damage to MartechSol and its reputation. Therefore,
|
| 350 |
+
employee shall not until the expiration of two years after the termination of the employment,
|
| 351 |
+
through any corporations or associates in any business, form a business that is directly competitive with
|
| 352 |
+
Company for a period of two years.
|
| 353 |
+
Since the region or territory in which MartechSol operates is based online, the jurisdiction of the
|
| 354 |
+
restraint of trade is not limited to a specific city, state, or country. Forming an organization that is based
|
| 355 |
+
in any city MartechSol operates in, and/or creation of an online business is a breach of policy.
|
| 356 |
+
Failure to comply with activity permission, restraint of trade, confidentiality, intangible property, non-
|
| 357 |
+
disclosure, and non-compete will result in immediate termination. The Company also reserves the right
|
| 358 |
+
to take appropriate legal action upon breach of policy both during employment and after separation.
|
| 359 |
+
Solicitation
|
| 360 |
+
Both during employment with the company and for a period of two (2) years following the termination/
|
| 361 |
+
resignation of employment with the company at any time and for any reason, employee’s will not, directly
|
| 362 |
+
or indirectly, on the employee’s own behalf or on behalf of any other person or entity, hire or solicit to
|
| 363 |
+
hire for employment or consulting other provision of services, any person who is actively employed or
|
| 364 |
+
engaged (or in the preceding six months was actively employed or engaged) by MartechSol.
|
| 365 |
+
This includes, but is not limited to, inducting or attempting to induce, or influencing or attempting to
|
| 366 |
+
influence, any person employed or engaged by the company to terminate his or her relationship with the
|
| 367 |
+
company. MartechSol reserves the right to take appropriate legal action against solicitation of employees.
|
| 368 |
+
Furthermore, both during the employee’s employment and after separation from the company at any
|
| 369 |
+
time and for any reason, the employee will not directly or indirectly, on the employee’s own behalf of
|
| 370 |
+
any other person or entity, solicit the business of any Customer or any other entity with which
|
| 371 |
+
MartechSol has worked with or discussed the possibility of any work, at the time of employee’s
|
| 372 |
+
termination, to provide services to such entity (a “Contractor”). Client information is confidential, and
|
| 373 |
+
MartechSol reserves the right to take legal action upon disclosure of privy information.
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
Cell Phone Usage
|
| 377 |
+
Emp
|
| 378 |
+
loyees shall not use cell phones for personal purposes while engaged in work activities. If the
|
| 379 |
+
employee needs to use the cell phone for important personal reasons, the employee shall excuse
|
| 380 |
+
themselves from the premises. Keep your cell phone on silent/vibrate. Use it responsibly, and make sure
|
| 381 |
+
that you do not disturb others while answering your calls.
|
| 382 |
+
Computer, E-Mail and Internet Policy
|
| 383 |
+
MartechSol maintains certain policy requirements for use of its “electronic communication systems.”
|
| 384 |
+
“Electronic communication systems” include computers, file servers, electronic information storage
|
| 385 |
+
systems, internal and external electronic mail systems, voice mail systems, local area networks and
|
| 386 |
+
any other system or technology used now or in the future by MartechSol to store or transmit data of
|
| 387 |
+
any kind electronically.
|
| 388 |
+
Our
|
| 389 |
+
policy requires the following:
|
| 390 |
+
1. Employees shall use the electronic communications systems for business purposes.
|
| 391 |
+
2. Logicose’s electronic communications systems and all information transmitted or received by, or
|
| 392 |
+
stored or contained in, these systems are the property of MartechSol. No employee shall have
|
| 393 |
+
any property rights in or expectation of privacy for any information, even if of a personal nature,
|
| 394 |
+
communicated or received through Logicose’s electronic communications systems. MartechSol
|
| 395 |
+
may review, delete and copy any such information without first obtaining the consent of the
|
| 396 |
+
employee who created, sent or received the information. No employee, including system
|
| 397 |
+
administrators and Supervising Authority, shall use any electronic communications systems for
|
| 398 |
+
purposes of satisfying idle curiosity about the affairs of others. There must be a business purpose
|
| 399 |
+
for obtaining access to the files or communications of others.
|
| 400 |
+
3. No employee shall use the electronic communications systems in a manner that constitutes a
|
| 401 |
+
violation of provincial or federal law or that is offensive to others.
|
| 402 |
+
4. Due to the threat to system stability posed by computer viruses and system incompatibility, no
|
| 403 |
+
employee shall install or download software from the Internet or elsewhere on his or her
|
| 404 |
+
computer without the supervision of the office administrator.
|
| 405 |
+
If you have any questions about the policy or its administration, please contact your Supervising
|
| 406 |
+
Authority. Violations of the policy may result in disciplinary action up to and including termination.
|
| 407 |
+
USBs cannot be used on any office computer. If necessary, please contact the System Administrator.
|
| 408 |
+
The completion of assigned work is the employee’s responsibility. If an employee has any issues
|
| 409 |
+
regarding their PC or internet, they are to raise a trouble ticket immediately.
|
| 410 |
+
The use of the following is prohibited:
|
| 411 |
+
Social media websites (e.g. Facebook)
|
| 412 |
+
Chatting websites (e.g. Meebo)
|
| 413 |
+
Chatting applications (e.g. MSN Messenger, Skype)
|
| 414 |
+
Online gaming websites (e.g. miniclips.com)
|
| 415 |
+
E-mail sites (e.g. Hotmail)
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
Political Websites
|
| 419 |
+
Pornography
|
| 420 |
+
Tobacco Use
|
| 421 |
+
MartechSol maintains a smoke-free workplace for all of its employees, which is why smoking is
|
| 422 |
+
prohibited in the workplace. Employees who must smoke are encouraged to do so away from Company
|
| 423 |
+
property and during work and/or meal breaks only.
|
| 424 |
+
Grievance
|
| 425 |
+
Procedure
|
| 426 |
+
MartechSol has a grievance procedure to make sure that problems and complaints an employee has
|
| 427 |
+
concerning his or her treatment or working conditions is called to our attention. This does not
|
| 428 |
+
include computer-related issues or similar technical issues. For such problems, the trouble ticket
|
| 429 |
+
system is provided. Employees with a problem should take the following steps:
|
| 430 |
+
1. Explai
|
| 431 |
+
n the problem to your immediate Supervising Authority. Allow 48 hours for an answer.
|
| 432 |
+
2. If you are still not satisfied, put your grievance in writing and submit it to MartechSol President via
|
| 433 |
+
the Feedback Form on the Portal. If possible, a decision will be given within three working days.
|
| 434 |
+
Copyright Violation
|
| 435 |
+
Any work performed within the premises of MartechSol for its clients, employees, or the Company itself
|
| 436 |
+
is under the sole ownership of MartechSol or those that the company transfers the rights to. Employees
|
| 437 |
+
cannot claim ownership of this work or use it outside company premises for other purposes, this
|
| 438 |
+
includes creating personal records on computers or e-mail accounts that aren’t the property of
|
| 439 |
+
MartechSol. Employees cannot use this work as reference to anyone else either, unless they get written
|
| 440 |
+
consent from MartechSol. Listed below are some examples of copyright violation:
|
| 441 |
+
Showing, sharing, using, creating a personal record of, or claiming ownership or involvement in the
|
| 442 |
+
creation of:
|
| 443 |
+
Co
|
| 444 |
+
py written
|
| 445 |
+
Software developed
|
| 446 |
+
Client contact information
|
| 447 |
+
Payments received at MartechSol
|
| 448 |
+
In
|
| 449 |
+
tellectual property
|
| 450 |
+
Data Theft
|
| 451 |
+
Any data relating to MartechSol, its clients, or employees is under the sole ownership of MartechSol.
|
| 452 |
+
Employees cannot use or transmit this data, as it is not under their ownership. Employees cannot use
|
| 453 |
+
this data as reference either.
|
| 454 |
+
Archiving such work on external hard drives, online, cloud, and/or on computers that are not property
|
| 455 |
+
of MartechSol is a breach of policy.
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
Separation from the Company
|
| 459 |
+
Prio
|
| 460 |
+
r to Confirmation:
|
| 461 |
+
o Minimum one day notice period, or as communicated in the offer letter, is required
|
| 462 |
+
during probationary period on behalf of the employee.
|
| 463 |
+
o If employee performance/behavior is not up to Company expectations, then the
|
| 464 |
+
Supervising Authority has the right to end the probation period and terminate the
|
| 465 |
+
employee.
|
| 466 |
+
After Confirmation:
|
| 467 |
+
o The employment relationship may be terminated either by you or by the company with a
|
| 468 |
+
prior written notice of one month or salary in lieu thereof, unless mentioned below. It is,
|
| 469 |
+
however, understood that no notice is required in case your services are terminated on
|
| 470 |
+
grounds of misconduct, willful neglect of duty, breach of trust or any other dereliction of
|
| 471 |
+
duty or misdemeanor prejudicial to the interest of the company. It is further understood
|
| 472 |
+
that any financial indebtedness of yours, standing to the detriment of the company’s
|
| 473 |
+
prestige will amount to a very serious misdemeanor towards the company
|
| 474 |
+
o A minimum notice period of at least 30 days is to served unless listed below otherwise:
|
| 475 |
+
Managerial Roles
|
| 476 |
+
Vice Team Lead 30 days
|
| 477 |
+
Team Lead 60 days
|
| 478 |
+
Group Team Lead 90 days
|
| 479 |
+
Exit Interviews
|
| 480 |
+
All employees separating from MartechSol must complete an exit interview with their immediate
|
| 481 |
+
supervisor. An exit interview gives the Human Resource Department the opportunity to obtain feedback
|
| 482 |
+
from employees as they transition out of their positions.
|
| 483 |
+
Final Settlement
|
| 484 |
+
All employees must undergo a final settlement before they separate from the organization. All
|
| 485 |
+
adjustments will be made according to below mentioned procedures:
|
| 486 |
+
Bef
|
| 487 |
+
ore Salary is Processed:
|
| 488 |
+
If any employee resigns before the salary is processed of any given month, their adjustments
|
| 489 |
+
(loans, advances, pro-rata leaves, etc.) will be made in the same month and the salary will be
|
| 490 |
+
deposited along with the rest of the employees. The remaining amount of the following month’s
|
| 491 |
+
salary will be processed as per the payroll process. No adjustments will be made in the following
|
| 492 |
+
salary except for early exit. For example, someone resigns on the 20th of April and his last working
|
| 493 |
+
day is the 20th of May, the adjustments will be made in the month of April’s salary. The salary for
|
| 494 |
+
the remaining days of the notice period to be served in May will be processed with May’s payroll
|
| 495 |
+
with Early Exit deduction of 11 days (21st May-31st May).
|
| 496 |
+
After/At the time Salary is Processed:
|
| 497 |
+
If any employee resigns once the salary is processed of any given month, their adjustments (loans,
|
| 498 |
+
advances, pro-rata leaves, etc.) will be made in the following month and the salary will be
|
| 499 |
+
deposited along with the rest of the employees. For example, someone resigns on 25th of April and
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
his last working day is on the 25th of May, the adjustments will be made in the month of May’s
|
| 503 |
+
salary.
|
| 504 |
+
Return of Property
|
| 505 |
+
Employees who separate from MartechSol service must return all issued property such as identification
|
| 506 |
+
cards, health insurance card, company maintained vehicle, and intellectual property of the organization.
|
| 507 |
+
Acceptability
|
| 508 |
+
Any changes in Policies, Rules, and Code of Conduct will first be shared through the MartechSol Portal.
|
| 509 |
+
A reasonable period of time will be allotted to discuss suggestions and amendments in changes before
|
| 510 |
+
its implementation.
|
| 511 |
+
Timing In to work after date of implementation implies that you understand and will abide by
|
| 512 |
+
organization policy.
|
| 513 |
+
Work
|
| 514 |
+
Rules
|
| 515 |
+
Any work performed within the premises of MartechSol is under the Company’s ownership and/or its
|
| 516 |
+
clients. Employees cannot claim ownership of any work done in or for MartechSol, neither can they use
|
| 517 |
+
it in the future.
|
| 518 |
+
All work performed at MartechSol has to be done in a legal and ethical manner so there is never
|
| 519 |
+
any copyright violation.
|
| 520 |
+
Employees shall perform their assignments within the specifics of the position description. Employees
|
| 521 |
+
who consistently fail to conform to the specifics of their position description or exhibit inappropriate
|
| 522 |
+
behavior or poor performance shall be required to meet with their supervisor.
|
| 523 |
+
This meeting will attempt to identify the problems, find ways to improve the situation and suggest
|
| 524 |
+
adequate solutions, concluding with a recommended course of action and an appropriate time frame
|
| 525 |
+
in which the employee will be expected to improve to the satisfaction of MartechSol. Details of the
|
| 526 |
+
meeting will be documented, signed by all parties as a correct representation of points discussed and
|
| 527 |
+
placed in the employee’s personal file.
|
| 528 |
+
Disciplinary action and corrective measures are taken at the discretion of the Management at
|
| 529 |
+
MartechSol. The possible steps for disciplinary action are:
|
| 530 |
+
1. Co
|
| 531 |
+
unseling/Verbal Warning
|
| 532 |
+
2. One or more formal written warnings through email
|
| 533 |
+
3. Issuance of Notice
|
| 534 |
+
4. Suspension
|
| 535 |
+
5. Dismissal
|
| 536 |
+
The choice of options depends on the seriousness of the behavior. Exceptions or deviations from the
|
| 537 |
+
progressive discipline sequence may occur whenever the Supervising Authority, in conjunction with the
|
| 538 |
+
President/Director, deems that circumstances warrant.
|
| 539 |
+
For level 1 offense, discussions between the employee and his or her supervisor will occur to allow the
|
| 540 |
+
employee to correct the situation. When a warning notice is issued, it becomes a part of an employee's
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
record and is considered when evaluating an employee for promotion, transfer, training or additional
|
| 544 |
+
discipline. Various offenses have been divided into different levels according to severity.
|
| 545 |
+
Three warning notices within twelve (12) months’ time, regardless of the type of level offense, will result
|
| 546 |
+
in discharge.
|
| 547 |
+
Level 1 Actions
|
| 548 |
+
These actions are taken for behaviors such as:
|
| 549 |
+
Unautho
|
| 550 |
+
rized or excessive absence, tardiness or early timing out
|
| 551 |
+
Unauthorized time away from work station
|
| 552 |
+
Slowing or interfering with the work of other employees
|
| 553 |
+
Failure to notify supervisor of incompletion of assigned work before leaving
|
| 554 |
+
Obscene, abusive, harassing, or disruptive language or behavior
|
| 555 |
+
Failure to perform assigned job responsibilities
|
| 556 |
+
Failure to follow prescribed work procedures
|
| 557 |
+
Failure to notify supervisor of absences
|
| 558 |
+
Neglect of organization property
|
| 559 |
+
Excessive personal use of cell telephone and/or email
|
| 560 |
+
Work shift not completed
|
| 561 |
+
Procedure For Dealing With Level 1 Behavior: These procedures are at the discretion of the Supervising
|
| 562 |
+
Authority.
|
| 563 |
+
Counseling/Verbal warning
|
| 564 |
+
Formal written warning in the form of email
|
| 565 |
+
Issuance of notice
|
| 566 |
+
Suspension
|
| 567 |
+
Level 2 Actions
|
| 568 |
+
These are more serious and must be dealt with firmly and immediately. Typical behaviors in this level
|
| 569 |
+
include:
|
| 570 |
+
Reoccurrin
|
| 571 |
+
g tardiness without reasonable explanation
|
| 572 |
+
Absences without approved leave
|
| 573 |
+
Refusal to comply with instructions of a supervisor
|
| 574 |
+
Conduct endangering the safety of the employee, co-workers or members
|
| 575 |
+
Working when ability is impaired by the use of alcohol, and/or illegal drugs
|
| 576 |
+
Unscheduled leaving from the workplace without informing supervisor
|
| 577 |
+
Habitual sleeping during work hours
|
| 578 |
+
Unauthorized use of organization materials and supplies
|
| 579 |
+
Fighting or threatening violence in the workplace
|
| 580 |
+
Unauthorized possession of weapons on organization property
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
Knowingly timing in another employee
|
| 584 |
+
Sharing portal login info
|
| 585 |
+
Inappropriate usage of internet, or any usage of internet unrelated to work during work hours
|
| 586 |
+
Harassing or bullying coworkers
|
| 587 |
+
Procedure for dealing with Level 2 behavior: These procedures are at the discretion of the Supervising
|
| 588 |
+
Authority.
|
| 589 |
+
Issuance of Notice
|
| 590 |
+
Suspension or probation
|
| 591 |
+
Termination
|
| 592 |
+
Level 3 Actions
|
| 593 |
+
These are b
|
| 594 |
+
ehaviors that are serious enough to justify either a suspension or, in extreme situations,
|
| 595 |
+
termination of employment without following the preceding disciplinary steps. Behaviors for which
|
| 596 |
+
immediate termination can be justified include, but are not limited to, the following:
|
| 597 |
+
Sexual harassment
|
| 598 |
+
Involvement in any income-generating activity without the consent of supervising authority
|
| 599 |
+
Insubordination, or the refusal to comply with the specific instructions of a supervisor in the
|
| 600 |
+
context of an assigned job duty
|
| 601 |
+
Falsification of personnel records, time records, or any other organization documents and records
|
| 602 |
+
Fighting during work time or on work premises
|
| 603 |
+
Use of, or possession of, alcohol or illegal drugs during work time or on work property
|
| 604 |
+
Damaging, defacing, or misusing organization property or the property of coworkers
|
| 605 |
+
Theft, misappropriation, embezzlement, unauthorized possession or removal of organization
|
| 606 |
+
property or the property of employees or customers
|
| 607 |
+
Immoral or indecent conduct which occurs on organization property
|
| 608 |
+
Possession of explosives, firearms, or other dangerous weapons on work premises
|
| 609 |
+
Failure to report an absence for a three-day period without a satisfactory explanation
|
| 610 |
+
Unauthorized release of work-related data or information
|
| 611 |
+
Continued unsatisfactory job performance
|
| 612 |
+
Violation of the organization's conflict of interest/ethical standards
|
| 613 |
+
Data theft: Stealing any digital or other type of data (e.g. email, company records, client records)
|
| 614 |
+
which belongs to MartechSol.
|
| 615 |
+
Copyright violation: Showing ownership of specific work performed at MartechSol. Discussion
|
| 616 |
+
of clients, websites, revenue generated, payments, or any work-related details outside the
|
| 617 |
+
Company’s premises.
|
| 618 |
+
Ot
|
| 619 |
+
her behaviors that, in the opinion of the Supervising Authority, seriously threaten the well-
|
| 620 |
+
being of co-workers.
|
| 621 |
+
In case of any ambiguity, please contact your supervising authority.
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
Procedure for dealing with Level 3 behavior: These procedures are at the discretion of the Supervising
|
| 625 |
+
Authority.
|
| 626 |
+
Suspension
|
| 627 |
+
Termination
|
| 628 |
+
Disciplinary Procedures
|
| 629 |
+
MartechSol may use progressive discipline as a means of enforcing minor violations of the work rules.
|
| 630 |
+
Progressive discipline may include steps such as counseling, written warning, and suspension.
|
| 631 |
+
Depending on the nature of the misconduct and the position involved, steps in the process may be
|
| 632 |
+
omitted or employment may be immediately terminated. The decision whether to apply progressive
|
| 633 |
+
discipline in any particular situation will rest within the sole discretion of management.
|
| 634 |
+
Examples of misconduct which will usually result in immediate discharge include theft, fighting or
|
| 635 |
+
physical assault, insubordination, dishonesty, falsification of time cards, production records, or other
|
| 636 |
+
Company documents, possession of alcohol or illegal drugs in the workplace, and disloyalty or breach of
|
| 637 |
+
confidentiality. This list is not intended to limit our right to discharge in other situations when we believe
|
| 638 |
+
termination of employment is appropriate.
|
| 639 |
+
Employee Benefits
|
| 640 |
+
Referral Program
|
| 641 |
+
Any employ
|
| 642 |
+
ee can benefit from the referral program by referring qualified friends or acquaintances.
|
| 643 |
+
The employee who has referred an individual is eligible for a PKR. 5,000 bonus once the new
|
| 644 |
+
candidate joins the organization.
|
| 645 |
+
Game Room
|
| 646 |
+
In order to promote a relaxed environment and provide employees with more forms of
|
| 647 |
+
entertainment, MartechSol has set up a game room in Room 301.
|
| 648 |
+
This game ro
|
| 649 |
+
om is accessible during lunchtime, so the morning shift has access to it from 1:30-2:30 pm
|
| 650 |
+
and the evening/night shift has access from 9:00-10:00pm. The game room can also be used after work.
|
| 651 |
+
Females are to use the game room on Monday, Wednesday, and Friday during lunchtime. Males should
|
| 652 |
+
use the game room on Tuesday, Thursday, and Saturday during lunch hour.
|
| 653 |
+
Holidays and Holiday Pay
|
| 654 |
+
MartechSol observes all Federal holidays as paid holidays. Due to our nature of work, employees
|
| 655 |
+
may be asked to work on specific holidays, according to their availability. They will be awarded
|
| 656 |
+
compensatory leave allowance or compensatory leave (employee’s choice), which they can utilize at
|
| 657 |
+
their convenience.
|
| 658 |
+
To qualify for holiday pay, an employee must have worked his or her regularly scheduled shift on the
|
| 659 |
+
day preceding the holiday and must work his or her regularly scheduled shift on the day following the
|
| 660 |
+
holiday.
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
Pay Period and Paychecks
|
| 664 |
+
For pay c
|
| 665 |
+
alculation purposes, the workweek (or “pay period”) will commence 12:01 a.m. on Sunday
|
| 666 |
+
morning and end at 12:00 p.m. on Saturday evening. Employees will be paid monthly on (payday), for the
|
| 667 |
+
preceding month. If payday falls on a holiday, then paychecks will be provided the day before the
|
| 668 |
+
holiday.
|
| 669 |
+
Employees are expected to immediately report any mistakes in their paychecks. Underpayments will be
|
| 670 |
+
corrected as soon as possible. If it is not practical to issue an employee a new paycheck to correct any
|
| 671 |
+
overpayment, the overpayment will be deducted from the employee's next paycheck.
|
| 672 |
+
Salary deduction for days off will be on “per-day” basis. {(Gross Salary x 12) / 365}.
|
| 673 |
+
Payroll for June and December
|
| 674 |
+
The salary for June and December will be processed and transferred during the first week of July and
|
| 675 |
+
January respectively. This is to ensure that the transition from the current to the revised salary is smooth
|
| 676 |
+
and all adjustments for the preceding month are processed correctly.
|
| 677 |
+
Leaves
|
| 678 |
+
Prior to Confirmation:
|
| 679 |
+
No leaves are allowed prior to your confirmation.
|
| 680 |
+
In case you skip a day, you will be marked absent, and your salary will be deducted for the day.
|
| 681 |
+
Prior approval from your supervising authority is required for situations where you would be
|
| 682 |
+
unable to come into work.
|
| 683 |
+
If you are absent from work as a result of sickness or any other unforeseeable personal matter,
|
| 684 |
+
you are required to notify your supervisory authority or the HR department by telephone or SMS
|
| 685 |
+
within two hours of your scheduled shift starting time.
|
| 686 |
+
After Confirmation:
|
| 687 |
+
Sick Leaves
|
| 688 |
+
Definition:
|
| 689 |
+
Sick Leave is defined as the period of time an employee is absent from work with full pay as a
|
| 690 |
+
result of an illness or injury.
|
| 691 |
+
Policy:
|
| 692 |
+
All full time confirmed employees are entitled for a maximum of 8 days of sick leave per annum.
|
| 693 |
+
Sick leaves cannot be carried forward to the next year and neither can they be encashed.
|
| 694 |
+
Each year on the 1st of January, your leave account will be credited with the sick leave
|
| 695 |
+
entitlement.
|
| 696 |
+
If anyone is confirmed in MartechSol during the year, then his/her sick leave entitlement will be
|
| 697 |
+
on a pro-rata basis.
|
| 698 |
+
In case of separation from the company during the year, sick leave availed will be adjusted on
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
pro- rata basis from your final salary payment.
|
| 702 |
+
If an employee takes sick leave from work starting any day of the week, any weekend or holiday
|
| 703 |
+
that falls in between will be counted as a full day sick leave.
|
| 704 |
+
Sick leave availed in excess of entitlement will be treated as casual or annual leave (if available
|
| 705 |
+
and subject to approval from reporting authority); incase of insufficient casual/annual leave
|
| 706 |
+
balance, excess sick leave will be treated as unpaid leave.
|
| 707 |
+
During notice period, no sick leave will be entertained even if the leave shows credit balance.
|
| 708 |
+
Process and Documentation:
|
| 709 |
+
Planned appointments, surgery, or any medical instance which can be deemed as anticipated is
|
| 710 |
+
subject to prior approval from your supervising authority.
|
| 711 |
+
If you are absent from work as a result of sickness or injury, you are required to notify your
|
| 712 |
+
supervisory authority or HR department by telephone or SMS within two hours of your
|
| 713 |
+
scheduled shift starting time (or before if possible). Failure to comply with the process may result
|
| 714 |
+
in absent without leave.
|
| 715 |
+
You must apply for sick leave immediately after returning from your sick leave by submitting
|
| 716 |
+
your leave application to the HR department after approval from your supervising authority.
|
| 717 |
+
The submission of a medical certificate or relevant documentation when applying for a sick leave
|
| 718 |
+
is mandatory, the leave will be considered absent without pay if proper documentation is not
|
| 719 |
+
submitted.
|
| 720 |
+
Sick leave can only be availed when sick and cannot be claimed for other purposes. Falsifying
|
| 721 |
+
information or using sick leave for a purpose other than illness or injury will make you liable for
|
| 722 |
+
disciplinary action.
|
| 723 |
+
Casual Leaves
|
| 724 |
+
Definition:
|
| 725 |
+
Casual leaves may be availed by employees in cases of urgent or unforeseen contingencies.
|
| 726 |
+
These cases include, but are not limited to, situations which may arise on account of unforeseen
|
| 727 |
+
circumstances e.g. birth/illness/death in the immediate family or of a close relative.
|
| 728 |
+
Any personal reason that prevents you from coming into work can be used for your casual leave.
|
| 729 |
+
Policy:
|
| 730 |
+
All full time confirmed employees are entitled for a maximum of 10 days casual leave per annum.
|
| 731 |
+
Casual leaves cannot be carried forward to the next year.
|
| 732 |
+
Casual leaves that have not been exhausted over the calendar year will be subject to encashment
|
| 733 |
+
with the paycheck of the following month.
|
| 734 |
+
Each year on the 1st of January, your leave account will be credited with the casual leave
|
| 735 |
+
entitlement if eligible.
|
| 736 |
+
If anyone is confirmed in MartechSol during the year, then his/her casual leave entitlement will
|
| 737 |
+
be on a pro-rata basis.
|
| 738 |
+
In case of separation from the company during the year, casual leave availed will be adjusted on
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
pro-rata basis from your final salary payment.
|
| 742 |
+
If an employee takes casual leave from work starting any day of the week, any weekend or
|
| 743 |
+
holiday that falls in between will be counted as a full day casual leave.
|
| 744 |
+
Casual leave cannot be taken for 3 consecutive days at any given time. If leave availed is for or in
|
| 745 |
+
excess of 3 days, the entire period will be treated as unpaid leave. However, if circumstances
|
| 746 |
+
warrant, the department head may approve the first 3 days to be treated as casual leave, and
|
| 747 |
+
only the additional days to be treated as annual leaves (if available).
|
| 748 |
+
Employees can take up to 2 consecutive casual leaves. If the number of casual leaves is 3 or
|
| 749 |
+
more, the whole duration will be treated as unpaid automatically unless approved by the line
|
| 750 |
+
manager.
|
| 751 |
+
In case of any urgent requirement, your reporting authority may ask you to report to work during
|
| 752 |
+
your casual leave.
|
| 753 |
+
Combining casual leave with a weekly holiday or a public holiday shall be inadmissible and that
|
| 754 |
+
additional leave shall be adjusted as leave without pay. For example, if a public holiday falls on
|
| 755 |
+
Friday and any employee takes leave on Saturday, all three days will be counted as casual leave.
|
| 756 |
+
During notice period, no casual leave will be allowed even if the leave shows credit balance.
|
| 757 |
+
Documentation and Process:
|
| 758 |
+
If you are absent from work as a result of any urgent personal responsibilities or any
|
| 759 |
+
unforeseeable circumstances, you are required to notify your supervisory authority or HR
|
| 760 |
+
department by telephone or SMS within two hours of your scheduled shift starting time (or
|
| 761 |
+
before if possible). Failure to comply with the process may result in absent without leave.
|
| 762 |
+
You must apply for casual leave immediately after returning from your casual leave by submitting
|
| 763 |
+
your leave application to the HR department after approval from your supervising authority.
|
| 764 |
+
In cases of planned or known circumstances, an employee must apply for casual leave to the
|
| 765 |
+
immediate supervisor for approval in advance.
|
| 766 |
+
Annual Leaves
|
| 767 |
+
Definition:
|
| 768 |
+
The purpose of annual leave is to provide employees with a reasonable period of rest and
|
| 769 |
+
a significant break from the workplace in order to maintain a healthy work life balance.
|
| 770 |
+
Policy:
|
| 771 |
+
All Full-time confirmed employee(s) shall be entitled to 14 days paid annual leave after
|
| 772 |
+
completion of one year of service.
|
| 773 |
+
Annuals leaves are not subject to encashment, except for TL roles or greater and for the Sales &
|
| 774 |
+
Support Department.
|
| 775 |
+
Up to 7 annual leaves can be carried forward to the next year, if approved by supervising
|
| 776 |
+
authority, for the following reasons:
|
| 777 |
+
Marriage
|
| 778 |
+
Obligation outside the country (Hajj, Residence Visa Process, etc.)
|
| 779 |
+
Or for any planned reason that warrants extended leave.
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
Carried forward annual leaves can’t be carried forward for more than a year.
|
| 783 |
+
If any employee completes one year of service at MartechSol during the calendar year, then his/
|
| 784 |
+
her annual leave entitlement will be on a pro-rata basis for that year.
|
| 785 |
+
In case of separation from the company during the year, annual leave availed will be adjusted on
|
| 786 |
+
pro-rata basis from your final salary payment.
|
| 787 |
+
Annual leave may be availed at one time during the year or may be divided and availed at various
|
| 788 |
+
times during the year, subject to approval.
|
| 789 |
+
Annual leaves will be granted in keeping with the operational exigencies of the organization, and
|
| 790 |
+
will be scheduled by the supervisor to meet the requirements of individual employees as far as
|
| 791 |
+
possible.
|
| 792 |
+
Management reserves the right to refuse a leave request, allow a partial request, revoke
|
| 793 |
+
approval if already granted, and/or to recall an employee before expiry of the leave period.
|
| 794 |
+
If you are hospitalized during your annual leave and produce a valid medical certificate stating
|
| 795 |
+
the fact, then these days will be adjusted as sick leave (if available).
|
| 796 |
+
If an employee takes annual leave from work starting any day of the week, any weekend or
|
| 797 |
+
holiday that falls in between will be counted as an annual leave.
|
| 798 |
+
During notice period, no annual leave will be allowed even if the leave shows credit balance.
|
| 799 |
+
Documentation and Process:
|
| 800 |
+
A completed leave application form is to be submitted for approval in advance to the HR
|
| 801 |
+
department prior to the anticipated start of the leave accordingly:
|
| 802 |
+
Number of leaves Approval time required
|
| 803 |
+
1-3 days Before 2 days
|
| 804 |
+
4-7 days Before 1 week
|
| 805 |
+
More than 7 days Before 1 month
|
| 806 |
+
An employee who wishes to obtain annual leaves shall apply in writing to HR in advance before
|
| 807 |
+
the commencement of leaves.
|
| 808 |
+
If the leave request is refused or postponed on account of company exigencies, the fact of such
|
| 809 |
+
refusal or postponement and reasons thereto shall be conveyed to the employee concerned.
|
| 810 |
+
If the requested leave is granted the employee will proceed on his leave on the approved date.
|
| 811 |
+
If an employee remains absent beyond the approved period of leave, he shall be liable for
|
| 812 |
+
disciplinary action unless the employee has justifiable reasons for absenteeism.
|
| 813 |
+
Annual leaves may only be availed after prior approval has been granted, however in case of an
|
| 814 |
+
emergency, annual leaves may be entertained subject to your supervising authority’s discretion.
|
| 815 |
+
Hajj Leave
|
| 816 |
+
Definition:
|
| 817 |
+
Employees who wish to make their pilgrimage to Mecca during Dhu'l Hijja may do so during
|
| 818 |
+
their tenure at MartechSol. These leaves are separate from the employee's annual leaves or any
|
| 819 |
+
other leave which he/she is entitled to.
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
Policy:
|
| 823 |
+
All full time permanent employees with 4 years of service are entitled for paid Hajj Leave for 10
|
| 824 |
+
days.
|
| 825 |
+
Any leaves beyond 10 days needs to come from your annual or casual leaves.
|
| 826 |
+
Hajj leave will be granted only once during the whole employment period.
|
| 827 |
+
|
| 828 |
+
Documentation and Process:
|
| 829 |
+
Your leave application must be submitted three months in advance for the necessary approval.
|
| 830 |
+
Copy of your Hajj Passport/Visa needs to be submitted to HR.
|
| 831 |
+
Maternity Leave
|
| 832 |
+
Definition:
|
| 833 |
+
A period of approved absence for female employees that wish to continue employment
|
| 834 |
+
at MartechSol after delivery, granted for the purpose of giving birth and taking care of
|
| 835 |
+
newborn children.
|
| 836 |
+
Policy:
|
| 837 |
+
All full-time female employees shall be entitled to 90 days paid maternity leave, taken no later
|
| 838 |
+
than two weeks before the expected delivery date and is only applicable after completion of one
|
| 839 |
+
year of service.
|
| 840 |
+
This entitlement shall be available only twice during the entire period of service.
|
| 841 |
+
Premature birth shall be considered as maternity leave. In case of still birth, female employee
|
| 842 |
+
shall be entitled to 30 days paid maternity leave.
|
| 843 |
+
Documentation and Process:
|
| 844 |
+
Maternity Leave form needs to be submitted and approved five months before planned
|
| 845 |
+
maternity leave.
|
| 846 |
+
|
| 847 |
+
Paternity Leave
|
| 848 |
+
Definition:
|
| 849 |
+
A period of approved absence for male employees granted after or shortly before the birth of his
|
| 850 |
+
child.
|
| 851 |
+
Policy:
|
| 852 |
+
All full-time male employees are entitled to 3 days paid paternity leave.
|
| 853 |
+
This entitlement shall be available only twice during the entire period of service.
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
Male employees may combine other leaves with paternity leave subject to approval from
|
| 858 |
+
supervising authority.
|
| 859 |
+
Premature birth shall be considered as paternity leave. In case of still birth, male employee shall
|
| 860 |
+
be entitled to up to 3 days paid paternity leave.
|
| 861 |
+
Documentation and Process:
|
| 862 |
+
Application for paternity leave should be submitted to HR department at least two months in
|
| 863 |
+
advance supported by the gynecologist’s certification.
|
| 864 |
+
Bereavement Leave
|
| 865 |
+
Definition:
|
| 866 |
+
Bereavement leaves are paid leaves for employees that cover absences related to the death of
|
| 867 |
+
immediate family members.
|
| 868 |
+
Policy:
|
| 869 |
+
All full-time employee(s) shall be entitled to 3 days paid bereavement leave.
|
| 870 |
+
Bereavement leave will normally be granted unless there are unusual business needs or staffing
|
| 871 |
+
requirements. An employee may, with his or her supervisor’s approval, use any available leaves
|
| 872 |
+
for additional time off as necessary.
|
| 873 |
+
Documentation and Process:
|
| 874 |
+
You must apply for bereavement leave immediately after returning from your leave by
|
| 875 |
+
submitting your leave application to the HR department after approval from your supervising
|
| 876 |
+
authority.
|
| 877 |
+
Unauthorized Leaves
|
| 878 |
+
Any
|
| 879 |
+
absence without prior approval from your reporting authority will be considered as an
|
| 880 |
+
unauthorized leave.
|
| 881 |
+
All unauthorized and/or unreported absences shall be considered Absences without Leave,
|
| 882 |
+
(AWOL). Each unauthorized leave will result in a disciplinary notice and will be added in your file.
|
| 883 |
+
3 unauthorized leaves in a quarter will result in a severe disciplinary action.
|
| 884 |
+
In case you were not able to inform your reporting authority due to some unavoidable reasons,
|
| 885 |
+
you may discuss it with your reporting authority later. Based on discretion, he/she may authorize
|
| 886 |
+
your leave.
|
| 887 |
+
For employees serving their probationary period, failure to communicate with the respective
|
| 888 |
+
authority for more than 3 consecutive days of leave will result in termination.
|
| 889 |
+
For permanent employees, failure to communicate with the respective authority for more than 7
|
| 890 |
+
consecutive days of leave will result in termination.
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
Unapproved Absence Without Pay
|
| 894 |
+
Definition:
|
| 895 |
+
Any leave, in excess of the 10 Casual leaves that you are entitled to, is an Absence without Pay.
|
| 896 |
+
These leaves, when taken without prior approval from your supervising authority are considered
|
| 897 |
+
Unapproved Absence without Pay.
|
| 898 |
+
Policy:
|
| 899 |
+
|
| 900 |
+
Unapproved Absence without Pay will result in salary deduction(s) per day absent.
|
| 901 |
+
Taking more than two consecutive Absences without Pay prior to the weekend will result in salary
|
| 902 |
+
deduction for the weekend. For example, if an employee is absent on Thursday, Friday, and
|
| 903 |
+
Saturday then Sunday will also be marked absent. In other words, employees must report to work
|
| 904 |
+
on the last working day of the week to avoid further deductions.
|
| 905 |
+
Leave Encashment
|
| 906 |
+
Each employee is entitled to a number of Sick, Casual, Annual, and other leaves. However, only Casual
|
| 907 |
+
leaves are subject to encashment for specific departments.
|
| 908 |
+
Listed below is breakdown of Department eligibility of leave encashment:
|
| 909 |
+
Department Casual Annual
|
| 910 |
+
Copywriting Yes No
|
| 911 |
+
SEO Yes No
|
| 912 |
+
Sales Yes Yes
|
| 913 |
+
Software Yes No
|
| 914 |
+
Networking Yes No
|
| 915 |
+
Human Resources Yes No
|
| 916 |
+
Admin Yes No
|
| 917 |
+
Other Benefits
|
| 918 |
+
Loans & Advance Salary
|
| 919 |
+
Definition:
|
| 920 |
+
The purpose of loans and advance salary is to meet an emergency need that can’t be
|
| 921 |
+
accommodated through other financial arrangements. Only genuine emergency needs such as
|
| 922 |
+
extraordinary medical costs not covered by insurance, or any other extreme financial or medical
|
| 923 |
+
emergency will be considered. The decision to approve or reject a loan/advance salary request will
|
| 924 |
+
be taken by a 3-person committee which will comprise of line manager/head of department, a
|
| 925 |
+
representative from HR, and a representative from finance.
|
| 926 |
+
Policy:
|
| 927 |
+
Decisions regarding loan requests will be relayed within 3 to 7 working days. For extremely urgent
|
| 928 |
+
matters, the committee will take a decision within 27 hours of request sent.
|
| 929 |
+
The decision of the committee will be final and non-negotiable.
|
| 930 |
+
A loans/advance salary can only be taken once in 6 months.
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
If an employee has an ongoing loan/advance salary, he/she will not be allowed to avail another
|
| 934 |
+
loan/advance salary.
|
| 935 |
+
Repayment period is up to 6 months.
|
| 936 |
+
Loan amount can’t exceed 3 months’ salary.
|
| 937 |
+
Advance salary amount shall not exceed from your monthly salary.
|
| 938 |
+
Documentation & Process:
|
| 939 |
+
If possible, the loan/advance salary form should be submitted at least a week (7 days) before it is
|
| 940 |
+
required.
|
| 941 |
+
The form can be acquired from HR and must be completed including all the signatures required.
|
| 942 |
+
The necessary documentation & supporting documents should be provided along with the filled
|
| 943 |
+
form.
|
| 944 |
+
Maternity (Pregnancy) Benefit
|
| 945 |
+
Definition:
|
| 946 |
+
This benefit provides financial assistance to female and male employees to cover the cost of childbirth and
|
| 947 |
+
other delivery related expenses.
|
| 948 |
+
Policy:
|
| 949 |
+
Male and female employees that have completed at least one year of service can avail maternity
|
| 950 |
+
benefit of PKR 50,000.
|
| 951 |
+
Maternity benefit can be utilized twice during tenure at MartechSol.
|
| 952 |
+
This benefit also covers stillbirth.
|
| 953 |
+
Documentation & Process:
|
| 954 |
+
Maternity Benefit Form and relevant documentation must be provided at least 60 days before
|
| 955 |
+
expected delivery.
|
| 956 |
+
Employees will be awarded this benefit a week before date of expected delivery.
|
| 957 |
+
In case of premature delivery, employees can either contact Supervising Authority or Admin
|
| 958 |
+
Department to arrange the benefit urgently or they may pay out of pocket at the time and avail the
|
| 959 |
+
benefit when they rejoin the office.
|
| 960 |
+
|
| 961 |
+
|
| 962 |
+
Company Maintained Vehicle
|
| 963 |
+
Definition:
|
| 964 |
+
MartechSol provides company maintained cars to employees at certain designation levels. The
|
| 965 |
+
policy varies across different departments. The company maintained car will be owned and
|
| 966 |
+
maintained by the company for the use of the employee.
|
| 967 |
+
Policy:
|
| 968 |
+
Employees should drive their allotted vehicle in a safe and responsible vehicle. They must abide by
|
| 969 |
+
driving laws.
|
| 970 |
+
Scheduled maintenance will be carried out by the company after every 5,000 KMs or 3 months
|
| 971 |
+
(whichever comes first).
|
| 972 |
+
Employees are requested to keep a copy of the following documents in the company vehicle:
|
| 973 |
+
o Authority Letter
|
| 974 |
+
o Motor Vehicle Tax paper
|
| 975 |
+
o Insurance paper
|
| 976 |
+
o Copy of the first page of the vehicle’s registration book
|
| 977 |
+
Documentation & Process:
|
| 978 |
+
Prior to vehicle allocation, employee must provide a copy of valid driving license of self (or driver)
|
| 979 |
+
for company record.
|
| 980 |
+
All maintenance and wear & tear requests need to be made through submitting a ticket on portal or
|
| 981 |
+
by contacting Admin Department.
|
| 982 |
+
In case of an accident:
|
| 983 |
+
o All accidents must be reported to the Admin Department.
|
| 984 |
+
o If the company vehicle is stolen, report the theft immediately to the local police and to the
|
| 985 |
+
Admin Department. Obtain a copy of the police report filed.
|
| 986 |
+
Healthcare Insurance – For Self & Spouse
|
| 987 |
+
Health care benefits are provided to all full-time employees and their spouse. Complete details of this
|
| 988 |
+
program are provided below:
|
| 989 |
+
“ANNEXURE A”
|
| 990 |
+
PROCEDURE FOR UTILIZATION OF HOSPITALIZATION EXPENSE BENEFIT
|
| 991 |
+
Emergency Hospitalization
|
| 992 |
+
In case of emergency, approach the nearest hospital. In case it is a PANEL HOSPITAL
|
| 993 |
+
1. Identify yourself as an NJI insured.
|
| 994 |
+
2. Produce Health Card / Letter of Authority
|
| 995 |
+
3. Get the treatment
|
| 996 |
+
4. Get the treatment on credit, up to the limits available.
|
| 997 |
+
5. Pay only the amount that exceeds the entitlement, if any, before discharge.
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
In case it is a NON-PANEL HOSPITAL
|
| 1001 |
+
1. Inform NJI within 24 hours of the hospitalization.
|
| 1002 |
+
2. Pay cash for the treatment.
|
| 1003 |
+
3. Submit all original bills /supporting documents* with our claim form for reimbursement,
|
| 1004 |
+
within 30 days of discharge from the hospital.
|
| 1005 |
+
4. Settlement of claim will be done in line of the policy terms.
|
| 1006 |
+
“ANNEXURE B”
|
| 1007 |
+
PROCEDURE FOR UTILIZATION OF HOSPITALIZATION EXPENSE BENEFIT
|
| 1008 |
+
Emergency Hospitalization
|
| 1009 |
+
If hospitalization is advised by a licensed physician
|
| 1010 |
+
In case it is a PANEL HOSPITAL
|
| 1011 |
+
1. Approach any of our panel hospital with our Letter of Authority / Health Card
|
| 1012 |
+
2. Identify yourself as an NJI insured
|
| 1013 |
+
3. Get the treatment on credit, up to the limits available.
|
| 1014 |
+
4. Pay only the amount that exceeds the entitlement, if any, before discharge.
|
| 1015 |
+
In case it is a NON-PANEL HOSPITAL
|
| 1016 |
+
1. Send us the cost estimate from the concerned doctor of the treatment with details, for prior
|
| 1017 |
+
approval.
|
| 1018 |
+
2. Get the treatment and pay cash for the treatment.
|
| 1019 |
+
3. Submit all original bills /supporting documents* including our approval letter with our claim
|
| 1020 |
+
form for reimbursement, within 30 days of discharge from the hospital.
|
| 1021 |
+
4. Settlement of claim will be done in line of prior approval given and other policy terms.
|
| 1022 |
+
*Supporting Documents:
|
| 1023 |
+
Duly completed original NJI Claim Form
|
| 1024 |
+
Original itemized bill/invoice (breakup of charged) on hospital
|
| 1025 |
+
bill-book
|
| 1026 |
+
Discharge card / clinical summary
|
| 1027 |
+
Copy of NJI card or letter of admission
|
| 1028 |
+
Diagnostic reports
|
| 1029 |
+
Doctors prescriptions
|
| 1030 |
+
Original pharmacy vouchers
|
| 1031 |
+
Original payment receipts
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
JUBILEE GENERAL INSURANCE COMPANY LTD
|
| 1035 |
+
GROUP HEALTHCARE INSURANCE PROPOSAL FOR
|
| 1036 |
+
MartechSol
|
| 1037 |
+
Hospitalization & Related Benefits
|
| 1038 |
+
Plan A Plan B Plan C Plan D
|
| 1039 |
+
H&R Limits (Per Person / Per Year) Rs.500,000 Rs.400,000 Rs.300,000 Rs.150,000
|
| 1040 |
+
Room & Board (per day) Rs.16,360 Rs.6,720 Rs.6,720 Rs.1,800
|
| 1041 |
+
Per Hospitalization
|
| 1042 |
+
Pre-Hospitalization Sub Limit (Diagnosis, 30 Days 30 Days 30 Days 30 Days
|
| 1043 |
+
Consultation, & Medicines)
|
| 1044 |
+
Post-Hospitalization Sub Limit (Follow-Ups) 30 Days 30 Days 30 Days 30 Days
|
| 1045 |
+
Daycare Surgeries & Specialized
|
| 1046 |
+
Investigations In Outpatient Settings
|
| 1047 |
+
Including but not limited to:
|
| 1048 |
+
COVERED
|
| 1049 |
+
Dialysis, Cataract Surgery, MRI, CT Scan,
|
| 1050 |
+
Endoscopy, Thallium Scan, Angiography, Treatment
|
| 1051 |
+
of Fractures, Local Road Ambulance for
|
| 1052 |
+
Emergencies only, Emergency Dental Treatment
|
| 1053 |
+
due to accidental injuries within 48 hours (for pain
|
| 1054 |
+
relief only).
|
| 1055 |
+
Plan A Presidential Layer
|
| 1056 |
+
Plan B Associate Managers, Managers & Senior Managers
|
| 1057 |
+
Plan C Executives, Senior Executives & Assistant Managers
|
| 1058 |
+
Plan D Office Boys & Other Support Staff
|
| 1059 |
+
Send us the cost estimate from the concerned doctor of the treatment with details, for prior
|
| 1060 |
+
approval. Get the treatment and pay cash for the treatment.
|
| 1061 |
+
Submit all original bills /supporting documents* including our approval letter with our claim form
|
| 1062 |
+
for reimbursement, within 30 days of discharge from the hospital.
|
| 1063 |
+
Settlement of claim will be done in line of prior approval given and other policy terms.
|
| 1064 |
+
Supporting documents
|
| 1065 |
+
Itemized hospital bill
|
| 1066 |
+
Discharge card/clinical summary
|
| 1067 |
+
Diagnostic reports
|
| 1068 |
+
Prescriptions
|
| 1069 |
+
Payment receipts
|
| 1070 |
+
|
| 1071 |
+
|
| 1072 |
+
EOBI
|
| 1073 |
+
Employee Old Age Benefit Income is a benefit provided to all full-time employees upon confirmation.
|
| 1074 |
+
The sole purpose of EOBI is to provide compulsory social insurance to employees. It extends following
|
| 1075 |
+
benefits to insured persons or their survivors:
|
| 1076 |
+
• Old-Age Pension
|
| 1077 |
+
• Survivor's Pension
|
| 1078 |
+
• Invalidity Pension
|
| 1079 |
+
• Old-Age Grant
|
| 1080 |
+
RATES:
|
| 1081 |
+
• 1- Employer's Contribution @ 5 % of the worker's minimum wages (i.e. Rs. 8000) - Rs. 400/= Per
|
| 1082 |
+
Month
|
| 1083 |
+
• 2- Employee's Contribution @ 1 % of the worker's minimum wages (i.e. Rs.8000) - Rs. 80/= Per
|
| 1084 |
+
Month
|
| 1085 |
+
Provident Fund
|
| 1086 |
+
Provident Fund is an investment/ saving fund contributed to - by employees & employers, out of which a
|
| 1087 |
+
lump sum amount is provided to each employee on retirement. This benefit is provided to all full time,
|
| 1088 |
+
confirmed employees.
|
| 1089 |
+
Click Here To View PF Policies in Detail
|
| 1090 |
+
|
| 1091 |
+
|
floaitng-icon/KhloeSAC.lottie
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:532a144eeea5a1a6e49ab22ae4c0dc4f94e12cf8540e3514f1bc676d929eeac2
|
| 3 |
+
size 462684
|
martech_sol_logo.jpg
ADDED
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
faiss-cpu
|
| 4 |
+
sentence-transformers
|
| 5 |
+
pydantic-settings
|
| 6 |
+
pypdf
|
| 7 |
+
python-dotenv
|
| 8 |
+
numpy
|
| 9 |
+
httpx
|
| 10 |
+
gradio
|
| 11 |
+
python-multipart
|
| 12 |
+
pandas
|
| 13 |
+
openpyxl
|
robocopy_backup.log
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 0 |
99%
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
99%
|
|
|
|
|
|
|
| 2 |
93%
|
|
|
|
| 3 |
99%
|
|
|
|
| 4 |
99%
|
|
|
|
|
|
|
|
|
|
| 5 |
99%
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
-------------------------------------------------------------------------------
|
| 3 |
+
ROBOCOPY :: Robust File Copy for Windows
|
| 4 |
+
-------------------------------------------------------------------------------
|
| 5 |
+
|
| 6 |
+
Started : Wednesday, April 29, 2026 9:00:28 PM
|
| 7 |
+
Source : c:\Users\DELL\Desktop\RAG_backup2\
|
| 8 |
+
Dest : C:\Users\DELL\Desktop\RAG_backup_final\
|
| 9 |
+
|
| 10 |
+
Files : *.*
|
| 11 |
+
|
| 12 |
+
Exc Files : .DS_Store
|
| 13 |
+
|
| 14 |
+
Exc Dirs : .git
|
| 15 |
+
.gemini
|
| 16 |
+
__pycache__
|
| 17 |
+
venv
|
| 18 |
+
|
| 19 |
+
Options : *.* /TEE /S /E /DCOPY:DA /COPY:DAT /PURGE /MIR /R:2 /W:5
|
| 20 |
+
|
| 21 |
+
------------------------------------------------------------------------------
|
| 22 |
+
|
| 23 |
+
13 c:\Users\DELL\Desktop\RAG_backup2\
|
| 24 |
+
*EXTRA File 60 exclude_list.txt
|
| 25 |
+
Newer 378 .env.example
|
| 26 |
+
New File 46 .gitattributes
|
| 27 |
+
Newer 194 app.py
|
| 28 |
+
New File 1198 CONFIG_NOTES.md
|
| 29 |
+
Newer 295 docker-compose.yml
|
| 30 |
+
Newer 782 Dockerfile
|
| 31 |
+
New File 6675 martech_sol_logo.jpg
|
| 32 |
99%
|
| 33 |
+
Newer 161 requirements.txt
|
| 34 |
+
New File 0 robocopy_backup.log
|
| 35 |
+
3 c:\Users\DELL\Desktop\RAG_backup2\app\
|
| 36 |
+
Newer 8109 main.py
|
| 37 |
+
Newer 10861 ui_gradio.py
|
| 38 |
+
2 c:\Users\DELL\Desktop\RAG_backup2\app\admin\
|
| 39 |
+
Newer 14492 router.py
|
| 40 |
+
1 c:\Users\DELL\Desktop\RAG_backup2\app\admin\templates\
|
| 41 |
+
Newer 35075 admin.html
|
| 42 |
+
2 c:\Users\DELL\Desktop\RAG_backup2\app\api\
|
| 43 |
+
Newer 693 schemas.py
|
| 44 |
+
2 c:\Users\DELL\Desktop\RAG_backup2\app\core\
|
| 45 |
+
Newer 2594 config.py
|
| 46 |
+
10 c:\Users\DELL\Desktop\RAG_backup2\app\services\
|
| 47 |
+
Newer 3568 chunker.py
|
| 48 |
+
Newer 8361 llm.py
|
| 49 |
+
Newer 4989 rag_pipeline.py
|
| 50 |
+
New File 1661 reranker.py
|
| 51 |
+
Newer 10783 session_store.py
|
| 52 |
+
Newer 4823 vector_store.py
|
| 53 |
+
0 c:\Users\DELL\Desktop\RAG_backup2\data\
|
| 54 |
+
1 c:\Users\DELL\Desktop\RAG_backup2\data\index\
|
| 55 |
+
1 c:\Users\DELL\Desktop\RAG_backup2\data\sessions\
|
| 56 |
+
2 c:\Users\DELL\Desktop\RAG_backup2\docs\
|
| 57 |
+
1 c:\Users\DELL\Desktop\RAG_backup2\floaitng-icon\
|
| 58 |
+
*EXTRA File 24856 Artificial Intelligence Chatbot.lottie
|
| 59 |
+
New File 462684 KhloeSAC.lottie
|
| 60 |
99%
|
| 61 |
+
New Dir 3 c:\Users\DELL\Desktop\RAG_backup2\PHPMailer\
|
| 62 |
+
New File 1256 Exception.php
|
| 63 |
93%
|
| 64 |
+
New File 183229 PHPMailer.php
|
| 65 |
99%
|
| 66 |
+
New File 49148 SMTP.php
|
| 67 |
99%
|
| 68 |
+
3 c:\Users\DELL\Desktop\RAG_backup2\static\
|
| 69 |
+
Newer 19723 addon.html
|
| 70 |
+
New File 462684 bot-icon.lottie
|
| 71 |
99%
|
| 72 |
+
Newer 2010 chat-loader.js
|
| 73 |
+
3 c:\Users\DELL\Desktop\RAG_backup2\wordpress code\
|
| 74 |
+
|
| 75 |
+
------------------------------------------------------------------------------
|
| 76 |
+
|
| 77 |
+
Total Copied Skipped Mismatch FAILED Extras
|
| 78 |
+
Dirs : 21 1 20 0 0 0
|
| 79 |
+
Files : 47 28 19 0 0 2
|
| 80 |
+
Bytes : 5.00 m 1.23 m 3.76 m 0 0 24.3 k
|
| 81 |
+
Times : 0:00:00 0:00:00 0:00:00 0:00:00
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
Speed : 20,257,375 Bytes/sec.
|
| 85 |
+
Speed : 1,159.136 MegaBytes/min.
|
| 86 |
+
Ended : Wednesday, April 29, 2026 9:00:29 PM
|
| 87 |
+
|
static/addon.html
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0">
|
| 6 |
+
<title>Martechsol Assistant Widget</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* Premium Design for Martechsol Assistant */
|
| 9 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
| 10 |
+
|
| 11 |
+
#martech-chat-wrapper {
|
| 12 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* Floating Action Button (FAB) */
|
| 16 |
+
#martech-chat-fab {
|
| 17 |
+
position: fixed;
|
| 18 |
+
right: 10px;
|
| 19 |
+
bottom: 25px;
|
| 20 |
+
width: auto;
|
| 21 |
+
height: auto;
|
| 22 |
+
background: transparent;
|
| 23 |
+
border-radius: 50%;
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: center;
|
| 26 |
+
justify-content: center;
|
| 27 |
+
cursor: pointer;
|
| 28 |
+
z-index: 999999;
|
| 29 |
+
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
|
| 30 |
+
-webkit-tap-highlight-color: transparent;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#martech-chat-fab:hover {
|
| 34 |
+
transform: scale(1.1);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#martech-chat-fab.is-active {
|
| 38 |
+
background: #1e293b;
|
| 39 |
+
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
|
| 40 |
+
transform: scale(0.8);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Chat Window Styling */
|
| 44 |
+
#martech-chat-box {
|
| 45 |
+
position: fixed;
|
| 46 |
+
right: 25px;
|
| 47 |
+
bottom: 100px;
|
| 48 |
+
width: 400px;
|
| 49 |
+
height: 550px;
|
| 50 |
+
background: #ffffff;
|
| 51 |
+
border-radius: 16px;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: none;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
|
| 56 |
+
z-index: 999998;
|
| 57 |
+
border: 1px solid #e2e8f0;
|
| 58 |
+
opacity: 0;
|
| 59 |
+
transform: translateY(20px);
|
| 60 |
+
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#martech-chat-box.is-visible {
|
| 64 |
+
display: flex;
|
| 65 |
+
opacity: 1;
|
| 66 |
+
transform: translateY(0);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#martech-chat-header {
|
| 70 |
+
background: #ffffff;
|
| 71 |
+
padding: 16px 20px;
|
| 72 |
+
border-bottom: 1px solid #f1f5f9;
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 12px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
@keyframes pulse-dot {
|
| 79 |
+
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
| 80 |
+
70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
| 81 |
+
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
#martech-chat-header .dot {
|
| 85 |
+
width: 10px;
|
| 86 |
+
height: 10px;
|
| 87 |
+
background: #10b981;
|
| 88 |
+
border-radius: 50%;
|
| 89 |
+
animation: pulse-dot 2s infinite;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
#martech-chat-header span {
|
| 93 |
+
font-weight: 600;
|
| 94 |
+
color: #1e293b;
|
| 95 |
+
font-size: 15px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Custom Chat Area Styling */
|
| 99 |
+
#chat-messages {
|
| 100 |
+
flex: 1;
|
| 101 |
+
overflow-y: auto;
|
| 102 |
+
padding: 16px;
|
| 103 |
+
display: flex;
|
| 104 |
+
flex-direction: column;
|
| 105 |
+
gap: 12px;
|
| 106 |
+
background: #f8fafc;
|
| 107 |
+
scroll-behavior: smooth;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Custom Scrollbar for messages */
|
| 111 |
+
#chat-messages::-webkit-scrollbar { width: 6px; }
|
| 112 |
+
#chat-messages::-webkit-scrollbar-track { background: transparent; }
|
| 113 |
+
#chat-messages::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
| 114 |
+
|
| 115 |
+
.chat-message {
|
| 116 |
+
max-width: 85%;
|
| 117 |
+
padding: 12px 16px;
|
| 118 |
+
border-radius: 16px;
|
| 119 |
+
font-size: 14px;
|
| 120 |
+
line-height: 1.5;
|
| 121 |
+
word-wrap: break-word;
|
| 122 |
+
white-space: pre-wrap;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.chat-message.user {
|
| 126 |
+
align-self: flex-end;
|
| 127 |
+
background: #1e293b;
|
| 128 |
+
color: white;
|
| 129 |
+
border-bottom-right-radius: 4px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.chat-message.bot {
|
| 133 |
+
align-self: flex-start;
|
| 134 |
+
background: #ffffff;
|
| 135 |
+
color: #334155;
|
| 136 |
+
border-bottom-left-radius: 4px;
|
| 137 |
+
border: 1px solid #e2e8f0;
|
| 138 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#chat-input-area {
|
| 142 |
+
padding: 16px;
|
| 143 |
+
background: #ffffff;
|
| 144 |
+
border-top: 1px solid #f1f5f9;
|
| 145 |
+
display: flex;
|
| 146 |
+
gap: 8px;
|
| 147 |
+
align-items: center;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
#chat-input {
|
| 151 |
+
flex: 1;
|
| 152 |
+
border: 1px solid #e2e8f0;
|
| 153 |
+
border-radius: 20px;
|
| 154 |
+
padding: 12px 16px;
|
| 155 |
+
font-size: 14px;
|
| 156 |
+
outline: none;
|
| 157 |
+
transition: border-color 0.2s;
|
| 158 |
+
font-family: inherit;
|
| 159 |
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
#chat-input:focus {
|
| 163 |
+
border-color: #10b981;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
#chat-input:disabled {
|
| 167 |
+
background: #f1f5f9;
|
| 168 |
+
cursor: not-allowed;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
#chat-send {
|
| 172 |
+
background: #10b981;
|
| 173 |
+
color: white;
|
| 174 |
+
border: none;
|
| 175 |
+
width: 44px;
|
| 176 |
+
height: 44px;
|
| 177 |
+
border-radius: 50%;
|
| 178 |
+
display: flex;
|
| 179 |
+
align-items: center;
|
| 180 |
+
justify-content: center;
|
| 181 |
+
cursor: pointer;
|
| 182 |
+
transition: background 0.2s, transform 0.1s;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
#chat-send:hover {
|
| 186 |
+
background: #059669;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
#chat-send:active {
|
| 190 |
+
transform: scale(0.95);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
#chat-send:disabled {
|
| 194 |
+
background: #cbd5e1;
|
| 195 |
+
cursor: not-allowed;
|
| 196 |
+
transform: none;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* Loading / Typing Animation */
|
| 200 |
+
.typing-indicator {
|
| 201 |
+
display: flex;
|
| 202 |
+
gap: 4px;
|
| 203 |
+
padding: 4px 2px;
|
| 204 |
+
align-items: center;
|
| 205 |
+
height: 18px;
|
| 206 |
+
}
|
| 207 |
+
.typing-indicator span {
|
| 208 |
+
width: 6px;
|
| 209 |
+
height: 6px;
|
| 210 |
+
background: #94a3b8;
|
| 211 |
+
border-radius: 50%;
|
| 212 |
+
animation: typing-dots 1.4s infinite ease-in-out both;
|
| 213 |
+
}
|
| 214 |
+
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
| 215 |
+
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
| 216 |
+
@keyframes typing-dots {
|
| 217 |
+
0%, 80%, 100% { transform: scale(0); }
|
| 218 |
+
40% { transform: scale(1); }
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
@media (max-width: 480px) {
|
| 222 |
+
#martech-chat-box {
|
| 223 |
+
right: 15px;
|
| 224 |
+
bottom: 100px;
|
| 225 |
+
width: calc(100vw - 30px);
|
| 226 |
+
height: 70vh;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
</style>
|
| 230 |
+
</head>
|
| 231 |
+
<body>
|
| 232 |
+
|
| 233 |
+
<!-- Lottie Player Script -->
|
| 234 |
+
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 235 |
+
|
| 236 |
+
<div id="martech-chat-wrapper">
|
| 237 |
+
<div id="martech-chat-fab">
|
| 238 |
+
<!-- Animated Chatbot Icon -->
|
| 239 |
+
<div id="chat-icon" style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 240 |
+
<dotlottie-player
|
| 241 |
+
src="https://joedown11-chatrag.hf.space/static/bot-icon.lottie"
|
| 242 |
+
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop autoplay>
|
| 243 |
+
</dotlottie-player>
|
| 244 |
+
</div>
|
| 245 |
+
<!-- Close Icon -->
|
| 246 |
+
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 247 |
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="white" />
|
| 248 |
+
</svg>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div id="martech-chat-box">
|
| 252 |
+
<div id="martech-chat-header">
|
| 253 |
+
<div class="dot"></div>
|
| 254 |
+
<span>Martechsol Assistant</span>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<!-- Custom Chat UI replacing iframe -->
|
| 258 |
+
<div id="chat-messages">
|
| 259 |
+
<div class="chat-message bot">Welcome! How can I help you today?</div>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<div id="chat-input-area">
|
| 263 |
+
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
|
| 264 |
+
<button id="chat-send" title="Send">
|
| 265 |
+
<!-- Send Icon -->
|
| 266 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 267 |
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 268 |
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 269 |
+
</svg>
|
| 270 |
+
</button>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<script>
|
| 276 |
+
(function () {
|
| 277 |
+
// UI Elements
|
| 278 |
+
const fab = document.getElementById('martech-chat-fab');
|
| 279 |
+
const box = document.getElementById('martech-chat-box');
|
| 280 |
+
const chatIcon = document.getElementById('chat-icon');
|
| 281 |
+
const closeIcon = document.getElementById('close-icon');
|
| 282 |
+
const messagesContainer = document.getElementById('chat-messages');
|
| 283 |
+
const inputField = document.getElementById('chat-input');
|
| 284 |
+
const sendButton = document.getElementById('chat-send');
|
| 285 |
+
|
| 286 |
+
// FAB Toggle Logic
|
| 287 |
+
fab.addEventListener('click', function () {
|
| 288 |
+
const isVisible = box.classList.contains('is-visible');
|
| 289 |
+
if (isVisible) {
|
| 290 |
+
box.classList.remove('is-visible');
|
| 291 |
+
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 292 |
+
chatIcon.style.display = 'block';
|
| 293 |
+
closeIcon.style.display = 'none';
|
| 294 |
+
fab.classList.remove('is-active');
|
| 295 |
+
} else {
|
| 296 |
+
box.style.display = 'flex';
|
| 297 |
+
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
|
| 298 |
+
chatIcon.style.display = 'none';
|
| 299 |
+
closeIcon.style.display = 'block';
|
| 300 |
+
fab.classList.add('is-active');
|
| 301 |
+
}
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
// Chat Logic
|
| 305 |
+
const API_URL = "https://joedown11-chatrag.hf.space/api/chat";
|
| 306 |
+
const BASE_URL = API_URL.replace("/api/chat", "");
|
| 307 |
+
let chatHistory = [];
|
| 308 |
+
|
| 309 |
+
// ── Session ID: generate once, persist in localStorage ──
|
| 310 |
+
let sessionId = localStorage.getItem('martech_session_id');
|
| 311 |
+
if (!sessionId) {
|
| 312 |
+
// Use a temporary name/id until we get the real one
|
| 313 |
+
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 314 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function appendMessage(role, content) {
|
| 318 |
+
const div = document.createElement('div');
|
| 319 |
+
div.className = `chat-message ${role}`;
|
| 320 |
+
div.textContent = content;
|
| 321 |
+
messagesContainer.appendChild(div);
|
| 322 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 323 |
+
return div;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// ── User Integration & History ──────────────────────────────────────────
|
| 327 |
+
async function initUserSession() {
|
| 328 |
+
// 1. Discover User Name immediately (0.2s)
|
| 329 |
+
let userName = null;
|
| 330 |
+
try {
|
| 331 |
+
const storageKeys = ['userInfo', 'user', 'profile', 'auth', 'currentUser', 'account'];
|
| 332 |
+
for (const key of storageKeys) {
|
| 333 |
+
const stored = localStorage.getItem(key) || sessionStorage.getItem(key);
|
| 334 |
+
if (stored) {
|
| 335 |
+
try {
|
| 336 |
+
const parsed = JSON.parse(stored);
|
| 337 |
+
userName = parsed.name || parsed.fullName || (parsed.user && parsed.user.name) || parsed.display_name;
|
| 338 |
+
if (userName) break;
|
| 339 |
+
} catch(err) {
|
| 340 |
+
if (typeof stored === 'string' && stored.length < 50 && !stored.includes('{')) {
|
| 341 |
+
userName = stored; break;
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
} catch (e) {}
|
| 347 |
+
|
| 348 |
+
if (!userName) {
|
| 349 |
+
try {
|
| 350 |
+
const pResp = await fetch("https://hrmmartechsol-ba71e8dc3fa2.herokuapp.com/api/users/profile", {
|
| 351 |
+
credentials: 'include'
|
| 352 |
+
});
|
| 353 |
+
if (pResp.ok) {
|
| 354 |
+
const pData = await pResp.json();
|
| 355 |
+
userName = pData.name || (pData.personalDetails && pData.personalDetails.fullName);
|
| 356 |
+
}
|
| 357 |
+
} catch (e) {}
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// Multi-user logic: if name changed, clear old session and start fresh
|
| 361 |
+
const lastKnownName = localStorage.getItem('martech_last_user_name');
|
| 362 |
+
if (userName && lastKnownName && userName !== lastKnownName) {
|
| 363 |
+
localStorage.removeItem('martech_session_id');
|
| 364 |
+
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 365 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 366 |
+
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
|
| 367 |
+
}
|
| 368 |
+
if (userName) {
|
| 369 |
+
localStorage.setItem('martech_last_user_name', userName);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// 2. Fetch history for this session
|
| 373 |
+
try {
|
| 374 |
+
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
|
| 375 |
+
if (hResp.ok) {
|
| 376 |
+
const hData = await hResp.json();
|
| 377 |
+
if (hData.history && hData.history.length > 0) {
|
| 378 |
+
messagesContainer.innerHTML = '';
|
| 379 |
+
chatHistory = hData.history;
|
| 380 |
+
chatHistory.forEach(m => {
|
| 381 |
+
appendMessage(m.role === 'assistant' ? 'bot' : 'user', m.content);
|
| 382 |
+
});
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
} catch (e) {}
|
| 386 |
+
|
| 387 |
+
// 3. Sync name with backend
|
| 388 |
+
if (userName) {
|
| 389 |
+
await fetch(BASE_URL + "/api/session/update-name", {
|
| 390 |
+
method: 'POST',
|
| 391 |
+
headers: { 'Content-Type': 'application/json' },
|
| 392 |
+
body: JSON.stringify({ session_id: sessionId, user_name: userName })
|
| 393 |
+
}).then(r => r.json()).then(rData => {
|
| 394 |
+
if (rData.new_session_id && rData.new_session_id !== sessionId) {
|
| 395 |
+
sessionId = rData.new_session_id;
|
| 396 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 397 |
+
}
|
| 398 |
+
}).catch(() => {});
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
initUserSession();
|
| 403 |
+
|
| 404 |
+
function showTypingIndicator() {
|
| 405 |
+
const div = document.createElement('div');
|
| 406 |
+
div.className = 'chat-message bot';
|
| 407 |
+
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
| 408 |
+
messagesContainer.appendChild(div);
|
| 409 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 410 |
+
return div;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
// Typo Configuration for Human-like Typing
|
| 414 |
+
const NEARBY_KEYS = {
|
| 415 |
+
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 416 |
+
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 417 |
+
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 418 |
+
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 419 |
+
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 420 |
+
'z': 'xas',
|
| 421 |
+
};
|
| 422 |
+
const TYPO_CHANCE = 0.07;
|
| 423 |
+
|
| 424 |
+
function getNearbyChar(ch) {
|
| 425 |
+
const lower = ch.toLowerCase();
|
| 426 |
+
if (NEARBY_KEYS[lower]) {
|
| 427 |
+
const options = NEARBY_KEYS[lower];
|
| 428 |
+
const replacement = options.charAt(Math.floor(Math.random() * options.length));
|
| 429 |
+
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
|
| 430 |
+
}
|
| 431 |
+
const abc = "abcdefghijklmnopqrstuvwxyz";
|
| 432 |
+
return abc.charAt(Math.floor(Math.random() * abc.length));
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 436 |
+
|
| 437 |
+
async function typeMessageWithTypos(element, text) {
|
| 438 |
+
let displayed = "";
|
| 439 |
+
const words = text.split(" ");
|
| 440 |
+
|
| 441 |
+
for (let i = 0; i < words.length; i++) {
|
| 442 |
+
const word = words[i];
|
| 443 |
+
const separator = i > 0 ? " " : "";
|
| 444 |
+
const isAlpha = /^[a-zA-Z]+$/.test(word);
|
| 445 |
+
|
| 446 |
+
// Randomly insert a typo
|
| 447 |
+
if (i > 0 && word.length > 4 && Math.random() < TYPO_CHANCE && isAlpha) {
|
| 448 |
+
const typoPos = Math.floor(Math.random() * (word.length - 2)) + 2;
|
| 449 |
+
|
| 450 |
+
// 1. Type correct part
|
| 451 |
+
for (let char of (separator + word.substring(0, typoPos))) {
|
| 452 |
+
displayed += char;
|
| 453 |
+
element.textContent = displayed;
|
| 454 |
+
await sleep(Math.random() * 40 + 30);
|
| 455 |
+
}
|
| 456 |
+
// 2. Type wrong char
|
| 457 |
+
const wrongChar = getNearbyChar(word[typoPos]);
|
| 458 |
+
displayed += wrongChar;
|
| 459 |
+
element.textContent = displayed;
|
| 460 |
+
await sleep(Math.random() * 150 + 150); // Realize error
|
| 461 |
+
|
| 462 |
+
// 3. Backspace
|
| 463 |
+
await sleep(Math.random() * 200 + 200);
|
| 464 |
+
displayed = displayed.slice(0, -1);
|
| 465 |
+
element.textContent = displayed;
|
| 466 |
+
await sleep(Math.random() * 100 + 100);
|
| 467 |
+
|
| 468 |
+
// 4. Type rest
|
| 469 |
+
for (let char of word.substring(typoPos)) {
|
| 470 |
+
displayed += char;
|
| 471 |
+
element.textContent = displayed;
|
| 472 |
+
await sleep(Math.random() * 40 + 40);
|
| 473 |
+
}
|
| 474 |
+
} else {
|
| 475 |
+
// Type normally
|
| 476 |
+
for (let char of (separator + word)) {
|
| 477 |
+
displayed += char;
|
| 478 |
+
element.textContent = displayed;
|
| 479 |
+
await sleep(char === " " ? Math.random() * 70 + 80 : Math.random() * 40 + 20);
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 484 |
+
|
| 485 |
+
// Punctuation pauses
|
| 486 |
+
if (word && [".", "!", "?"].includes(word[word.length - 1])) {
|
| 487 |
+
await sleep(Math.random() * 400 + 400);
|
| 488 |
+
} else if (word && [",", ":", ";"].includes(word[word.length - 1])) {
|
| 489 |
+
await sleep(Math.random() * 200 + 200);
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
async function handleSend() {
|
| 495 |
+
const text = inputField.value.trim();
|
| 496 |
+
if (!text) return;
|
| 497 |
+
|
| 498 |
+
// UI state lock
|
| 499 |
+
inputField.value = '';
|
| 500 |
+
inputField.disabled = true;
|
| 501 |
+
sendButton.disabled = true;
|
| 502 |
+
|
| 503 |
+
appendMessage('user', text);
|
| 504 |
+
const typingEl = showTypingIndicator();
|
| 505 |
+
|
| 506 |
+
try {
|
| 507 |
+
// Call the FastAPI Backend directly
|
| 508 |
+
const response = await fetch(API_URL, {
|
| 509 |
+
method: 'POST',
|
| 510 |
+
headers: { 'Content-Type': 'application/json' },
|
| 511 |
+
body: JSON.stringify({
|
| 512 |
+
message: text,
|
| 513 |
+
history: chatHistory,
|
| 514 |
+
session_id: sessionId
|
| 515 |
+
})
|
| 516 |
+
});
|
| 517 |
+
|
| 518 |
+
if (!response.ok) {
|
| 519 |
+
if (response.status === 429) throw new Error("Rate limit exceeded.");
|
| 520 |
+
throw new Error("Server error.");
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
const data = await response.json();
|
| 524 |
+
const reply = data.reply;
|
| 525 |
+
|
| 526 |
+
typingEl.remove();
|
| 527 |
+
const botEl = appendMessage('bot', '');
|
| 528 |
+
|
| 529 |
+
// Update local memory
|
| 530 |
+
chatHistory.push({ role: "user", content: text });
|
| 531 |
+
chatHistory.push({ role: "assistant", content: reply });
|
| 532 |
+
|
| 533 |
+
// Stream text out
|
| 534 |
+
await typeMessageWithTypos(botEl, reply);
|
| 535 |
+
|
| 536 |
+
} catch (error) {
|
| 537 |
+
console.error(error);
|
| 538 |
+
typingEl.remove();
|
| 539 |
+
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
|
| 540 |
+
} finally {
|
| 541 |
+
inputField.disabled = false;
|
| 542 |
+
sendButton.disabled = false;
|
| 543 |
+
inputField.focus();
|
| 544 |
+
}
|
| 545 |
+
}
|
| 546 |
+
|
| 547 |
+
// Event Listeners
|
| 548 |
+
sendButton.addEventListener('click', handleSend);
|
| 549 |
+
inputField.addEventListener('keypress', function(e) {
|
| 550 |
+
if (e.key === 'Enter') handleSend();
|
| 551 |
+
});
|
| 552 |
+
|
| 553 |
+
})();
|
| 554 |
+
</script>
|
| 555 |
+
|
| 556 |
+
</body>
|
| 557 |
+
</html>
|
static/bot-icon.lottie
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:532a144eeea5a1a6e49ab22ae4c0dc4f94e12cf8540e3514f1bc676d929eeac2
|
| 3 |
+
size 462684
|
static/chat-loader.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Martechsol Assistant Loader
|
| 3 |
+
* Usage: <script src="https://joedown11-chatrag.hf.space/static/chat-loader.js"></script>
|
| 4 |
+
*/
|
| 5 |
+
(function() {
|
| 6 |
+
const baseUrl = 'https://joedown11-chatrag.hf.space';
|
| 7 |
+
|
| 8 |
+
// Create container
|
| 9 |
+
const container = document.createElement('div');
|
| 10 |
+
container.id = 'martech-chat-widget-container';
|
| 11 |
+
document.body.appendChild(container);
|
| 12 |
+
|
| 13 |
+
// Fetch the addon HTML from the dedicated /widget endpoint
|
| 14 |
+
fetch(`${baseUrl}/widget?v=${Date.now()}`) // Cache busting
|
| 15 |
+
.then(response => response.text())
|
| 16 |
+
.then(html => {
|
| 17 |
+
// Create a temporary element to parse the HTML
|
| 18 |
+
const parser = new DOMParser();
|
| 19 |
+
const doc = parser.parseFromString(html, 'text/html');
|
| 20 |
+
|
| 21 |
+
// 1. Extract and inject styles
|
| 22 |
+
doc.querySelectorAll('style').forEach(st => {
|
| 23 |
+
document.head.appendChild(st.cloneNode(true));
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
// 2. Extract and inject body content
|
| 27 |
+
const wrapper = doc.querySelector('#martech-chat-wrapper');
|
| 28 |
+
if (wrapper) {
|
| 29 |
+
container.innerHTML = wrapper.outerHTML;
|
| 30 |
+
} else {
|
| 31 |
+
// Fallback: just take the body content
|
| 32 |
+
container.innerHTML = doc.body.innerHTML;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// 3. Extract and execute scripts (critical for logic)
|
| 36 |
+
doc.querySelectorAll('script').forEach(oldScript => {
|
| 37 |
+
const newScript = document.createElement('script');
|
| 38 |
+
Array.from(oldScript.attributes).forEach(attr => {
|
| 39 |
+
newScript.setAttribute(attr.name, attr.value);
|
| 40 |
+
});
|
| 41 |
+
newScript.innerHTML = oldScript.innerHTML;
|
| 42 |
+
document.body.appendChild(newScript);
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
console.log("Martechsol Assistant Loaded Successfully");
|
| 46 |
+
})
|
| 47 |
+
.catch(err => console.error("Failed to load Martechsol Assistant:", err));
|
| 48 |
+
})();
|
wordpress code/WP_INTEGRATION_SCRIPT.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Martechsol Chatbot Integration Script
|
| 3 |
+
Paste this snippet right before the closing </body> tag on your WordPress site.
|
| 4 |
+
It will automatically load the UI and logic from your Hugging Face space.
|
| 5 |
+
-->
|
| 6 |
+
|
| 7 |
+
<script>
|
| 8 |
+
fetch('https://joedown11-chatrag.hf.space/static/addon.html').then(r => r.text()).then(h => {
|
| 9 |
+
const d = document.createElement('div'); d.innerHTML = h; document.body.appendChild(d);
|
| 10 |
+
d.querySelectorAll('script').forEach(s => { const n = document.createElement('script'); Array.from(s.attributes).forEach(a => n.setAttribute(a.name, a.value)); n.innerHTML = s.innerHTML; document.body.appendChild(n); });
|
| 11 |
+
});
|
| 12 |
+
</script>
|
wordpress code/addon.html
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.0">
|
| 6 |
+
<title>Martechsol Assistant Widget</title>
|
| 7 |
+
<style>
|
| 8 |
+
/* Premium Design for Martechsol Assistant */
|
| 9 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
| 10 |
+
|
| 11 |
+
#martech-chat-wrapper {
|
| 12 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* Floating Action Button (FAB) */
|
| 16 |
+
#martech-chat-fab {
|
| 17 |
+
position: fixed;
|
| 18 |
+
right: 10px;
|
| 19 |
+
bottom: 25px;
|
| 20 |
+
width: auto;
|
| 21 |
+
height: auto;
|
| 22 |
+
background: transparent;
|
| 23 |
+
border-radius: 50%;
|
| 24 |
+
display: flex;
|
| 25 |
+
align-items: center;
|
| 26 |
+
justify-content: center;
|
| 27 |
+
cursor: pointer;
|
| 28 |
+
z-index: 999999;
|
| 29 |
+
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
|
| 30 |
+
-webkit-tap-highlight-color: transparent;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#martech-chat-fab:hover {
|
| 34 |
+
transform: scale(1.1);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#martech-chat-fab.is-active {
|
| 38 |
+
background: #1e293b;
|
| 39 |
+
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
|
| 40 |
+
transform: scale(0.8);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* Chat Window Styling */
|
| 44 |
+
#martech-chat-box {
|
| 45 |
+
position: fixed;
|
| 46 |
+
right: 25px;
|
| 47 |
+
bottom: 100px;
|
| 48 |
+
width: 400px;
|
| 49 |
+
height: 550px;
|
| 50 |
+
background: #ffffff;
|
| 51 |
+
border-radius: 16px;
|
| 52 |
+
overflow: hidden;
|
| 53 |
+
display: none;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
|
| 56 |
+
z-index: 999998;
|
| 57 |
+
border: 1px solid #e2e8f0;
|
| 58 |
+
opacity: 0;
|
| 59 |
+
transform: translateY(20px);
|
| 60 |
+
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#martech-chat-box.is-visible {
|
| 64 |
+
display: flex;
|
| 65 |
+
opacity: 1;
|
| 66 |
+
transform: translateY(0);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
#martech-chat-header {
|
| 70 |
+
background: #ffffff;
|
| 71 |
+
padding: 16px 20px;
|
| 72 |
+
border-bottom: 1px solid #f1f5f9;
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 12px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
@keyframes pulse-dot {
|
| 79 |
+
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
| 80 |
+
70% { transform: scale(1.1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
| 81 |
+
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
#martech-chat-header .dot {
|
| 85 |
+
width: 10px;
|
| 86 |
+
height: 10px;
|
| 87 |
+
background: #10b981;
|
| 88 |
+
border-radius: 50%;
|
| 89 |
+
animation: pulse-dot 2s infinite;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
#martech-chat-header span {
|
| 93 |
+
font-weight: 600;
|
| 94 |
+
color: #1e293b;
|
| 95 |
+
font-size: 15px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Custom Chat Area Styling */
|
| 99 |
+
#chat-messages {
|
| 100 |
+
flex: 1;
|
| 101 |
+
overflow-y: auto;
|
| 102 |
+
padding: 16px;
|
| 103 |
+
display: flex;
|
| 104 |
+
flex-direction: column;
|
| 105 |
+
gap: 12px;
|
| 106 |
+
background: #f8fafc;
|
| 107 |
+
scroll-behavior: smooth;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/* Custom Scrollbar for messages */
|
| 111 |
+
#chat-messages::-webkit-scrollbar { width: 6px; }
|
| 112 |
+
#chat-messages::-webkit-scrollbar-track { background: transparent; }
|
| 113 |
+
#chat-messages::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
| 114 |
+
|
| 115 |
+
.chat-message {
|
| 116 |
+
max-width: 85%;
|
| 117 |
+
padding: 12px 16px;
|
| 118 |
+
border-radius: 16px;
|
| 119 |
+
font-size: 14px;
|
| 120 |
+
line-height: 1.5;
|
| 121 |
+
word-wrap: break-word;
|
| 122 |
+
white-space: pre-wrap;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.chat-message.user {
|
| 126 |
+
align-self: flex-end;
|
| 127 |
+
background: #1e293b;
|
| 128 |
+
color: white;
|
| 129 |
+
border-bottom-right-radius: 4px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.chat-message.bot {
|
| 133 |
+
align-self: flex-start;
|
| 134 |
+
background: #ffffff;
|
| 135 |
+
color: #334155;
|
| 136 |
+
border-bottom-left-radius: 4px;
|
| 137 |
+
border: 1px solid #e2e8f0;
|
| 138 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
#chat-input-area {
|
| 142 |
+
padding: 16px;
|
| 143 |
+
background: #ffffff;
|
| 144 |
+
border-top: 1px solid #f1f5f9;
|
| 145 |
+
display: flex;
|
| 146 |
+
gap: 8px;
|
| 147 |
+
align-items: center;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
#chat-input {
|
| 151 |
+
flex: 1;
|
| 152 |
+
border: 1px solid #e2e8f0;
|
| 153 |
+
border-radius: 20px;
|
| 154 |
+
padding: 12px 16px;
|
| 155 |
+
font-size: 14px;
|
| 156 |
+
outline: none;
|
| 157 |
+
transition: border-color 0.2s;
|
| 158 |
+
font-family: inherit;
|
| 159 |
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
#chat-input:focus {
|
| 163 |
+
border-color: #10b981;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
#chat-input:disabled {
|
| 167 |
+
background: #f1f5f9;
|
| 168 |
+
cursor: not-allowed;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
#chat-send {
|
| 172 |
+
background: #10b981;
|
| 173 |
+
color: white;
|
| 174 |
+
border: none;
|
| 175 |
+
width: 44px;
|
| 176 |
+
height: 44px;
|
| 177 |
+
border-radius: 50%;
|
| 178 |
+
display: flex;
|
| 179 |
+
align-items: center;
|
| 180 |
+
justify-content: center;
|
| 181 |
+
cursor: pointer;
|
| 182 |
+
transition: background 0.2s, transform 0.1s;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
#chat-send:hover {
|
| 186 |
+
background: #059669;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
#chat-send:active {
|
| 190 |
+
transform: scale(0.95);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
#chat-send:disabled {
|
| 194 |
+
background: #cbd5e1;
|
| 195 |
+
cursor: not-allowed;
|
| 196 |
+
transform: none;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* Loading / Typing Animation */
|
| 200 |
+
.typing-indicator {
|
| 201 |
+
display: flex;
|
| 202 |
+
gap: 4px;
|
| 203 |
+
padding: 4px 2px;
|
| 204 |
+
align-items: center;
|
| 205 |
+
height: 18px;
|
| 206 |
+
}
|
| 207 |
+
.typing-indicator span {
|
| 208 |
+
width: 6px;
|
| 209 |
+
height: 6px;
|
| 210 |
+
background: #94a3b8;
|
| 211 |
+
border-radius: 50%;
|
| 212 |
+
animation: typing-dots 1.4s infinite ease-in-out both;
|
| 213 |
+
}
|
| 214 |
+
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
| 215 |
+
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
| 216 |
+
@keyframes typing-dots {
|
| 217 |
+
0%, 80%, 100% { transform: scale(0); }
|
| 218 |
+
40% { transform: scale(1); }
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
@media (max-width: 480px) {
|
| 222 |
+
#martech-chat-box {
|
| 223 |
+
right: 15px;
|
| 224 |
+
bottom: 100px;
|
| 225 |
+
width: calc(100vw - 30px);
|
| 226 |
+
height: 70vh;
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
</style>
|
| 230 |
+
</head>
|
| 231 |
+
<body>
|
| 232 |
+
|
| 233 |
+
<!-- Lottie Player Script -->
|
| 234 |
+
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 235 |
+
|
| 236 |
+
<div id="martech-chat-wrapper">
|
| 237 |
+
<div id="martech-chat-fab">
|
| 238 |
+
<!-- Animated Chatbot Icon -->
|
| 239 |
+
<div id="chat-icon" style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 240 |
+
<dotlottie-player
|
| 241 |
+
src="https://seashell-cattle-543014.hostingersite.com/wp-content/uploads/2026/04/KhloeSAC.lottie"
|
| 242 |
+
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop autoplay>
|
| 243 |
+
</dotlottie-player>
|
| 244 |
+
</div>
|
| 245 |
+
<!-- Close Icon -->
|
| 246 |
+
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 247 |
+
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="white" />
|
| 248 |
+
</svg>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div id="martech-chat-box">
|
| 252 |
+
<div id="martech-chat-header">
|
| 253 |
+
<div class="dot"></div>
|
| 254 |
+
<span>Martechsol Assistant</span>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<!-- Custom Chat UI replacing iframe -->
|
| 258 |
+
<div id="chat-messages">
|
| 259 |
+
<div class="chat-message bot">Welcome! How can I help you today?</div>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<div id="chat-input-area">
|
| 263 |
+
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
|
| 264 |
+
<button id="chat-send" title="Send">
|
| 265 |
+
<!-- Send Icon -->
|
| 266 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 267 |
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 268 |
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 269 |
+
</svg>
|
| 270 |
+
</button>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<script>
|
| 276 |
+
(function () {
|
| 277 |
+
// UI Elements
|
| 278 |
+
const fab = document.getElementById('martech-chat-fab');
|
| 279 |
+
const box = document.getElementById('martech-chat-box');
|
| 280 |
+
const chatIcon = document.getElementById('chat-icon');
|
| 281 |
+
const closeIcon = document.getElementById('close-icon');
|
| 282 |
+
const messagesContainer = document.getElementById('chat-messages');
|
| 283 |
+
const inputField = document.getElementById('chat-input');
|
| 284 |
+
const sendButton = document.getElementById('chat-send');
|
| 285 |
+
|
| 286 |
+
// FAB Toggle Logic
|
| 287 |
+
fab.addEventListener('click', function () {
|
| 288 |
+
const isVisible = box.classList.contains('is-visible');
|
| 289 |
+
if (isVisible) {
|
| 290 |
+
box.classList.remove('is-visible');
|
| 291 |
+
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 292 |
+
chatIcon.style.display = 'block';
|
| 293 |
+
closeIcon.style.display = 'none';
|
| 294 |
+
fab.classList.remove('is-active');
|
| 295 |
+
} else {
|
| 296 |
+
box.style.display = 'flex';
|
| 297 |
+
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
|
| 298 |
+
chatIcon.style.display = 'none';
|
| 299 |
+
closeIcon.style.display = 'block';
|
| 300 |
+
fab.classList.add('is-active');
|
| 301 |
+
}
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
// Chat Logic
|
| 305 |
+
const API_URL = "https://joedown11-chatrag.hf.space/api/chat";
|
| 306 |
+
let chatHistory = [];
|
| 307 |
+
|
| 308 |
+
// ── Session ID: generate once, persist in localStorage ──
|
| 309 |
+
let sessionId = localStorage.getItem('martech_session_id');
|
| 310 |
+
if (!sessionId) {
|
| 311 |
+
sessionId = 'sess-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 312 |
+
localStorage.setItem('martech_session_id', sessionId);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function appendMessage(role, content) {
|
| 316 |
+
const div = document.createElement('div');
|
| 317 |
+
div.className = `chat-message ${role}`;
|
| 318 |
+
div.textContent = content;
|
| 319 |
+
messagesContainer.appendChild(div);
|
| 320 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 321 |
+
return div;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function showTypingIndicator() {
|
| 325 |
+
const div = document.createElement('div');
|
| 326 |
+
div.className = 'chat-message bot';
|
| 327 |
+
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
| 328 |
+
messagesContainer.appendChild(div);
|
| 329 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 330 |
+
return div;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Typo Configuration for Human-like Typing
|
| 334 |
+
const NEARBY_KEYS = {
|
| 335 |
+
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 336 |
+
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 337 |
+
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 338 |
+
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 339 |
+
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 340 |
+
'z': 'xas',
|
| 341 |
+
};
|
| 342 |
+
const TYPO_CHANCE = 0.07;
|
| 343 |
+
|
| 344 |
+
function getNearbyChar(ch) {
|
| 345 |
+
const lower = ch.toLowerCase();
|
| 346 |
+
if (NEARBY_KEYS[lower]) {
|
| 347 |
+
const options = NEARBY_KEYS[lower];
|
| 348 |
+
const replacement = options.charAt(Math.floor(Math.random() * options.length));
|
| 349 |
+
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
|
| 350 |
+
}
|
| 351 |
+
const abc = "abcdefghijklmnopqrstuvwxyz";
|
| 352 |
+
return abc.charAt(Math.floor(Math.random() * abc.length));
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 356 |
+
|
| 357 |
+
async function typeMessageWithTypos(element, text) {
|
| 358 |
+
let displayed = "";
|
| 359 |
+
const words = text.split(" ");
|
| 360 |
+
|
| 361 |
+
for (let i = 0; i < words.length; i++) {
|
| 362 |
+
const word = words[i];
|
| 363 |
+
const separator = i > 0 ? " " : "";
|
| 364 |
+
const isAlpha = /^[a-zA-Z]+$/.test(word);
|
| 365 |
+
|
| 366 |
+
// Randomly insert a typo
|
| 367 |
+
if (i > 0 && word.length > 4 && Math.random() < TYPO_CHANCE && isAlpha) {
|
| 368 |
+
const typoPos = Math.floor(Math.random() * (word.length - 2)) + 2;
|
| 369 |
+
|
| 370 |
+
// 1. Type correct part
|
| 371 |
+
for (let char of (separator + word.substring(0, typoPos))) {
|
| 372 |
+
displayed += char;
|
| 373 |
+
element.textContent = displayed;
|
| 374 |
+
await sleep(Math.random() * 40 + 30);
|
| 375 |
+
}
|
| 376 |
+
// 2. Type wrong char
|
| 377 |
+
const wrongChar = getNearbyChar(word[typoPos]);
|
| 378 |
+
displayed += wrongChar;
|
| 379 |
+
element.textContent = displayed;
|
| 380 |
+
await sleep(Math.random() * 150 + 150); // Realize error
|
| 381 |
+
|
| 382 |
+
// 3. Backspace
|
| 383 |
+
await sleep(Math.random() * 200 + 200);
|
| 384 |
+
displayed = displayed.slice(0, -1);
|
| 385 |
+
element.textContent = displayed;
|
| 386 |
+
await sleep(Math.random() * 100 + 100);
|
| 387 |
+
|
| 388 |
+
// 4. Type rest
|
| 389 |
+
for (let char of word.substring(typoPos)) {
|
| 390 |
+
displayed += char;
|
| 391 |
+
element.textContent = displayed;
|
| 392 |
+
await sleep(Math.random() * 40 + 40);
|
| 393 |
+
}
|
| 394 |
+
} else {
|
| 395 |
+
// Type normally
|
| 396 |
+
for (let char of (separator + word)) {
|
| 397 |
+
displayed += char;
|
| 398 |
+
element.textContent = displayed;
|
| 399 |
+
await sleep(char === " " ? Math.random() * 70 + 80 : Math.random() * 40 + 20);
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 404 |
+
|
| 405 |
+
// Punctuation pauses
|
| 406 |
+
if (word && [".", "!", "?"].includes(word[word.length - 1])) {
|
| 407 |
+
await sleep(Math.random() * 400 + 400);
|
| 408 |
+
} else if (word && [",", ":", ";"].includes(word[word.length - 1])) {
|
| 409 |
+
await sleep(Math.random() * 200 + 200);
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
async function handleSend() {
|
| 415 |
+
const text = inputField.value.trim();
|
| 416 |
+
if (!text) return;
|
| 417 |
+
|
| 418 |
+
// UI state lock
|
| 419 |
+
inputField.value = '';
|
| 420 |
+
inputField.disabled = true;
|
| 421 |
+
sendButton.disabled = true;
|
| 422 |
+
|
| 423 |
+
appendMessage('user', text);
|
| 424 |
+
const typingEl = showTypingIndicator();
|
| 425 |
+
|
| 426 |
+
try {
|
| 427 |
+
// Call the FastAPI Backend directly
|
| 428 |
+
const response = await fetch(API_URL, {
|
| 429 |
+
method: 'POST',
|
| 430 |
+
headers: { 'Content-Type': 'application/json' },
|
| 431 |
+
body: JSON.stringify({
|
| 432 |
+
message: text,
|
| 433 |
+
history: chatHistory,
|
| 434 |
+
session_id: sessionId
|
| 435 |
+
})
|
| 436 |
+
});
|
| 437 |
+
|
| 438 |
+
if (!response.ok) {
|
| 439 |
+
if (response.status === 429) throw new Error("Rate limit exceeded.");
|
| 440 |
+
throw new Error("Server error.");
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
const data = await response.json();
|
| 444 |
+
const reply = data.reply;
|
| 445 |
+
|
| 446 |
+
typingEl.remove();
|
| 447 |
+
const botEl = appendMessage('bot', '');
|
| 448 |
+
|
| 449 |
+
// Update local memory
|
| 450 |
+
chatHistory.push({ role: "user", content: text });
|
| 451 |
+
chatHistory.push({ role: "assistant", content: reply });
|
| 452 |
+
|
| 453 |
+
// Stream text out
|
| 454 |
+
await typeMessageWithTypos(botEl, reply);
|
| 455 |
+
|
| 456 |
+
} catch (error) {
|
| 457 |
+
console.error(error);
|
| 458 |
+
typingEl.remove();
|
| 459 |
+
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
|
| 460 |
+
} finally {
|
| 461 |
+
inputField.disabled = false;
|
| 462 |
+
sendButton.disabled = false;
|
| 463 |
+
inputField.focus();
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
// Event Listeners
|
| 468 |
+
sendButton.addEventListener('click', handleSend);
|
| 469 |
+
inputField.addEventListener('keypress', function(e) {
|
| 470 |
+
if (e.key === 'Enter') handleSend();
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
})();
|
| 474 |
+
</script>
|
| 475 |
+
|
| 476 |
+
</body>
|
| 477 |
+
</html>
|
wordpress code/addon_backup.html
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<style>
|
| 2 |
+
/* Premium Design for Martechsol Assistant */
|
| 3 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
| 4 |
+
|
| 5 |
+
#martech-chat-wrapper {
|
| 6 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/* Floating Action Button (FAB) */
|
| 10 |
+
#martech-chat-fab {
|
| 11 |
+
position: fixed;
|
| 12 |
+
right: 10px;
|
| 13 |
+
bottom: 25px;
|
| 14 |
+
width: auto;
|
| 15 |
+
height: auto;
|
| 16 |
+
background: transparent;
|
| 17 |
+
border-radius: 50%;
|
| 18 |
+
display: flex;
|
| 19 |
+
align-items: center;
|
| 20 |
+
justify-content: center;
|
| 21 |
+
cursor: pointer;
|
| 22 |
+
z-index: 999999;
|
| 23 |
+
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
|
| 24 |
+
-webkit-tap-highlight-color: transparent;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
#martech-chat-fab:hover {
|
| 28 |
+
transform: scale(1.1);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
#martech-chat-fab.is-active {
|
| 32 |
+
background: #1e293b;
|
| 33 |
+
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
|
| 34 |
+
transform: scale(0.8);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Chat Window Styling */
|
| 38 |
+
#martech-chat-box {
|
| 39 |
+
position: fixed;
|
| 40 |
+
right: 25px;
|
| 41 |
+
bottom: 100px;
|
| 42 |
+
width: 400px;
|
| 43 |
+
height: 550px;
|
| 44 |
+
background: #ffffff;
|
| 45 |
+
border-radius: 16px;
|
| 46 |
+
overflow: hidden;
|
| 47 |
+
display: none;
|
| 48 |
+
flex-direction: column;
|
| 49 |
+
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
|
| 50 |
+
z-index: 999998;
|
| 51 |
+
border: 1px solid #e2e8f0;
|
| 52 |
+
opacity: 0;
|
| 53 |
+
transform: translateY(20px);
|
| 54 |
+
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#martech-chat-box.is-visible {
|
| 58 |
+
display: flex;
|
| 59 |
+
opacity: 1;
|
| 60 |
+
transform: translateY(0);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#martech-chat-header {
|
| 64 |
+
background: #ffffff;
|
| 65 |
+
padding: 16px 20px;
|
| 66 |
+
border-bottom: 1px solid #f1f5f9;
|
| 67 |
+
display: flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
gap: 12px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
@keyframes pulse-dot {
|
| 73 |
+
0% {
|
| 74 |
+
transform: scale(1);
|
| 75 |
+
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
70% {
|
| 79 |
+
transform: scale(1.1);
|
| 80 |
+
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
100% {
|
| 84 |
+
transform: scale(1);
|
| 85 |
+
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
#martech-chat-header .dot {
|
| 90 |
+
width: 10px;
|
| 91 |
+
height: 10px;
|
| 92 |
+
background: #10b981;
|
| 93 |
+
border-radius: 50%;
|
| 94 |
+
animation: pulse-dot 2s infinite;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
#martech-chat-header span {
|
| 98 |
+
font-weight: 600;
|
| 99 |
+
color: #1e293b;
|
| 100 |
+
font-size: 15px;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
#martech-chat-box iframe {
|
| 104 |
+
width: 100%;
|
| 105 |
+
flex: 1;
|
| 106 |
+
border: 0;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
@media (max-width: 480px) {
|
| 110 |
+
#martech-chat-box {
|
| 111 |
+
right: 15px;
|
| 112 |
+
bottom: 100px;
|
| 113 |
+
width: calc(100vw - 30px);
|
| 114 |
+
height: 70vh;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
</style>
|
| 118 |
+
|
| 119 |
+
<!-- Lottie Player Script -->
|
| 120 |
+
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 121 |
+
|
| 122 |
+
<div id="martech-chat-wrapper">
|
| 123 |
+
<div id="martech-chat-fab">
|
| 124 |
+
<!-- Animated Chatbot Icon -->
|
| 125 |
+
<div id="chat-icon"
|
| 126 |
+
style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 127 |
+
<dotlottie-player
|
| 128 |
+
src="https://seashell-cattle-543014.hostingersite.com/wp-content/uploads/2026/04/KhloeSAC.lottie"
|
| 129 |
+
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop
|
| 130 |
+
autoplay>
|
| 131 |
+
</dotlottie-player>
|
| 132 |
+
</div>
|
| 133 |
+
<!-- Close Icon -->
|
| 134 |
+
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 135 |
+
<path
|
| 136 |
+
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
| 137 |
+
fill="white" />
|
| 138 |
+
</svg>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
<div id="martech-chat-box">
|
| 142 |
+
<div id="martech-chat-header">
|
| 143 |
+
<div class="dot"></div>
|
| 144 |
+
<span>Martechsol Assistant</span>
|
| 145 |
+
</div>
|
| 146 |
+
<iframe src="https://joedown11-chatrag.hf.space" allow="clipboard-write; microphone" loading="lazy"
|
| 147 |
+
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
|
| 151 |
+
<script>
|
| 152 |
+
(function () {
|
| 153 |
+
const fab = document.getElementById('martech-chat-fab');
|
| 154 |
+
const box = document.getElementById('martech-chat-box');
|
| 155 |
+
const chatIcon = document.getElementById('chat-icon');
|
| 156 |
+
const closeIcon = document.getElementById('close-icon');
|
| 157 |
+
|
| 158 |
+
fab.addEventListener('click', function () {
|
| 159 |
+
const isVisible = box.classList.contains('is-visible');
|
| 160 |
+
if (isVisible) {
|
| 161 |
+
box.classList.remove('is-visible');
|
| 162 |
+
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 163 |
+
chatIcon.style.display = 'block';
|
| 164 |
+
closeIcon.style.display = 'none';
|
| 165 |
+
fab.classList.remove('is-active');
|
| 166 |
+
} else {
|
| 167 |
+
box.style.display = 'flex';
|
| 168 |
+
setTimeout(() => { box.classList.add('is-visible'); }, 10);
|
| 169 |
+
chatIcon.style.display = 'none';
|
| 170 |
+
closeIcon.style.display = 'block';
|
| 171 |
+
fab.classList.add('is-active');
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
+
})();
|
| 175 |
+
</script>
|