Martechsol commited on
Commit ·
5879cf4
1
Parent(s): dae9ce0
chore: ignore and untrack backup folders
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +4 -0
- backup_2026-05-04/.env.example +0 -15
- backup_2026-05-04/.gitattributes +0 -2
- backup_2026-05-04/.gitignore +0 -9
- backup_2026-05-04/CONFIG_NOTES.md +0 -38
- backup_2026-05-04/CUSTOMIZATION_GUIDE.md +0 -55
- backup_2026-05-04/Dockerfile +0 -31
- backup_2026-05-04/PHPMailer/Exception.php +0 -40
- backup_2026-05-04/PHPMailer/PHPMailer.php +0 -0
- backup_2026-05-04/PHPMailer/SMTP.php +0 -1509
- backup_2026-05-04/PROJECT_OVERVIEW.md +0 -32
- backup_2026-05-04/README.md +0 -164
- backup_2026-05-04/app.py +0 -6
- backup_2026-05-04/app/__init__.py +0 -0
- backup_2026-05-04/app/admin/__init__.py +0 -1
- backup_2026-05-04/app/admin/router.py +0 -346
- backup_2026-05-04/app/admin/templates/admin.html +0 -1143
- backup_2026-05-04/app/api/__init__.py +0 -0
- backup_2026-05-04/app/api/schemas.py +0 -27
- backup_2026-05-04/app/core/__init__.py +0 -0
- backup_2026-05-04/app/core/config.py +0 -51
- backup_2026-05-04/app/main.py +0 -196
- backup_2026-05-04/app/services/__init__.py +0 -0
- backup_2026-05-04/app/services/chunker.py +0 -39
- backup_2026-05-04/app/services/document_loader.py +0 -37
- backup_2026-05-04/app/services/embeddings.py +0 -34
- backup_2026-05-04/app/services/llm.py +0 -264
- backup_2026-05-04/app/services/rag_pipeline.py +0 -98
- backup_2026-05-04/app/services/rate_limiter.py +0 -24
- backup_2026-05-04/app/services/reranker.py +0 -26
- backup_2026-05-04/app/services/session_store.py +0 -284
- backup_2026-05-04/app/services/vector_store.py +0 -167
- backup_2026-05-04/app/ui_gradio.py +0 -275
- backup_2026-05-04/docker-compose.yml +0 -14
- backup_2026-05-04/docs/MartechSol_Employee_HandBook (7) (1).pdf +0 -3
- backup_2026-05-04/docs/MartechSol_Employee_Handbook_Extracted.txt +0 -1091
- backup_2026-05-04/floaitng-icon/KhloeSAC.lottie +0 -3
- backup_2026-05-04/get_models.py +0 -25
- backup_2026-05-04/martech_sol_logo.jpg +0 -0
- backup_2026-05-04/requirements.txt +0 -14
- backup_2026-05-04/static/addon.html +0 -543
- backup_2026-05-04/static/bot-icon.lottie +0 -3
- backup_2026-05-04/static/chat-loader.js +0 -48
- backup_2026-05-04/test_groq.py +0 -26
- backup_2026-05-04/wordpress code/WP_INTEGRATION_SCRIPT.txt +0 -12
- backup_2026-05-04/wordpress code/addon.html +0 -477
- backup_2026-05-04/wordpress code/addon_backup.html +0 -175
- backup_2026-05-13_pre_intelligent_retrieval/.env.example +0 -19
- backup_2026-05-13_pre_intelligent_retrieval/.gitattributes +0 -2
- backup_2026-05-13_pre_intelligent_retrieval/.gitignore +0 -9
.gitignore
CHANGED
|
@@ -7,3 +7,7 @@ venv/
|
|
| 7 |
# Ignoring data directory to prevent pushing binary index files
|
| 8 |
data/index/
|
| 9 |
data/sessions/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# Ignoring data directory to prevent pushing binary index files
|
| 8 |
data/index/
|
| 9 |
data/sessions/
|
| 10 |
+
|
| 11 |
+
# Ignore local backup directories
|
| 12 |
+
backup*/
|
| 13 |
+
*backup*/
|
backup_2026-05-04/.env.example
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 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=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/.gitattributes
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
*.lottie filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.pdf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
backup_2026-05-04/.gitignore
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
.env
|
| 2 |
-
__pycache__/
|
| 3 |
-
.pytest_cache/
|
| 4 |
-
.mypy_cache/
|
| 5 |
-
.venv/
|
| 6 |
-
venv/
|
| 7 |
-
# Ignoring data directory to prevent pushing binary index files
|
| 8 |
-
data/index/
|
| 9 |
-
data/sessions/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/CONFIG_NOTES.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/CUSTOMIZATION_GUIDE.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 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).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/Dockerfile
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 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"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/PHPMailer/Exception.php
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/PHPMailer/PHPMailer.php
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backup_2026-05-04/PHPMailer/SMTP.php
DELETED
|
@@ -1,1509 +0,0 @@
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/PROJECT_OVERVIEW.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 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 a hybrid LLM approach:
|
| 7 |
-
- **Qwen 2.5 32B** (specifically `qwen/qwen3-32b`) as the main reasoning and generation engine.
|
| 8 |
-
- **Llama 3.1 8B** (specifically `llama-3.1-8b-instant`) as a lightning-fast model for query rewriting and expansion.
|
| 9 |
-
- **Provider**: Both are powered by **Groq**, enabling near-instantaneous responses.
|
| 10 |
-
|
| 11 |
-
## 2. How it Works (The "RAG" Process)
|
| 12 |
-
Instead of just relying on general knowledge, this bot "reads" your documents to give specific answers.
|
| 13 |
-
1. **Reading**: It looks at your files in the `docs/` folder (PDFs and Text files).
|
| 14 |
-
2. **Memorizing**: It breaks the text into small chunks and converts them into mathematical "vectors" (using the `bge-small-en-v1.5` model).
|
| 15 |
-
3. **Searching**: When you ask a question, it expands the query using Llama 3.1 8B, then performs a **Hybrid Search** combining Dense vectors (**FAISS**) and Keyword search (**BM25**).
|
| 16 |
-
4. **Reranking**: It deeply evaluates the top retrieved chunks using `bge-reranker-base` to ensure maximum relevance.
|
| 17 |
-
5. **Answering**: It sends your question along with the most relevant document parts to the Qwen 2.5 AI, which then writes a highly precise, formatted reply.
|
| 18 |
-
|
| 19 |
-
## 3. The Architecture
|
| 20 |
-
### Frontend (The Face)
|
| 21 |
-
- **Gradio**: This is the clean, chat-like interface you see. It is hosted on **Hugging Face Spaces**.
|
| 22 |
-
- **WordPress Addon**: A custom HTML/CSS/JS wrapper that lets you embed the chat as a beautiful floating button on your website.
|
| 23 |
-
|
| 24 |
-
### Backend (The Engine)
|
| 25 |
-
- **FastAPI**: A high-performance Python framework that connects everything. It manages the messages, handles the document search, and talks to the AI provider.
|
| 26 |
-
- **Uvicorn**: The lightning-fast server that runs the FastAPI code.
|
| 27 |
-
|
| 28 |
-
## 4. Key Features
|
| 29 |
-
- **Humanistic Replies**: The bot is programmed to be polite, conversational, and professional.
|
| 30 |
-
- **Context-Aware**: It remembers the last few messages in the conversation so you can ask follow-up questions.
|
| 31 |
-
- **Greeting Support**: It can handle "Hi" and "Hello" naturally before getting down to business.
|
| 32 |
-
- **Safe & Grounded**: It is instructed to only answer based on your documents to prevent "hallucinations" (making things up).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/README.md
DELETED
|
@@ -1,164 +0,0 @@
|
|
| 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/`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app.py
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/__init__.py
DELETED
|
File without changes
|
backup_2026-05-04/app/admin/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# app/admin/__init__.py
|
|
|
|
|
|
backup_2026-05-04/app/admin/router.py
DELETED
|
@@ -1,346 +0,0 @@
|
|
| 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 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/admin/templates/admin.html
DELETED
|
@@ -1,1143 +0,0 @@
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/api/__init__.py
DELETED
|
File without changes
|
backup_2026-05-04/app/api/schemas.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/core/__init__.py
DELETED
|
File without changes
|
backup_2026-05-04/app/core/config.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 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="qwen/qwen3-32b", alias="GROQ_MODEL")
|
| 14 |
-
groq_rewrite_model: str = Field(default="llama-3.1-8b-instant", alias="GROQ_REWRITE_MODEL")
|
| 15 |
-
hf_api_key: str = Field(default="", alias="HF_API_KEY")
|
| 16 |
-
hf_model: str = Field(default="meta-llama/Llama-3.1-8B-Instruct", alias="HF_MODEL")
|
| 17 |
-
embedding_model: str = Field(default="BAAI/bge-small-en-v1.5", alias="EMBEDDING_MODEL")
|
| 18 |
-
reranker_model: str = Field(default="BAAI/bge-reranker-base", alias="RERANKER_MODEL")
|
| 19 |
-
docs_dir: Path = Field(default=Path("docs"), alias="DOCS_DIR")
|
| 20 |
-
@model_validator(mode='after')
|
| 21 |
-
def set_persistent_paths(self) -> 'Settings':
|
| 22 |
-
# If we are on Hugging Face (detected by /data volume),
|
| 23 |
-
# force use of /data for persistence regardless of other settings.
|
| 24 |
-
if Path("/data").exists():
|
| 25 |
-
self.index_dir = Path("/data/index")
|
| 26 |
-
self.sessions_dir = Path("/data/sessions")
|
| 27 |
-
return self
|
| 28 |
-
|
| 29 |
-
index_dir: Path = Field(default=Path("data/index"), alias="INDEX_DIR")
|
| 30 |
-
sessions_dir: Path = Field(default=Path("data/sessions"), alias="SESSIONS_DIR")
|
| 31 |
-
chunk_size_tokens: int = 350 # Reduced for TPM safety
|
| 32 |
-
chunk_overlap_tokens: int = 80
|
| 33 |
-
top_k: int = Field(default=20, alias="TOP_K")
|
| 34 |
-
max_context_chunks: int = 10 # Increased to 10 to ensure all list items (like leaves) are captured
|
| 35 |
-
request_timeout_s: float = 20.0
|
| 36 |
-
cors_allow_origins: str = Field(default="*", alias="CORS_ALLOW_ORIGINS")
|
| 37 |
-
api_key: str = Field(default="", alias="API_KEY")
|
| 38 |
-
rate_limit_requests: int = Field(default=60, alias="RATE_LIMIT_REQUESTS")
|
| 39 |
-
rate_limit_window_seconds: int = Field(default=60, alias="RATE_LIMIT_WINDOW_SECONDS")
|
| 40 |
-
|
| 41 |
-
# SMTP Settings for OTP
|
| 42 |
-
smtp_server: str = Field(default="smtp.gmail.com", alias="SMTP_SERVER")
|
| 43 |
-
smtp_port: int = Field(default=465, alias="SMTP_PORT")
|
| 44 |
-
smtp_user: str = Field(default="", alias="SMTP_USER")
|
| 45 |
-
smtp_pass: str = Field(default="", alias="SMTP_PASS")
|
| 46 |
-
admin_email: str = Field(default="randomjoedown@gmail.com", alias="ADMIN_EMAIL")
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
@lru_cache
|
| 50 |
-
def get_settings() -> Settings:
|
| 51 |
-
return Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/main.py
DELETED
|
@@ -1,196 +0,0 @@
|
|
| 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 |
-
groq_rewrite_model=settings.groq_rewrite_model,
|
| 40 |
-
hf_api_key=settings.hf_api_key,
|
| 41 |
-
hf_model=settings.hf_model,
|
| 42 |
-
timeout_s=settings.request_timeout_s,
|
| 43 |
-
)
|
| 44 |
-
reranker_service = RerankerService(settings.reranker_model)
|
| 45 |
-
pipeline = RAGPipeline(
|
| 46 |
-
vector_store=vector_store,
|
| 47 |
-
llm_service=llm_service,
|
| 48 |
-
reranker=reranker_service,
|
| 49 |
-
top_k=settings.top_k,
|
| 50 |
-
max_context_chunks=settings.max_context_chunks
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
# ── FastAPI App ───────────────────────────────────────────────────────────────
|
| 54 |
-
fastapi_app = FastAPI(title=settings.app_name)
|
| 55 |
-
|
| 56 |
-
allow_origins = [o.strip() for o in settings.cors_allow_origins.split(",") if o.strip()]
|
| 57 |
-
fastapi_app.add_middleware(
|
| 58 |
-
CORSMiddleware,
|
| 59 |
-
allow_origins=["*"], # Explicitly allow all for testing
|
| 60 |
-
allow_credentials=True,
|
| 61 |
-
allow_methods=["*"],
|
| 62 |
-
allow_headers=["*"],
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
rate_limiter = InMemoryRateLimiter(settings.rate_limit_requests, settings.rate_limit_window_seconds)
|
| 66 |
-
|
| 67 |
-
# ── Admin Panel (mounted on FastAPI before Gradio wrapping) ───────────────────
|
| 68 |
-
fastapi_app.include_router(admin_router)
|
| 69 |
-
|
| 70 |
-
# ── Static Files ──────────────────────────────────────────────────────────────
|
| 71 |
-
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
| 72 |
-
if os.path.exists(static_dir):
|
| 73 |
-
fastapi_app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
| 74 |
-
|
| 75 |
-
@fastapi_app.get("/widget", include_in_schema=False)
|
| 76 |
-
async def get_widget():
|
| 77 |
-
"""Serves the chat widget HTML with explicit CORS headers."""
|
| 78 |
-
from fastapi.responses import FileResponse
|
| 79 |
-
path = os.path.join(static_dir, "addon.html")
|
| 80 |
-
if not os.path.exists(path):
|
| 81 |
-
raise HTTPException(status_code=404, detail="Widget not found")
|
| 82 |
-
return FileResponse(
|
| 83 |
-
path,
|
| 84 |
-
headers={
|
| 85 |
-
"Access-Control-Allow-Origin": "*",
|
| 86 |
-
"Cache-Control": "no-cache, no-store, must-revalidate"
|
| 87 |
-
}
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
@fastapi_app.on_event("startup")
|
| 92 |
-
def startup_event() -> None:
|
| 93 |
-
logger.info("─── RAG SERVICE STARTUP ───")
|
| 94 |
-
logger.info("Index Directory: %s", settings.index_dir)
|
| 95 |
-
logger.info("Sessions Directory: %s", settings.sessions_dir)
|
| 96 |
-
|
| 97 |
-
# Verify persistence mount
|
| 98 |
-
if str(settings.index_dir).startswith("/data"):
|
| 99 |
-
if os.path.exists("/data"):
|
| 100 |
-
logger.info("✅ Persistent storage /data detected and in use.")
|
| 101 |
-
else:
|
| 102 |
-
logger.warning("⚠️ /data requested but NOT found on filesystem!")
|
| 103 |
-
else:
|
| 104 |
-
logger.warning("⚠️ Using ephemeral storage (data will be lost on rebuild).")
|
| 105 |
-
|
| 106 |
-
logger.info("Loading/building FAISS index...")
|
| 107 |
-
vector_store.build_or_load()
|
| 108 |
-
logger.info("RAG service started with %s chunks", len(vector_store.metadata))
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
@fastapi_app.get("/", include_in_schema=False)
|
| 112 |
-
async def root():
|
| 113 |
-
"""Prevent direct browser access to the root URL."""
|
| 114 |
-
raise HTTPException(
|
| 115 |
-
status_code=403,
|
| 116 |
-
detail="Direct access to this resource is restricted. Please use the authorized interface."
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
@fastapi_app.get("/health", response_model=HealthResponse)
|
| 121 |
-
def health() -> HealthResponse:
|
| 122 |
-
h = vector_store.health()
|
| 123 |
-
return HealthResponse(status="ok", **h)
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
@fastapi_app.post("/api/chat", response_model=ChatResponse)
|
| 127 |
-
async def chat(
|
| 128 |
-
payload: ChatRequest,
|
| 129 |
-
request: Request,
|
| 130 |
-
x_api_key: str | None = Header(default=None),
|
| 131 |
-
) -> ChatResponse:
|
| 132 |
-
if not payload.message.strip():
|
| 133 |
-
raise HTTPException(status_code=400, detail="message must not be empty")
|
| 134 |
-
if settings.api_key and x_api_key != settings.api_key:
|
| 135 |
-
raise HTTPException(status_code=401, detail="unauthorized")
|
| 136 |
-
|
| 137 |
-
client_ip = request.client.host if request.client else "unknown"
|
| 138 |
-
if not rate_limiter.allow(client_ip):
|
| 139 |
-
raise HTTPException(status_code=429, detail="rate limit exceeded")
|
| 140 |
-
|
| 141 |
-
logger.info("Incoming chat request: msg='%s', sid='%s'", payload.message[:30], payload.session_id)
|
| 142 |
-
|
| 143 |
-
# Fetch user_name if available for personalization
|
| 144 |
-
user_name = None
|
| 145 |
-
if payload.session_id:
|
| 146 |
-
sess_data = await _ss.get_session(payload.session_id)
|
| 147 |
-
user_name = sess_data.get("user_name")
|
| 148 |
-
|
| 149 |
-
try:
|
| 150 |
-
result = await pipeline.chat(payload.message, payload.history, user_name=user_name)
|
| 151 |
-
|
| 152 |
-
# ── Fire-and-forget: persist to session store in background ──
|
| 153 |
-
if payload.session_id:
|
| 154 |
-
asyncio.create_task(_ss.save_message(payload.session_id, payload.message, result["reply"]))
|
| 155 |
-
|
| 156 |
-
return ChatResponse(**result)
|
| 157 |
-
except Exception as e:
|
| 158 |
-
import httpx
|
| 159 |
-
import traceback
|
| 160 |
-
logger.error(f"Error in chat endpoint: {str(e)}\n{traceback.format_exc()}")
|
| 161 |
-
|
| 162 |
-
error_msg = "⚠️ Oops! Something went wrong."
|
| 163 |
-
if isinstance(e, httpx.HTTPStatusError):
|
| 164 |
-
if e.response.status_code == 429:
|
| 165 |
-
error_msg = "⚠️ Rate limit reached. Please slow down a bit!"
|
| 166 |
-
else:
|
| 167 |
-
error_msg = "⚠️ I encountered an error. Please try again in a few seconds."
|
| 168 |
-
|
| 169 |
-
return ChatResponse(reply=error_msg, retrieved_chunks=[])
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
@fastapi_app.post("/api/session/update-name")
|
| 173 |
-
async def update_session_user_name(payload: SessionUpdateNameRequest):
|
| 174 |
-
"""Updates the user name and renames the session file to match the name."""
|
| 175 |
-
new_id = await _ss.rename_session(payload.session_id, payload.user_name)
|
| 176 |
-
return {"status": "ok", "new_session_id": new_id}
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
@fastapi_app.get("/api/session/history/{session_id}")
|
| 180 |
-
async def get_session_history(session_id: str):
|
| 181 |
-
"""Returns the chat history for a session formatted for the frontend."""
|
| 182 |
-
data = await _ss.get_session(session_id)
|
| 183 |
-
if not data:
|
| 184 |
-
return {"history": []}
|
| 185 |
-
|
| 186 |
-
history = []
|
| 187 |
-
for msg in data.get("messages", []):
|
| 188 |
-
history.append({"role": "user", "content": msg["question"]})
|
| 189 |
-
history.append({"role": "assistant", "content": msg["answer"]})
|
| 190 |
-
return {"history": history, "user_name": data.get("user_name", "")}
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
# ── Mount Gradio at /ui — MUST be last, wraps the FastAPI app ─────────────────
|
| 194 |
-
# All FastAPI routes (including /admin) are already registered above and are
|
| 195 |
-
# preserved inside the Gradio-wrapped Starlette app.
|
| 196 |
-
app = gr.mount_gradio_app(fastapi_app, demo, path="/ui")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/__init__.py
DELETED
|
File without changes
|
backup_2026-05-04/app/services/chunker.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/document_loader.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/embeddings.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/llm.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
| 1 |
-
from typing import List, Dict
|
| 2 |
-
import httpx
|
| 3 |
-
import re
|
| 4 |
-
|
| 5 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 6 |
-
# MASTER SYSTEM PROMPT — Martechsol HR Assistant
|
| 7 |
-
# Intelligence: Understand Intent → Retrieve Facts → Respond Precisely
|
| 8 |
-
# ═══════════════════════════════════════════════════════════════════════
|
| 9 |
-
SYSTEM_PROMPT = """You are the Martechsol HR Assistant — intelligent, precise, and formal.
|
| 10 |
-
|
| 11 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 12 |
-
STEP 1 — UNDERSTAND THE INTENT
|
| 13 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 14 |
-
Read the question carefully. Identify the SINGLE core topic being asked. Apply intelligent defaults:
|
| 15 |
-
• "timing" / "timings" (no context) → office working hours ONLY — not payment or any other timing
|
| 16 |
-
• "leaves" / "leave" (no context) → leave names + day counts ONLY — NOT leave policies or eligibility
|
| 17 |
-
• "paid leaves" / "all leaves" → enumerate EVERY leave type with its name and count
|
| 18 |
-
• "salary" / "pay" (no context) → salary structure or amount — NOT payment date unless explicitly asked
|
| 19 |
-
• "benefits" / "perks" / "allowances" → list EVERY benefit with its name and value
|
| 20 |
-
• "terminate" / "termination" → resignation/termination procedure — NOT general policies
|
| 21 |
-
If a question has an obvious workplace context, always default to the most common interpretation.
|
| 22 |
-
|
| 23 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 24 |
-
STEP 2 — STRICT SCOPE DISCIPLINE
|
| 25 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 26 |
-
Answer ONLY what was asked. NEVER expand into:
|
| 27 |
-
• Policies, approval processes, eligibility rules, or consequences — unless user asks for policy/process
|
| 28 |
-
• Related topics the user did not mention
|
| 29 |
-
• Broad overviews when a specific fact was requested
|
| 30 |
-
• Context that wasn't in the question
|
| 31 |
-
|
| 32 |
-
Scope examples:
|
| 33 |
-
"How many sick leaves?" → ONE number. Not the sick leave policy.
|
| 34 |
-
"What are office timings?" → ONE sentence with time range. Not break times or exceptions.
|
| 35 |
-
"What are paid leaves?" → Complete list of all paid leave types + counts. Not rules for taking them.
|
| 36 |
-
"Tell me about maternity leave" → Count + brief key fact. Not full policies unless asked.
|
| 37 |
-
|
| 38 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 39 |
-
STEP 3 — CHOOSE THE RIGHT FORMAT
|
| 40 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 41 |
-
|
| 42 |
-
FORMAT A — SINGLE FACT (default for most questions)
|
| 43 |
-
When: One direct answer needed (timings, a specific leave count, a single number/date)
|
| 44 |
-
Rule: ONE complete sentence. Maximum 25–30 words. Never cut mid-sentence.
|
| 45 |
-
Example: Office hours are <b>9:00 AM to 6:00 PM</b>, Monday to Saturday.
|
| 46 |
-
|
| 47 |
-
FORMAT B — EXHAUSTIVE LIST
|
| 48 |
-
When: User asks for ALL items in a category ("all leaves", "all benefits", "list all X", "paid leaves")
|
| 49 |
-
Rule:
|
| 50 |
-
• Include EVERY single item found — omitting even one is FORBIDDEN
|
| 51 |
-
• One item per line: <b>Item Name:</b> value<br>
|
| 52 |
-
• No intro sentence, no closing sentence, no extra commentary
|
| 53 |
-
• Continue until ALL items are listed
|
| 54 |
-
Example:
|
| 55 |
-
<b>Casual Leave:</b> 10 days<br>
|
| 56 |
-
<b>Sick Leave:</b> 10 days<br>
|
| 57 |
-
<b>Annual Leave:</b> 14 days<br>
|
| 58 |
-
<b>Maternity Leave:</b> 90 days<br>
|
| 59 |
-
<b>Paternity Leave:</b> 3 days<br>
|
| 60 |
-
<b>Hajj Leave:</b> 30 days<br>
|
| 61 |
-
...(list every item — do NOT stop early)
|
| 62 |
-
|
| 63 |
-
FORMAT C — BRIEF EXPLANATION
|
| 64 |
-
When: User asks HOW something works, or asks for a process/procedure
|
| 65 |
-
Rule: Maximum 3 bullet points. Each bullet = one complete, factual sentence. No filler words.
|
| 66 |
-
|
| 67 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 68 |
-
STRICT QUALITY RULES
|
| 69 |
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 70 |
-
✓ ZERO hallucination — every fact must exist in Expert Data only. No guessing.
|
| 71 |
-
✓ If not in Expert Data → reply exactly: "I don't have that information."
|
| 72 |
-
✓ Never cut a sentence mid-way — always complete every sentence fully
|
| 73 |
-
✓ NEVER mention: "document", "handbook", "manual", "policy file", or any source reference
|
| 74 |
-
✓ Use <b>bold</b> for names, numbers, dates, leave types, and all key terms
|
| 75 |
-
✓ Use <br> between list items for clean vertical spacing
|
| 76 |
-
✓ Tone: formal, warm, and professional — never robotic, never chatty
|
| 77 |
-
✓ Do NOT add greetings, closings, or "Is there anything else?" type phrases"""
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def _build_context(chunks: List[Dict[str, str]], max_words: int = 1500) -> str:
|
| 81 |
-
"""Combines retrieved chunks into a clean context string, capped at max_words.
|
| 82 |
-
Prevents TPM spikes when chunks are unexpectedly large."""
|
| 83 |
-
if not chunks:
|
| 84 |
-
return ""
|
| 85 |
-
parts = []
|
| 86 |
-
total_words = 0
|
| 87 |
-
for c in chunks:
|
| 88 |
-
text = c['text']
|
| 89 |
-
word_count = len(text.split())
|
| 90 |
-
if total_words + word_count > max_words:
|
| 91 |
-
# Add a trimmed version of this chunk if possible
|
| 92 |
-
remaining = max_words - total_words
|
| 93 |
-
if remaining > 30: # Only worth adding if meaningful content remains
|
| 94 |
-
trimmed_words = text.split()[:remaining]
|
| 95 |
-
parts.append(" ".join(trimmed_words))
|
| 96 |
-
break
|
| 97 |
-
parts.append(text)
|
| 98 |
-
total_words += word_count
|
| 99 |
-
return "\n\n".join(parts)
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
class LLMService:
|
| 103 |
-
def __init__(
|
| 104 |
-
self,
|
| 105 |
-
provider: str,
|
| 106 |
-
groq_api_key: str,
|
| 107 |
-
groq_model: str,
|
| 108 |
-
groq_rewrite_model: str,
|
| 109 |
-
hf_api_key: str,
|
| 110 |
-
hf_model: str,
|
| 111 |
-
timeout_s: float = 20.0,
|
| 112 |
-
) -> None:
|
| 113 |
-
self.provider = provider.lower().strip()
|
| 114 |
-
self.groq_api_key = groq_api_key
|
| 115 |
-
self.groq_model = groq_model
|
| 116 |
-
self.groq_rewrite_model = groq_rewrite_model
|
| 117 |
-
self.hf_api_key = hf_api_key
|
| 118 |
-
self.hf_model = hf_model
|
| 119 |
-
self.timeout_s = timeout_s
|
| 120 |
-
|
| 121 |
-
async def generate_multi_queries(self, query: str, history: List[Dict[str, str]]) -> List[str]:
|
| 122 |
-
"""Generates multiple search queries to capture broader context from the document."""
|
| 123 |
-
prompt = f"""You are an intelligent HR search optimizer. A user asked: "{query}"
|
| 124 |
-
|
| 125 |
-
STEP 1 — UNDERSTAND THE INTENT:
|
| 126 |
-
What is the user ACTUALLY asking about? Apply workplace common sense:
|
| 127 |
-
- "timing" / "timings" (no qualifier) = office working hours, workday schedule
|
| 128 |
-
- "leaves" / "paid leaves" / "all leaves" = all leave types available to employees
|
| 129 |
-
- "salary" / "pay" = salary structure or amount
|
| 130 |
-
- "benefits" / "allowances" = employee benefits and perks
|
| 131 |
-
- "terminate" / "resign" = termination or resignation process
|
| 132 |
-
|
| 133 |
-
STEP 2 — GENERATE 3 TARGETED SEARCH QUERIES:
|
| 134 |
-
Write 3 diverse search queries to retrieve ALL relevant information from an employee handbook.
|
| 135 |
-
For the identified intent, cover synonyms, related terms, and sub-categories:
|
| 136 |
-
- For office timings → search: working hours, office schedule, workday timing, shift hours
|
| 137 |
-
- For leaves → Query 1 MUST be a keyword dump: "casual leave sick leave annual leave maternity paternity hajj bereavement study unauthorized"
|
| 138 |
-
- For salary → cover: salary structure, payroll, compensation, increments, deductions
|
| 139 |
-
- For benefits → cover: allowances, perks, medical, bonuses, reimbursements
|
| 140 |
-
|
| 141 |
-
Output ONLY 3 queries, one per line. No numbers, no bullets, no explanations.
|
| 142 |
-
|
| 143 |
-
Queries:"""
|
| 144 |
-
|
| 145 |
-
try:
|
| 146 |
-
resp = await self._call_groq(
|
| 147 |
-
prompt, [],
|
| 148 |
-
"You output only search queries, one per line. No explanations, no numbering.",
|
| 149 |
-
model_override=self.groq_rewrite_model
|
| 150 |
-
)
|
| 151 |
-
queries = [
|
| 152 |
-
q.strip().lstrip('0123456789.-) ').strip()
|
| 153 |
-
for q in resp.split("\n")
|
| 154 |
-
if q.strip() and len(q.strip()) > 3
|
| 155 |
-
]
|
| 156 |
-
# Always include original query
|
| 157 |
-
if query not in queries:
|
| 158 |
-
queries.append(query)
|
| 159 |
-
return queries[:4]
|
| 160 |
-
except Exception:
|
| 161 |
-
return [query]
|
| 162 |
-
|
| 163 |
-
async def answer(
|
| 164 |
-
self,
|
| 165 |
-
question: str,
|
| 166 |
-
chunks: List[Dict[str, str]],
|
| 167 |
-
history: List[Dict[str, str]],
|
| 168 |
-
user_name: str = None
|
| 169 |
-
) -> str:
|
| 170 |
-
# Keep only last 4 messages (2 turns) for TPM safety
|
| 171 |
-
pruned_history = history[-4:]
|
| 172 |
-
context = _build_context(chunks)
|
| 173 |
-
|
| 174 |
-
# Build system message
|
| 175 |
-
system_msg = SYSTEM_PROMPT
|
| 176 |
-
if user_name and user_name not in ("default name", "", None):
|
| 177 |
-
system_msg += f"\n\nYou are speaking with {user_name}."
|
| 178 |
-
|
| 179 |
-
# Build user prompt — clean and direct
|
| 180 |
-
if not context:
|
| 181 |
-
user_prompt = question
|
| 182 |
-
else:
|
| 183 |
-
user_prompt = f"Expert Data:\n{context}\n\nQuestion: {question}"
|
| 184 |
-
|
| 185 |
-
return await self._call_groq(user_prompt, pruned_history, system_msg)
|
| 186 |
-
|
| 187 |
-
async def _call_groq(
|
| 188 |
-
self,
|
| 189 |
-
user_prompt: str,
|
| 190 |
-
history: List[Dict[str, str]],
|
| 191 |
-
system_msg: str,
|
| 192 |
-
model_override: str = None
|
| 193 |
-
) -> str:
|
| 194 |
-
if not self.groq_api_key:
|
| 195 |
-
return "Expert access required."
|
| 196 |
-
|
| 197 |
-
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 198 |
-
headers = {
|
| 199 |
-
"Authorization": f"Bearer {self.groq_api_key}",
|
| 200 |
-
"Content-Type": "application/json"
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
messages = [{"role": "system", "content": system_msg}]
|
| 204 |
-
for msg in history:
|
| 205 |
-
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 206 |
-
messages.append({"role": "user", "content": user_prompt})
|
| 207 |
-
|
| 208 |
-
target_model = model_override or self.groq_model
|
| 209 |
-
|
| 210 |
-
# Qwen3 and DeepSeek-R1 are thinking models
|
| 211 |
-
is_thinking_model = any(k in target_model.lower() for k in ["deepseek", "qwen3", "qwen/qwen3"])
|
| 212 |
-
temp = 0.6 if is_thinking_model else 0.0
|
| 213 |
-
|
| 214 |
-
# For Qwen3: suppress thinking mode on answer calls to reduce latency & TPM usage.
|
| 215 |
-
# /no_think is Qwen3's built-in signal to skip chain-of-thought reasoning.
|
| 216 |
-
# We ONLY suppress for the main answer model, not the rewrite model.
|
| 217 |
-
if is_thinking_model and model_override is None:
|
| 218 |
-
messages[-1]["content"] = "/no_think\n\n" + messages[-1]["content"]
|
| 219 |
-
|
| 220 |
-
payload = {
|
| 221 |
-
"model": target_model,
|
| 222 |
-
"temperature": temp,
|
| 223 |
-
"max_tokens": 512, # Enough for complete listings with HTML, well within TPM limits
|
| 224 |
-
"messages": messages,
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
async with httpx.AsyncClient(timeout=self.timeout_s) as client:
|
| 228 |
-
resp = await client.post(url, headers=headers, json=payload)
|
| 229 |
-
if resp.status_code >= 400:
|
| 230 |
-
import logging
|
| 231 |
-
logging.getLogger(__name__).error(
|
| 232 |
-
f"Groq API Error {resp.status_code}: {resp.text}"
|
| 233 |
-
)
|
| 234 |
-
resp.raise_for_status()
|
| 235 |
-
data = resp.json()
|
| 236 |
-
content = data["choices"][0]["message"]["content"].strip()
|
| 237 |
-
|
| 238 |
-
# ── Post-Processing: Strip all internal reasoning artifacts ──
|
| 239 |
-
|
| 240 |
-
# 1. Strip <think>...</think> blocks (Qwen3, DeepSeek-R1)
|
| 241 |
-
content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL).strip()
|
| 242 |
-
|
| 243 |
-
# 2. Strip leading conversational filler (single line only, not entire content)
|
| 244 |
-
content = re.sub(
|
| 245 |
-
r'^(Okay[,.]?\s*|Alright[,.]?\s*|Sure[,.]?\s*|Let\'s see[,.]?\s*|'
|
| 246 |
-
r'Based on (?:the )?(?:provided |available )?(?:data|information|context)[,.]?\s*|'
|
| 247 |
-
r'According to (?:the )?(?:provided |available )?(?:data|information)[,.]?\s*)',
|
| 248 |
-
'', content, flags=re.IGNORECASE
|
| 249 |
-
).strip()
|
| 250 |
-
|
| 251 |
-
# 3. Remove lines that are pure internal self-talk (only if they appear alone at start)
|
| 252 |
-
lines = content.split('\n')
|
| 253 |
-
filtered = []
|
| 254 |
-
for i, line in enumerate(lines):
|
| 255 |
-
is_self_talk = bool(re.match(
|
| 256 |
-
r'^\s*(I need to|I will|I should|I\'m going to|Let me|Now I|First,? I|'
|
| 257 |
-
r'I\'ll|The user is asking|The question is about)',
|
| 258 |
-
line, re.IGNORECASE
|
| 259 |
-
))
|
| 260 |
-
if not is_self_talk:
|
| 261 |
-
filtered.append(line)
|
| 262 |
-
content = '\n'.join(filtered).strip()
|
| 263 |
-
|
| 264 |
-
return content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/rag_pipeline.py
DELETED
|
@@ -1,98 +0,0 @@
|
|
| 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 |
-
# RRF scores are small (e.g. 0.016–0.033), so threshold must be very low
|
| 11 |
-
RELEVANCE_THRESHOLD = 0.01
|
| 12 |
-
# Cross-encoder logit > 0 means > 50% relevance probability
|
| 13 |
-
RERANK_THRESHOLD = 0.0
|
| 14 |
-
# If ALL chunks fail rerank threshold, fall back to this many top chunks
|
| 15 |
-
RERANK_FALLBACK_N = 2
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
class RAGPipeline:
|
| 19 |
-
def __init__(
|
| 20 |
-
self,
|
| 21 |
-
vector_store: FaissVectorStore,
|
| 22 |
-
llm_service: LLMService,
|
| 23 |
-
reranker: RerankerService,
|
| 24 |
-
top_k: int,
|
| 25 |
-
max_context_chunks: int
|
| 26 |
-
) -> None:
|
| 27 |
-
self.vector_store = vector_store
|
| 28 |
-
self.llm_service = llm_service
|
| 29 |
-
self.reranker = reranker
|
| 30 |
-
self.top_k = top_k
|
| 31 |
-
self.max_context_chunks = max_context_chunks
|
| 32 |
-
|
| 33 |
-
async def chat(self, message: str, history: List[Dict[str, str]], user_name: str = None) -> Dict[str, object]:
|
| 34 |
-
# ── Step 1: Generate multiple search queries for broad coverage ──
|
| 35 |
-
queries = await self.llm_service.generate_multi_queries(message, history)
|
| 36 |
-
logger.info("Generated %d queries for: '%s' → %s", len(queries), message[:40], queries)
|
| 37 |
-
|
| 38 |
-
# ── Step 2: Collect unique chunks across all queries ──
|
| 39 |
-
seen_ids = set()
|
| 40 |
-
all_retrieved = []
|
| 41 |
-
|
| 42 |
-
for q in queries:
|
| 43 |
-
query_chunks = self.vector_store.search(q, top_k=self.top_k)
|
| 44 |
-
for chunk in query_chunks:
|
| 45 |
-
if chunk["id"] not in seen_ids:
|
| 46 |
-
all_retrieved.append(chunk)
|
| 47 |
-
seen_ids.add(chunk["id"])
|
| 48 |
-
|
| 49 |
-
# ── Step 3: Initial relevance filter ──
|
| 50 |
-
initial_chunks = [c for c in all_retrieved if c["score"] >= RELEVANCE_THRESHOLD]
|
| 51 |
-
|
| 52 |
-
if not initial_chunks:
|
| 53 |
-
logger.info("No relevant chunks found for: '%s' — returning no-info response", message)
|
| 54 |
-
# Pass empty chunks; LLM is instructed to say "I don't have that information"
|
| 55 |
-
reply = await self.llm_service.answer(
|
| 56 |
-
question=message,
|
| 57 |
-
chunks=[],
|
| 58 |
-
history=history,
|
| 59 |
-
user_name=user_name
|
| 60 |
-
)
|
| 61 |
-
return {"reply": reply, "retrieved_chunks": []}
|
| 62 |
-
|
| 63 |
-
# ── Step 4: Deep reranking via Cross-Encoder ──
|
| 64 |
-
# Enrihc the reranker query with the LLM's expanded search terms
|
| 65 |
-
rerank_query = message
|
| 66 |
-
if len(queries) > 0 and queries[0] != message:
|
| 67 |
-
rerank_query = f"{message} {queries[0]}"
|
| 68 |
-
|
| 69 |
-
reranked_chunks = self.reranker.rerank(rerank_query, initial_chunks, top_n=self.max_context_chunks)
|
| 70 |
-
|
| 71 |
-
# Filter by rerank score threshold
|
| 72 |
-
final_chunks = [c for c in reranked_chunks if c.get("rerank_score", 0) > RERANK_THRESHOLD]
|
| 73 |
-
|
| 74 |
-
# ── Smart Fallback: if ALL chunks fail the threshold, use the top N anyway ──
|
| 75 |
-
# This prevents the bot from saying "I don't have info" when content WAS retrieved
|
| 76 |
-
# but the cross-encoder wasn't confident enough (e.g. paraphrased queries)
|
| 77 |
-
if not final_chunks and reranked_chunks:
|
| 78 |
-
final_chunks = reranked_chunks[:RERANK_FALLBACK_N]
|
| 79 |
-
logger.info(
|
| 80 |
-
"Rerank fallback activated — all chunks below threshold (best score=%.3f), using top %d",
|
| 81 |
-
reranked_chunks[0].get("rerank_score", 0),
|
| 82 |
-
len(final_chunks)
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
logger.info(
|
| 86 |
-
"Pipeline: retrieved=%d → relevance_filtered=%d → reranked=%d → final=%d (best_score=%.3f)",
|
| 87 |
-
len(all_retrieved), len(initial_chunks), len(reranked_chunks), len(final_chunks),
|
| 88 |
-
reranked_chunks[0].get("rerank_score", 0) if reranked_chunks else 0.0
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
# ── Step 5: Generate answer with top-ranked context ──
|
| 92 |
-
reply = await self.llm_service.answer(
|
| 93 |
-
question=message,
|
| 94 |
-
chunks=final_chunks,
|
| 95 |
-
history=history,
|
| 96 |
-
user_name=user_name
|
| 97 |
-
)
|
| 98 |
-
return {"reply": reply, "retrieved_chunks": final_chunks}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/rate_limiter.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/reranker.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 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]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/session_store.py
DELETED
|
@@ -1,284 +0,0 @@
|
|
| 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"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/services/vector_store.py
DELETED
|
@@ -1,167 +0,0 @@
|
|
| 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 |
-
import pickle
|
| 9 |
-
from rank_bm25 import BM25Okapi
|
| 10 |
-
|
| 11 |
-
from app.services.chunker import chunk_documents
|
| 12 |
-
from app.services.document_loader import load_documents
|
| 13 |
-
from app.services.embeddings import EmbeddingService
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
class FaissVectorStore:
|
| 17 |
-
def __init__(
|
| 18 |
-
self,
|
| 19 |
-
embedding_service: EmbeddingService,
|
| 20 |
-
docs_dir: Path,
|
| 21 |
-
index_dir: Path,
|
| 22 |
-
chunk_size_tokens: int,
|
| 23 |
-
chunk_overlap_tokens: int,
|
| 24 |
-
) -> None:
|
| 25 |
-
self.embedding_service = embedding_service
|
| 26 |
-
self.docs_dir = docs_dir
|
| 27 |
-
self.index_dir = index_dir
|
| 28 |
-
self.chunk_size_tokens = chunk_size_tokens
|
| 29 |
-
self.chunk_overlap_tokens = chunk_overlap_tokens
|
| 30 |
-
|
| 31 |
-
self.faiss_index_path = index_dir / "faiss.index"
|
| 32 |
-
self.bm25_index_path = index_dir / "bm25.pkl"
|
| 33 |
-
self.metadata_path = index_dir / "metadata.json"
|
| 34 |
-
self.state_path = index_dir / "state.json"
|
| 35 |
-
|
| 36 |
-
self.index = None
|
| 37 |
-
self.bm25 = None
|
| 38 |
-
self.metadata: List[Dict[str, str]] = []
|
| 39 |
-
self.docs_loaded = False
|
| 40 |
-
self.last_retrieved: List[Dict[str, str]] = []
|
| 41 |
-
|
| 42 |
-
def _compute_docs_fingerprint(self) -> str:
|
| 43 |
-
hasher = hashlib.sha256()
|
| 44 |
-
# Include chunk settings in fingerprint so changing them triggers re-index
|
| 45 |
-
hasher.update(str(self.chunk_size_tokens).encode("utf-8"))
|
| 46 |
-
hasher.update(str(self.chunk_overlap_tokens).encode("utf-8"))
|
| 47 |
-
|
| 48 |
-
if not self.docs_dir.exists():
|
| 49 |
-
return "no_docs"
|
| 50 |
-
for path in sorted(self.docs_dir.rglob("*")):
|
| 51 |
-
if path.is_file() and path.suffix.lower() in {".txt", ".pdf"}:
|
| 52 |
-
stat = path.stat()
|
| 53 |
-
hasher.update(str(path).encode("utf-8"))
|
| 54 |
-
hasher.update(str(stat.st_mtime_ns).encode("utf-8"))
|
| 55 |
-
hasher.update(str(stat.st_size).encode("utf-8"))
|
| 56 |
-
return hasher.hexdigest()
|
| 57 |
-
|
| 58 |
-
def _can_use_cached_index(self, fingerprint: str) -> bool:
|
| 59 |
-
if not (self.faiss_index_path.exists() and self.metadata_path.exists() and self.state_path.exists()):
|
| 60 |
-
return False
|
| 61 |
-
try:
|
| 62 |
-
state = json.loads(self.state_path.read_text(encoding="utf-8"))
|
| 63 |
-
return state.get("docs_fingerprint") == fingerprint
|
| 64 |
-
except Exception:
|
| 65 |
-
return False
|
| 66 |
-
|
| 67 |
-
def build_or_load(self) -> None:
|
| 68 |
-
self.index_dir.mkdir(parents=True, exist_ok=True)
|
| 69 |
-
fingerprint = self._compute_docs_fingerprint()
|
| 70 |
-
|
| 71 |
-
if self._can_use_cached_index(fingerprint):
|
| 72 |
-
self.index = faiss.read_index(str(self.faiss_index_path))
|
| 73 |
-
with open(self.bm25_index_path, "rb") as f:
|
| 74 |
-
self.bm25 = pickle.load(f)
|
| 75 |
-
self.metadata = json.loads(self.metadata_path.read_text(encoding="utf-8"))
|
| 76 |
-
self.docs_loaded = len(self.metadata) > 0
|
| 77 |
-
return
|
| 78 |
-
|
| 79 |
-
docs = load_documents(self.docs_dir)
|
| 80 |
-
chunks = chunk_documents(docs, self.chunk_size_tokens, self.chunk_overlap_tokens)
|
| 81 |
-
if not chunks:
|
| 82 |
-
self.index = None
|
| 83 |
-
self.metadata = []
|
| 84 |
-
self.docs_loaded = False
|
| 85 |
-
return
|
| 86 |
-
|
| 87 |
-
vectors = self.embedding_service.encode([c["text"] for c in chunks])
|
| 88 |
-
dim = vectors.shape[1]
|
| 89 |
-
index = faiss.IndexFlatIP(dim)
|
| 90 |
-
index.add(vectors)
|
| 91 |
-
|
| 92 |
-
tokenized_corpus = [c["text"].lower().split() for c in chunks]
|
| 93 |
-
bm25 = BM25Okapi(tokenized_corpus)
|
| 94 |
-
|
| 95 |
-
self.index = index
|
| 96 |
-
self.bm25 = bm25
|
| 97 |
-
self.metadata = chunks
|
| 98 |
-
self.docs_loaded = True
|
| 99 |
-
|
| 100 |
-
faiss.write_index(index, str(self.faiss_index_path))
|
| 101 |
-
with open(self.bm25_index_path, "wb") as f:
|
| 102 |
-
pickle.dump(bm25, f)
|
| 103 |
-
self.metadata_path.write_text(json.dumps(chunks, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 104 |
-
self.state_path.write_text(
|
| 105 |
-
json.dumps({"docs_fingerprint": fingerprint, "chunk_count": len(chunks)}, indent=2),
|
| 106 |
-
encoding="utf-8",
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
def search(self, query: str, top_k: int = 4) -> List[Dict[str, str]]:
|
| 110 |
-
if self.index is None or not self.metadata or self.bm25 is None:
|
| 111 |
-
self.last_retrieved = []
|
| 112 |
-
return []
|
| 113 |
-
|
| 114 |
-
# Vector Search (FAISS)
|
| 115 |
-
query_vec = self.embedding_service.encode_query(query)
|
| 116 |
-
faiss_scores, faiss_indices = self.index.search(np.asarray(query_vec, dtype=np.float32), top_k * 2)
|
| 117 |
-
|
| 118 |
-
# BM25 Search
|
| 119 |
-
tokenized_query = query.lower().split()
|
| 120 |
-
bm25_scores = self.bm25.get_scores(tokenized_query)
|
| 121 |
-
|
| 122 |
-
# Get top indices for BM25
|
| 123 |
-
bm25_top_indices = np.argsort(bm25_scores)[::-1][:top_k * 2]
|
| 124 |
-
|
| 125 |
-
# Combine using Reciprocal Rank Fusion (RRF)
|
| 126 |
-
# RRF_score = 1 / (k + rank)
|
| 127 |
-
k = 60
|
| 128 |
-
rrf_scores = {}
|
| 129 |
-
|
| 130 |
-
# Add FAISS ranks
|
| 131 |
-
for rank, idx in enumerate(faiss_indices[0]):
|
| 132 |
-
if idx < 0 or idx >= len(self.metadata):
|
| 133 |
-
continue
|
| 134 |
-
rrf_scores[idx] = rrf_scores.get(idx, 0.0) + (1.0 / (k + rank + 1))
|
| 135 |
-
|
| 136 |
-
# Add BM25 ranks
|
| 137 |
-
for rank, idx in enumerate(bm25_top_indices):
|
| 138 |
-
if idx < 0 or idx >= len(self.metadata):
|
| 139 |
-
continue
|
| 140 |
-
rrf_scores[idx] = rrf_scores.get(idx, 0.0) + (1.0 / (k + rank + 1))
|
| 141 |
-
|
| 142 |
-
# Sort by combined RRF score
|
| 143 |
-
sorted_indices = sorted(rrf_scores.keys(), key=lambda x: rrf_scores[x], reverse=True)[:top_k]
|
| 144 |
-
|
| 145 |
-
results: List[Dict[str, str]] = []
|
| 146 |
-
for idx in sorted_indices:
|
| 147 |
-
chunk = self.metadata[idx]
|
| 148 |
-
# Fetch the original FAISS score for fallback relevance checking if needed, or just use RRF score
|
| 149 |
-
# Note: RRF scores are small (e.g. 0.03), so we must adjust the threshold in rag_pipeline
|
| 150 |
-
results.append(
|
| 151 |
-
{
|
| 152 |
-
"id": chunk["id"],
|
| 153 |
-
"source": chunk["source"],
|
| 154 |
-
"text": chunk["text"],
|
| 155 |
-
"score": float(rrf_scores[idx]), # RRF score
|
| 156 |
-
}
|
| 157 |
-
)
|
| 158 |
-
self.last_retrieved = results
|
| 159 |
-
return results
|
| 160 |
-
|
| 161 |
-
def health(self) -> Dict[str, object]:
|
| 162 |
-
return {
|
| 163 |
-
"docs_loaded": self.docs_loaded,
|
| 164 |
-
"index_ready": self.index is not None,
|
| 165 |
-
"chunk_count": len(self.metadata),
|
| 166 |
-
"index_path": str(self.faiss_index_path),
|
| 167 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/app/ui_gradio.py
DELETED
|
@@ -1,275 +0,0 @@
|
|
| 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 |
-
groq_rewrite_model=settings.groq_rewrite_model,
|
| 40 |
-
hf_api_key=settings.hf_api_key,
|
| 41 |
-
hf_model=settings.hf_model,
|
| 42 |
-
timeout_s=settings.request_timeout_s,
|
| 43 |
-
)
|
| 44 |
-
reranker_service = RerankerService(settings.reranker_model)
|
| 45 |
-
pipeline = RAGPipeline(
|
| 46 |
-
vector_store=vector_store,
|
| 47 |
-
llm_service=llm_service,
|
| 48 |
-
reranker=reranker_service,
|
| 49 |
-
top_k=settings.top_k,
|
| 50 |
-
max_context_chunks=settings.max_context_chunks
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
# Ensure index is ready
|
| 54 |
-
vector_store.build_or_load()
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# ── Keyboard layout for realistic typos ──────────────────────────────
|
| 58 |
-
NEARBY_KEYS = {
|
| 59 |
-
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 60 |
-
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 61 |
-
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 62 |
-
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 63 |
-
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 64 |
-
'z': 'xas',
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
TYPO_CHANCE = 0.07 # 7% chance per word
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def _nearby_char(ch: str) -> str:
|
| 71 |
-
"""Return a plausible neighbouring key for the given character."""
|
| 72 |
-
lower = ch.lower()
|
| 73 |
-
if lower in NEARBY_KEYS:
|
| 74 |
-
replacement = random.choice(NEARBY_KEYS[lower])
|
| 75 |
-
return replacement.upper() if ch.isupper() else replacement
|
| 76 |
-
return random.choice(string.ascii_lowercase)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def _to_history(messages: List[Dict[str, str]]) -> list:
|
| 80 |
-
"""Returns the history as a list of dicts (already in this format for Gradio 5)."""
|
| 81 |
-
return messages
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
async def _chat_via_api(message: str, history: List[Dict[str, str]]) -> str:
|
| 85 |
-
payload = {"message": message, "history": history}
|
| 86 |
-
headers = {"Content-Type": "application/json"}
|
| 87 |
-
if settings.api_key:
|
| 88 |
-
headers["x-api-key"] = settings.api_key
|
| 89 |
-
|
| 90 |
-
async with httpx.AsyncClient(timeout=30.0) as client:
|
| 91 |
-
resp = await client.post(API_URL, json=payload, headers=headers)
|
| 92 |
-
if resp.status_code == 429:
|
| 93 |
-
return "⚠️ I'm receiving too many requests right now. Please wait a moment before sending another message."
|
| 94 |
-
resp.raise_for_status()
|
| 95 |
-
return resp.json()["reply"]
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
async def _get_reply(message: str, chat_history: List[Dict[str, str]]) -> str:
|
| 99 |
-
"""Fetch the full reply from the LLM (API or direct pipeline)."""
|
| 100 |
-
if API_URL:
|
| 101 |
-
return await _chat_via_api(message, chat_history)
|
| 102 |
-
result = await pipeline.chat(message=message, history=chat_history)
|
| 103 |
-
return result["reply"]
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
async def chat_fn(message: str, chat_history: List[Dict[str, str]], session_id: str = ""):
|
| 107 |
-
"""Streaming generator: yields word-by-word with human-like timing & typos."""
|
| 108 |
-
if not message or not message.strip():
|
| 109 |
-
yield gr.update(interactive=True), chat_history
|
| 110 |
-
return
|
| 111 |
-
|
| 112 |
-
original_history = chat_history.copy()
|
| 113 |
-
chat_history = chat_history + [
|
| 114 |
-
{"role": "user", "content": message},
|
| 115 |
-
{"role": "assistant", "content": ""}
|
| 116 |
-
]
|
| 117 |
-
yield gr.update(value="", interactive=False), chat_history
|
| 118 |
-
|
| 119 |
-
try:
|
| 120 |
-
reply = await _get_reply(message, original_history)
|
| 121 |
-
except httpx.HTTPStatusError as e:
|
| 122 |
-
logger.error(f"HTTP Error: {e.response.status_code} - {e.response.text}")
|
| 123 |
-
reply = "⚠️ Rate limit reached. Please slow down a bit!" if e.response.status_code == 429 \
|
| 124 |
-
else "⚠️ I encountered an error. Please try again in a few seconds."
|
| 125 |
-
except Exception as e:
|
| 126 |
-
logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
|
| 127 |
-
reply = f"⚠️ Oops! Something went wrong."
|
| 128 |
-
|
| 129 |
-
# ── Humanistic letter-by-letter typing — HTML-safe ────────────────────
|
| 130 |
-
displayed = "" # what is shown in the chat bubble
|
| 131 |
-
buffer = "" # invisible accumulator for mid-HTML-tag characters
|
| 132 |
-
in_tag = False # True while we are inside a < … > sequence
|
| 133 |
-
|
| 134 |
-
for char in reply:
|
| 135 |
-
if char == '<':
|
| 136 |
-
in_tag = True
|
| 137 |
-
buffer += char
|
| 138 |
-
continue
|
| 139 |
-
|
| 140 |
-
if in_tag:
|
| 141 |
-
buffer += char
|
| 142 |
-
if char == '>':
|
| 143 |
-
# Tag is now complete — flush it to the display all at once
|
| 144 |
-
in_tag = False
|
| 145 |
-
displayed += buffer
|
| 146 |
-
buffer = ""
|
| 147 |
-
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 148 |
-
yield gr.update(value="", interactive=False), chat_history
|
| 149 |
-
# Small pause after a <br> to mimic line-break pacing
|
| 150 |
-
if displayed.endswith('<br>') or displayed.endswith('<br/>'):
|
| 151 |
-
await asyncio.sleep(random.uniform(0.1, 0.2))
|
| 152 |
-
continue
|
| 153 |
-
|
| 154 |
-
# ── Normal character (not inside a tag) ───────────────────────────
|
| 155 |
-
# Decide typo chance only for plain alphabetic words
|
| 156 |
-
do_typo = (
|
| 157 |
-
char.isalpha()
|
| 158 |
-
and len(displayed) > 8 # skip the very beginning
|
| 159 |
-
and random.random() < TYPO_CHANCE
|
| 160 |
-
)
|
| 161 |
-
|
| 162 |
-
if do_typo:
|
| 163 |
-
wrong_char = _nearby_char(char)
|
| 164 |
-
displayed += wrong_char
|
| 165 |
-
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 166 |
-
yield gr.update(value="", interactive=False), chat_history
|
| 167 |
-
await asyncio.sleep(random.uniform(0.15, 0.3))
|
| 168 |
-
|
| 169 |
-
# Backspace the wrong character
|
| 170 |
-
displayed = displayed[:-1]
|
| 171 |
-
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 172 |
-
yield gr.update(value="", interactive=False), chat_history
|
| 173 |
-
await asyncio.sleep(random.uniform(0.1, 0.2))
|
| 174 |
-
|
| 175 |
-
# Type the correct character
|
| 176 |
-
displayed += char
|
| 177 |
-
chat_history[-1] = {"role": "assistant", "content": displayed}
|
| 178 |
-
yield gr.update(value="", interactive=False), chat_history
|
| 179 |
-
|
| 180 |
-
# Pacing: punctuation pauses, space micro-pause, normal char speed
|
| 181 |
-
if char in '.!?':
|
| 182 |
-
await asyncio.sleep(random.uniform(0.35, 0.7))
|
| 183 |
-
elif char in ',:;':
|
| 184 |
-
await asyncio.sleep(random.uniform(0.15, 0.3))
|
| 185 |
-
elif char == ' ':
|
| 186 |
-
await asyncio.sleep(random.uniform(0.06, 0.13))
|
| 187 |
-
else:
|
| 188 |
-
await asyncio.sleep(random.uniform(0.02, 0.06))
|
| 189 |
-
|
| 190 |
-
# ── Fire-and-forget: persist to session store (never blocks the UI) ───
|
| 191 |
-
# We only save successful responses, not error messages
|
| 192 |
-
if session_id and not displayed.startswith("⚠️"):
|
| 193 |
-
asyncio.create_task(_ss.save_message(session_id, message, displayed))
|
| 194 |
-
|
| 195 |
-
# Re-enable the input box at the end
|
| 196 |
-
yield gr.update(interactive=True), chat_history
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
custom_css = """
|
| 200 |
-
/* Aggressively force height: auto and min-height: 40px on the chatbot and flex containers */
|
| 201 |
-
.gradio-container, .flex, .block, #chatbot-window {
|
| 202 |
-
height: auto !important;
|
| 203 |
-
min-height: 40px !important;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
/* Ensure the chatbot doesn't overflow the viewport if it gets too full */
|
| 207 |
-
#chatbot-window {
|
| 208 |
-
max-height: 70vh !important;
|
| 209 |
-
border: none !important;
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
-
/* Aggressively hide the footer, including HF injected footers if inside the container */
|
| 213 |
-
footer, .footer, footer * {
|
| 214 |
-
display: none !important;
|
| 215 |
-
visibility: hidden !important;
|
| 216 |
-
height: 0 !important;
|
| 217 |
-
margin: 0 !important;
|
| 218 |
-
padding: 0 !important;
|
| 219 |
-
}
|
| 220 |
-
"""
|
| 221 |
-
|
| 222 |
-
# JS: runs immediately on page load — generates/loads session_id from localStorage
|
| 223 |
-
# and restores previous chat history. Completely non-blocking.
|
| 224 |
-
_SESSION_JS = """
|
| 225 |
-
async () => {
|
| 226 |
-
// ── Session ID: generate once, persist forever in localStorage ──
|
| 227 |
-
let sid = localStorage.getItem('martech_session_id');
|
| 228 |
-
if (!sid) {
|
| 229 |
-
sid = 'sess-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 230 |
-
localStorage.setItem('martech_session_id', sid);
|
| 231 |
-
}
|
| 232 |
-
// Write the session_id into the hidden Gradio textbox
|
| 233 |
-
const el = document.getElementById('session-id-box')?.querySelector('textarea');
|
| 234 |
-
if (el) { el.value = sid; el.dispatchEvent(new Event('input', {bubbles:true})); }
|
| 235 |
-
return sid;
|
| 236 |
-
}
|
| 237 |
-
"""
|
| 238 |
-
|
| 239 |
-
with gr.Blocks(title="Martechsol Assistant", theme=gr.themes.Soft(), css=custom_css) as demo:
|
| 240 |
-
gr.Markdown("# Martechsol Assistant")
|
| 241 |
-
gr.Markdown("Welcome! How can I help you today?")
|
| 242 |
-
|
| 243 |
-
chatbot = gr.Chatbot(
|
| 244 |
-
show_label=False,
|
| 245 |
-
elem_id="chatbot-window",
|
| 246 |
-
render_markdown=True,
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/docker-compose.yml
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/docs/MartechSol_Employee_HandBook (7) (1).pdf
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:a48b14cc28d7892ae03a5dfe06a244832e2807ce178ca1f1acdbc58d6af4e8ef
|
| 3 |
-
size 3844502
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/docs/MartechSol_Employee_Handbook_Extracted.txt
DELETED
|
@@ -1,1091 +0,0 @@
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/floaitng-icon/KhloeSAC.lottie
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:532a144eeea5a1a6e49ab22ae4c0dc4f94e12cf8540e3514f1bc676d929eeac2
|
| 3 |
-
size 462684
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/get_models.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
-
import os
|
| 3 |
-
import httpx
|
| 4 |
-
from dotenv import load_dotenv
|
| 5 |
-
|
| 6 |
-
load_dotenv()
|
| 7 |
-
|
| 8 |
-
async def main():
|
| 9 |
-
api_key = os.getenv("GROQ_API_KEY")
|
| 10 |
-
if not api_key:
|
| 11 |
-
print("No API Key")
|
| 12 |
-
return
|
| 13 |
-
url = "https://api.groq.com/openai/v1/models"
|
| 14 |
-
headers = {"Authorization": f"Bearer {api_key}"}
|
| 15 |
-
async with httpx.AsyncClient() as client:
|
| 16 |
-
resp = await client.get(url, headers=headers)
|
| 17 |
-
if resp.status_code == 200:
|
| 18 |
-
models = resp.json().get("data", [])
|
| 19 |
-
for m in models:
|
| 20 |
-
if "deepseek" in m["id"].lower() or "qwen" in m["id"].lower():
|
| 21 |
-
print(m["id"])
|
| 22 |
-
else:
|
| 23 |
-
print(resp.status_code, resp.text)
|
| 24 |
-
|
| 25 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/martech_sol_logo.jpg
DELETED
|
Binary file (6.68 kB)
|
|
|
backup_2026-05-04/requirements.txt
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 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>=5.0.0
|
| 11 |
-
python-multipart
|
| 12 |
-
pandas
|
| 13 |
-
openpyxl
|
| 14 |
-
rank_bm25
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/static/addon.html
DELETED
|
@@ -1,543 +0,0 @@
|
|
| 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 |
-
/* Surgical Bolding & List Style */
|
| 142 |
-
.chat-message b {
|
| 143 |
-
font-weight: 800;
|
| 144 |
-
color: #0f172a;
|
| 145 |
-
}
|
| 146 |
-
.chat-message.user b {
|
| 147 |
-
color: #ffffff;
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
#chat-input-area {
|
| 151 |
-
padding: 16px;
|
| 152 |
-
background: #ffffff;
|
| 153 |
-
border-top: 1px solid #f1f5f9;
|
| 154 |
-
display: flex;
|
| 155 |
-
gap: 8px;
|
| 156 |
-
align-items: center;
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
#chat-input {
|
| 160 |
-
flex: 1;
|
| 161 |
-
border: 1px solid #e2e8f0;
|
| 162 |
-
border-radius: 20px;
|
| 163 |
-
padding: 12px 16px;
|
| 164 |
-
font-size: 14px;
|
| 165 |
-
outline: none;
|
| 166 |
-
transition: border-color 0.2s;
|
| 167 |
-
font-family: inherit;
|
| 168 |
-
box-shadow: inset 0 1px 2px rgba(0,0,0,0.02);
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
#chat-input:focus {
|
| 172 |
-
border-color: #10b981;
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
#chat-input:disabled {
|
| 176 |
-
background: #f1f5f9;
|
| 177 |
-
cursor: not-allowed;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
#chat-send {
|
| 181 |
-
background: #10b981;
|
| 182 |
-
color: white;
|
| 183 |
-
border: none;
|
| 184 |
-
width: 44px;
|
| 185 |
-
height: 44px;
|
| 186 |
-
border-radius: 50%;
|
| 187 |
-
display: flex;
|
| 188 |
-
align-items: center;
|
| 189 |
-
justify-content: center;
|
| 190 |
-
cursor: pointer;
|
| 191 |
-
transition: background 0.2s, transform 0.1s;
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
#chat-send:hover {
|
| 195 |
-
background: #059669;
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
#chat-send:active {
|
| 199 |
-
transform: scale(0.95);
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
#chat-send:disabled {
|
| 203 |
-
background: #cbd5e1;
|
| 204 |
-
cursor: not-allowed;
|
| 205 |
-
transform: none;
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
/* Loading / Typing Animation */
|
| 209 |
-
.typing-indicator {
|
| 210 |
-
display: flex;
|
| 211 |
-
gap: 4px;
|
| 212 |
-
padding: 4px 2px;
|
| 213 |
-
align-items: center;
|
| 214 |
-
height: 18px;
|
| 215 |
-
}
|
| 216 |
-
.typing-indicator span {
|
| 217 |
-
width: 6px;
|
| 218 |
-
height: 6px;
|
| 219 |
-
background: #94a3b8;
|
| 220 |
-
border-radius: 50%;
|
| 221 |
-
animation: typing-dots 1.4s infinite ease-in-out both;
|
| 222 |
-
}
|
| 223 |
-
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
| 224 |
-
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
| 225 |
-
@keyframes typing-dots {
|
| 226 |
-
0%, 80%, 100% { transform: scale(0); }
|
| 227 |
-
40% { transform: scale(1); }
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
@media (max-width: 480px) {
|
| 231 |
-
#martech-chat-box {
|
| 232 |
-
right: 15px;
|
| 233 |
-
bottom: 100px;
|
| 234 |
-
width: calc(100vw - 30px);
|
| 235 |
-
height: 70vh;
|
| 236 |
-
}
|
| 237 |
-
}
|
| 238 |
-
</style>
|
| 239 |
-
</head>
|
| 240 |
-
<body>
|
| 241 |
-
|
| 242 |
-
<!-- Lottie Player Script -->
|
| 243 |
-
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
|
| 244 |
-
|
| 245 |
-
<div id="martech-chat-wrapper">
|
| 246 |
-
<div id="martech-chat-fab">
|
| 247 |
-
<!-- Animated Chatbot Icon -->
|
| 248 |
-
<div id="chat-icon" style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
|
| 249 |
-
<dotlottie-player
|
| 250 |
-
src="https://joedown11-chatrag.hf.space/static/bot-icon.lottie"
|
| 251 |
-
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop autoplay>
|
| 252 |
-
</dotlottie-player>
|
| 253 |
-
</div>
|
| 254 |
-
<!-- Close Icon -->
|
| 255 |
-
<svg id="close-icon" viewBox="0 0 24 24" style="display:none; width: 60px; height: 60px;">
|
| 256 |
-
<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" />
|
| 257 |
-
</svg>
|
| 258 |
-
</div>
|
| 259 |
-
|
| 260 |
-
<div id="martech-chat-box">
|
| 261 |
-
<div id="martech-chat-header">
|
| 262 |
-
<div class="dot"></div>
|
| 263 |
-
<span>Martechsol Assistant</span>
|
| 264 |
-
</div>
|
| 265 |
-
|
| 266 |
-
<!-- Custom Chat UI replacing iframe -->
|
| 267 |
-
<div id="chat-messages">
|
| 268 |
-
<div class="chat-message bot">Welcome! How can I help you today?</div>
|
| 269 |
-
</div>
|
| 270 |
-
|
| 271 |
-
<div id="chat-input-area">
|
| 272 |
-
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
|
| 273 |
-
<button id="chat-send" title="Send">
|
| 274 |
-
<!-- Send Icon -->
|
| 275 |
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 276 |
-
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 277 |
-
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 278 |
-
</svg>
|
| 279 |
-
</button>
|
| 280 |
-
</div>
|
| 281 |
-
</div>
|
| 282 |
-
</div>
|
| 283 |
-
|
| 284 |
-
<script>
|
| 285 |
-
(function () {
|
| 286 |
-
// UI Elements
|
| 287 |
-
const fab = document.getElementById('martech-chat-fab');
|
| 288 |
-
const box = document.getElementById('martech-chat-box');
|
| 289 |
-
const chatIcon = document.getElementById('chat-icon');
|
| 290 |
-
const closeIcon = document.getElementById('close-icon');
|
| 291 |
-
const messagesContainer = document.getElementById('chat-messages');
|
| 292 |
-
const inputField = document.getElementById('chat-input');
|
| 293 |
-
const sendButton = document.getElementById('chat-send');
|
| 294 |
-
|
| 295 |
-
// FAB Toggle Logic
|
| 296 |
-
fab.addEventListener('click', function () {
|
| 297 |
-
const isVisible = box.classList.contains('is-visible');
|
| 298 |
-
if (isVisible) {
|
| 299 |
-
box.classList.remove('is-visible');
|
| 300 |
-
setTimeout(() => { box.style.display = 'none'; }, 300);
|
| 301 |
-
chatIcon.style.display = 'block';
|
| 302 |
-
closeIcon.style.display = 'none';
|
| 303 |
-
fab.classList.remove('is-active');
|
| 304 |
-
} else {
|
| 305 |
-
box.style.display = 'flex';
|
| 306 |
-
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
|
| 307 |
-
chatIcon.style.display = 'none';
|
| 308 |
-
closeIcon.style.display = 'block';
|
| 309 |
-
fab.classList.add('is-active');
|
| 310 |
-
}
|
| 311 |
-
});
|
| 312 |
-
|
| 313 |
-
// Chat Logic
|
| 314 |
-
const API_URL = "https://joedown11-chatrag.hf.space/api/chat";
|
| 315 |
-
const BASE_URL = API_URL.replace("/api/chat", "");
|
| 316 |
-
let chatHistory = [];
|
| 317 |
-
|
| 318 |
-
// ── Session ID: generate once, persist in localStorage ──
|
| 319 |
-
let sessionId = localStorage.getItem('martech_session_id');
|
| 320 |
-
if (!sessionId) {
|
| 321 |
-
// Use a temporary name/id until we get the real one
|
| 322 |
-
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 323 |
-
localStorage.setItem('martech_session_id', sessionId);
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
function formatMessage(text) {
|
| 327 |
-
if (!text) return "";
|
| 328 |
-
// 1. Convert Markdown Bold to HTML Bold (safety)
|
| 329 |
-
let formatted = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
| 330 |
-
// 2. Remove the '*' at the start of lines and use a bullet
|
| 331 |
-
formatted = formatted.replace(/^\* /gm, '• ');
|
| 332 |
-
// 3. Newlines to BR
|
| 333 |
-
return formatted.replace(/\n/g, '<br>');
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
function appendMessage(role, content) {
|
| 337 |
-
const div = document.createElement('div');
|
| 338 |
-
div.className = `chat-message ${role}`;
|
| 339 |
-
div.innerHTML = formatMessage(content);
|
| 340 |
-
messagesContainer.appendChild(div);
|
| 341 |
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 342 |
-
return div;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
// ── User Integration & History ──────────────────────────────────────────
|
| 346 |
-
async function initUserSession() {
|
| 347 |
-
// 1. Discover User Name immediately (0.2s)
|
| 348 |
-
let userName = null;
|
| 349 |
-
try {
|
| 350 |
-
const storageKeys = ['userInfo', 'user', 'profile', 'auth', 'currentUser', 'account'];
|
| 351 |
-
for (const key of storageKeys) {
|
| 352 |
-
const stored = localStorage.getItem(key) || sessionStorage.getItem(key);
|
| 353 |
-
if (stored) {
|
| 354 |
-
try {
|
| 355 |
-
const parsed = JSON.parse(stored);
|
| 356 |
-
userName = parsed.name || parsed.fullName || (parsed.user && parsed.user.name) || parsed.display_name;
|
| 357 |
-
if (userName) break;
|
| 358 |
-
} catch(err) {
|
| 359 |
-
if (typeof stored === 'string' && stored.length < 50 && !stored.includes('{')) {
|
| 360 |
-
userName = stored; break;
|
| 361 |
-
}
|
| 362 |
-
}
|
| 363 |
-
}
|
| 364 |
-
}
|
| 365 |
-
} catch (e) {}
|
| 366 |
-
|
| 367 |
-
if (!userName) {
|
| 368 |
-
try {
|
| 369 |
-
const pResp = await fetch("https://hrmmartechsol-ba71e8dc3fa2.herokuapp.com/api/users/profile", {
|
| 370 |
-
credentials: 'include'
|
| 371 |
-
});
|
| 372 |
-
if (pResp.ok) {
|
| 373 |
-
const pData = await pResp.json();
|
| 374 |
-
userName = pData.name || (pData.personalDetails && pData.personalDetails.fullName);
|
| 375 |
-
}
|
| 376 |
-
} catch (e) {}
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
// Multi-user logic: if name changed, clear old session and start fresh
|
| 380 |
-
const lastKnownName = localStorage.getItem('martech_last_user_name');
|
| 381 |
-
if (userName && lastKnownName && userName !== lastKnownName) {
|
| 382 |
-
localStorage.removeItem('martech_session_id');
|
| 383 |
-
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
| 384 |
-
localStorage.setItem('martech_session_id', sessionId);
|
| 385 |
-
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
|
| 386 |
-
}
|
| 387 |
-
if (userName) {
|
| 388 |
-
localStorage.setItem('martech_last_user_name', userName);
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
// 2. Fetch history for this session
|
| 392 |
-
try {
|
| 393 |
-
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
|
| 394 |
-
if (hResp.ok) {
|
| 395 |
-
const hData = await hResp.json();
|
| 396 |
-
if (hData.history && hData.history.length > 0) {
|
| 397 |
-
messagesContainer.innerHTML = '';
|
| 398 |
-
chatHistory = hData.history;
|
| 399 |
-
chatHistory.forEach(m => {
|
| 400 |
-
appendMessage(m.role === 'assistant' ? 'bot' : 'user', m.content);
|
| 401 |
-
});
|
| 402 |
-
}
|
| 403 |
-
}
|
| 404 |
-
} catch (e) {}
|
| 405 |
-
|
| 406 |
-
// 3. Sync name with backend
|
| 407 |
-
if (userName) {
|
| 408 |
-
await fetch(BASE_URL + "/api/session/update-name", {
|
| 409 |
-
method: 'POST',
|
| 410 |
-
headers: { 'Content-Type': 'application/json' },
|
| 411 |
-
body: JSON.stringify({ session_id: sessionId, user_name: userName })
|
| 412 |
-
}).then(r => r.json()).then(rData => {
|
| 413 |
-
if (rData.new_session_id && rData.new_session_id !== sessionId) {
|
| 414 |
-
sessionId = rData.new_session_id;
|
| 415 |
-
localStorage.setItem('martech_session_id', sessionId);
|
| 416 |
-
}
|
| 417 |
-
}).catch(() => {});
|
| 418 |
-
}
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
initUserSession();
|
| 422 |
-
|
| 423 |
-
function showTypingIndicator() {
|
| 424 |
-
const div = document.createElement('div');
|
| 425 |
-
div.className = 'chat-message bot';
|
| 426 |
-
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
| 427 |
-
messagesContainer.appendChild(div);
|
| 428 |
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 429 |
-
return div;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
// Typo Configuration for Human-like Typing
|
| 433 |
-
const NEARBY_KEYS = {
|
| 434 |
-
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
|
| 435 |
-
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
|
| 436 |
-
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
|
| 437 |
-
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
| 438 |
-
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
|
| 439 |
-
'z': 'xas',
|
| 440 |
-
};
|
| 441 |
-
const TYPO_CHANCE = 0.07;
|
| 442 |
-
|
| 443 |
-
function getNearbyChar(ch) {
|
| 444 |
-
const lower = ch.toLowerCase();
|
| 445 |
-
if (NEARBY_KEYS[lower]) {
|
| 446 |
-
const options = NEARBY_KEYS[lower];
|
| 447 |
-
const replacement = options.charAt(Math.floor(Math.random() * options.length));
|
| 448 |
-
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
|
| 449 |
-
}
|
| 450 |
-
const abc = "abcdefghijklmnopqrstuvwxyz";
|
| 451 |
-
return abc.charAt(Math.floor(Math.random() * abc.length));
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
| 455 |
-
|
| 456 |
-
async function typeMessageWithTypos(element, text) {
|
| 457 |
-
const fullHtml = formatMessage(text);
|
| 458 |
-
|
| 459 |
-
// Simple HTML-aware typing: split by tags
|
| 460 |
-
const tokens = fullHtml.split(/(<[^>]*>)/);
|
| 461 |
-
let currentHtml = "";
|
| 462 |
-
|
| 463 |
-
for (const token of tokens) {
|
| 464 |
-
if (token.startsWith("<")) {
|
| 465 |
-
// Instantly add tags
|
| 466 |
-
currentHtml += token;
|
| 467 |
-
element.innerHTML = currentHtml;
|
| 468 |
-
} else {
|
| 469 |
-
// Type text content char by char
|
| 470 |
-
for (let char of token) {
|
| 471 |
-
currentHtml += char;
|
| 472 |
-
element.innerHTML = currentHtml;
|
| 473 |
-
await sleep(Math.random() * 15 + 5);
|
| 474 |
-
}
|
| 475 |
-
}
|
| 476 |
-
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
| 477 |
-
}
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
async function handleSend() {
|
| 481 |
-
const text = inputField.value.trim();
|
| 482 |
-
if (!text) return;
|
| 483 |
-
|
| 484 |
-
// UI state lock
|
| 485 |
-
inputField.value = '';
|
| 486 |
-
inputField.disabled = true;
|
| 487 |
-
sendButton.disabled = true;
|
| 488 |
-
|
| 489 |
-
appendMessage('user', text);
|
| 490 |
-
const typingEl = showTypingIndicator();
|
| 491 |
-
|
| 492 |
-
try {
|
| 493 |
-
// Call the FastAPI Backend directly
|
| 494 |
-
const response = await fetch(API_URL, {
|
| 495 |
-
method: 'POST',
|
| 496 |
-
headers: { 'Content-Type': 'application/json' },
|
| 497 |
-
body: JSON.stringify({
|
| 498 |
-
message: text,
|
| 499 |
-
history: chatHistory,
|
| 500 |
-
session_id: sessionId
|
| 501 |
-
})
|
| 502 |
-
});
|
| 503 |
-
|
| 504 |
-
if (!response.ok) {
|
| 505 |
-
if (response.status === 429) throw new Error("Rate limit exceeded.");
|
| 506 |
-
throw new Error("Server error.");
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
const data = await response.json();
|
| 510 |
-
const reply = data.reply;
|
| 511 |
-
|
| 512 |
-
typingEl.remove();
|
| 513 |
-
const botEl = appendMessage('bot', '');
|
| 514 |
-
|
| 515 |
-
// Update local memory
|
| 516 |
-
chatHistory.push({ role: "user", content: text });
|
| 517 |
-
chatHistory.push({ role: "assistant", content: reply });
|
| 518 |
-
|
| 519 |
-
// Stream text out
|
| 520 |
-
await typeMessageWithTypos(botEl, reply);
|
| 521 |
-
|
| 522 |
-
} catch (error) {
|
| 523 |
-
console.error(error);
|
| 524 |
-
typingEl.remove();
|
| 525 |
-
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
|
| 526 |
-
} finally {
|
| 527 |
-
inputField.disabled = false;
|
| 528 |
-
sendButton.disabled = false;
|
| 529 |
-
inputField.focus();
|
| 530 |
-
}
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
// Event Listeners
|
| 534 |
-
sendButton.addEventListener('click', handleSend);
|
| 535 |
-
inputField.addEventListener('keypress', function(e) {
|
| 536 |
-
if (e.key === 'Enter') handleSend();
|
| 537 |
-
});
|
| 538 |
-
|
| 539 |
-
})();
|
| 540 |
-
</script>
|
| 541 |
-
|
| 542 |
-
</body>
|
| 543 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/static/bot-icon.lottie
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:532a144eeea5a1a6e49ab22ae4c0dc4f94e12cf8540e3514f1bc676d929eeac2
|
| 3 |
-
size 462684
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/static/chat-loader.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 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 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/test_groq.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
-
import os
|
| 3 |
-
import httpx
|
| 4 |
-
from dotenv import load_dotenv
|
| 5 |
-
|
| 6 |
-
load_dotenv()
|
| 7 |
-
|
| 8 |
-
async def main():
|
| 9 |
-
api_key = os.getenv("GROQ_API_KEY")
|
| 10 |
-
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 11 |
-
headers = {
|
| 12 |
-
"Authorization": f"Bearer {api_key}",
|
| 13 |
-
"Content-Type": "application/json"
|
| 14 |
-
}
|
| 15 |
-
payload = {
|
| 16 |
-
"model": "deepseek-r1-distill-llama-70b",
|
| 17 |
-
"temperature": 0.0,
|
| 18 |
-
"max_tokens": 1200,
|
| 19 |
-
"messages": [{"role": "system", "content": "hello"}, {"role": "user", "content": "test"}]
|
| 20 |
-
}
|
| 21 |
-
async with httpx.AsyncClient() as client:
|
| 22 |
-
resp = await client.post(url, headers=headers, json=payload)
|
| 23 |
-
print(resp.status_code)
|
| 24 |
-
print(resp.text)
|
| 25 |
-
|
| 26 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/wordpress code/WP_INTEGRATION_SCRIPT.txt
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/wordpress code/addon.html
DELETED
|
@@ -1,477 +0,0 @@
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-04/wordpress code/addon_backup.html
DELETED
|
@@ -1,175 +0,0 @@
|
|
| 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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-13_pre_intelligent_retrieval/.env.example
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
LLM_PROVIDER=fireworks
|
| 2 |
-
FIREWORKS_API_KEY=your_fireworks_api_key
|
| 3 |
-
FIREWORKS_MODEL=accounts/fireworks/models/qwen3-32b
|
| 4 |
-
FIREWORKS_REWRITE_MODEL=accounts/fireworks/models/llama-v3p1-8b-instruct
|
| 5 |
-
GROQ_API_KEY=your_groq_api_key
|
| 6 |
-
GROQ_MODEL=qwen/qwen3-32b
|
| 7 |
-
GROQ_REWRITE_MODEL=llama-3.1-8b-instant
|
| 8 |
-
HF_API_KEY=your_huggingface_api_key
|
| 9 |
-
HF_MODEL=meta-llama/Llama-3.1-8B-Instruct
|
| 10 |
-
EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
|
| 11 |
-
DOCS_DIR=docs
|
| 12 |
-
INDEX_DIR=data/index
|
| 13 |
-
SESSIONS_DIR=data/sessions
|
| 14 |
-
TOP_K=4
|
| 15 |
-
CORS_ALLOW_ORIGINS=*
|
| 16 |
-
API_KEY=
|
| 17 |
-
RATE_LIMIT_REQUESTS=60
|
| 18 |
-
RATE_LIMIT_WINDOW_SECONDS=60
|
| 19 |
-
RAG_API_URL=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backup_2026-05-13_pre_intelligent_retrieval/.gitattributes
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
*.lottie filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.pdf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
backup_2026-05-13_pre_intelligent_retrieval/.gitignore
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
.env
|
| 2 |
-
__pycache__/
|
| 3 |
-
.pytest_cache/
|
| 4 |
-
.mypy_cache/
|
| 5 |
-
.venv/
|
| 6 |
-
venv/
|
| 7 |
-
# Ignoring data directory to prevent pushing binary index files
|
| 8 |
-
data/index/
|
| 9 |
-
data/sessions/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|