Spaces:
Runtime error
Runtime error
Commit ·
6256536
0
Parent(s):
Initial clean deploy without binary files
Browse files- .gitignore +12 -0
- Dockerfile +6 -0
- LICENSE +21 -0
- README.md +219 -0
- app/LLM/LLM.py +12 -0
- app/Prompt/Prompt.py +23 -0
- app/VectorStores/Vectorstores.py +14 -0
- app/chain.py +51 -0
- app/config/config.py +9 -0
- app/embeddings/embedding.py +16 -0
- app/index.py +51 -0
- app/ingest.py +52 -0
- app/requirements.txt +9 -0
- app/static/atomcamp_logo.png +0 -0
- app/static/css/style.css +253 -0
- app/static/js/chat.js +98 -0
- app/static/templates/index.html +39 -0
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# API Keys and Secrets
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python Environment
|
| 5 |
+
botenv/
|
| 6 |
+
venv/
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
|
| 10 |
+
# OS files
|
| 11 |
+
.DS_Store
|
| 12 |
+
Thumbs.db
|
Dockerfile
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9
|
| 2 |
+
WORKDIR /code
|
| 3 |
+
COPY ./app/requirements.txt /code/requirements.txt
|
| 4 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 5 |
+
COPY . .
|
| 6 |
+
CMD ["uvicorn", "app.index:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Ali Abdullah
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# <img src="app/static/atomcamp_logo.png" alt="atomcamp logo" height="40" align="center"> RAG Chatbot
|
| 2 |
+
|
| 3 |
+
This is a Retrieval-Augmented Generation (RAG) chatbot I built to serve as a factual representative for **atomcamp** programs, courses, and admissions. Instead of relying on general AI knowledge, this system uses a **FastAPI** backend and **LangChain** to retrieve verified data from a **Qdrant** vector database, ensuring every response from the **GPT-OSS-120B** model is grounded in actual company information. By processing website data through **Hugging Face** embeddings and using a **Maximal Marginal Relevance (MMR)** retrieval strategy, the bot provides accurate, non-robotic answers without the "hallucinations" typical of standard AI.
|
| 4 |
+
|
| 5 |
+
## Table of Contents
|
| 6 |
+
|
| 7 |
+
1. Project Overview
|
| 8 |
+
2. Interface Preview
|
| 9 |
+
3. Core Technical Features
|
| 10 |
+
4. System Architecture
|
| 11 |
+
5. Tech Stack Specifications
|
| 12 |
+
6. Installation and Environment Setup
|
| 13 |
+
7. Configuration and API Integration
|
| 14 |
+
8. Project Directory Structure
|
| 15 |
+
9. Execution Workflow
|
| 16 |
+
10. Key Component Breakdown
|
| 17 |
+
11. Advanced UI/UX Implementation
|
| 18 |
+
12. Status and Versioning
|
| 19 |
+
|
| 20 |
+
-----
|
| 21 |
+
|
| 22 |
+
## Project Overview
|
| 23 |
+
|
| 24 |
+
The primary objective of this project is to eliminate the "robotic" nature of traditional AI assistants. By implementing a sophisticated RAG pipeline, the system grounds every response in verified atomcamp data. The architecture is designed for low latency, high relevance, and a professional user experience through a bespoke web interface that supports dynamic theme switching and responsive data rendering.
|
| 25 |
+
|
| 26 |
+
-----
|
| 27 |
+
|
| 28 |
+
## Interface Preview
|
| 29 |
+
|
| 30 |
+
The platform provides a professional-grade interface with a custom-built theme toggle to support various user environments.
|
| 31 |
+
|
| 32 |
+
| Light Mode Interface | Dark Mode Interface |
|
| 33 |
+
| :--- | :--- |
|
| 34 |
+
|  |  |
|
| 35 |
+
|
| 36 |
+
-----
|
| 37 |
+
|
| 38 |
+
## Core Technical Features
|
| 39 |
+
|
| 40 |
+
* Official Representative Persona: The system is programmed with a custom prompt that forces the LLM to "own" the knowledge, speaking as an authoritative human representative rather than a machine reading a file.
|
| 41 |
+
* High-Speed Inference: Powered by the Groq LPU (Language Processing Unit) engine using the GPT-OSS-120B model for near-instantaneous responses.
|
| 42 |
+
* Cloud-Native Vector Search: Utilizes a managed Qdrant collection for high-dimensional semantic search and retrieval.
|
| 43 |
+
* Maximal Marginal Relevance (MMR): A specialized retrieval strategy that balances document relevance with information diversity to provide more comprehensive answers.
|
| 44 |
+
* Dynamic Dark Mode: A fully integrated CSS variable system that swaps entire color palettes, including high-contrast link colors (Orange in Dark Mode) for maximum accessibility.
|
| 45 |
+
* Auto-Expanding Interface: The input section utilizes an intelligent vertical-growth textarea that expands as the user types complex inquiries.
|
| 46 |
+
* Markdown Integration: Full support for professional text formatting, including bold terms and standard bulleted lists.
|
| 47 |
+
|
| 48 |
+
-----
|
| 49 |
+
|
| 50 |
+
## System Architecture
|
| 51 |
+
|
| 52 |
+
### The RAG Pipeline
|
| 53 |
+
|
| 54 |
+
1. Data Ingestion: The system crawls the official atomcamp web domain, extracts core content using BeautifulSoup4, and splits it into semantic chunks.
|
| 55 |
+
2. Embedding Generation: Chunks are converted into 768-dimensional vectors using the Hugging Face all-mpnet-base-v2 model.
|
| 56 |
+
3. Vector Storage: Vectors are stored in a Qdrant Cloud collection with full metadata support.
|
| 57 |
+
4. User Query: The user submits a question through the FastAPI web interface.
|
| 58 |
+
5. Semantic Retrieval: The system performs a similarity search in Qdrant, retrieving the top contexts while applying MMR to reduce redundancy.
|
| 59 |
+
6. Contextual Synthesis: The LLM processes the retrieved context, user question, and conversation history to generate a natural, authoritative response.
|
| 60 |
+
|
| 61 |
+
-----
|
| 62 |
+
|
| 63 |
+
## Tech Stack Specifications
|
| 64 |
+
|
| 65 |
+
### Backend and Orchestration
|
| 66 |
+
|
| 67 |
+
* Framework: FastAPI (v0.105.0) for high-performance API routing.
|
| 68 |
+
* Orchestrator: LangChain (v0.3.0) for managing the RAG chain and memory.
|
| 69 |
+
* Web Server: Uvicorn (v0.34.0) for asynchronous execution.
|
| 70 |
+
|
| 71 |
+
### Artificial Intelligence
|
| 72 |
+
|
| 73 |
+
* Inference Engine: Groq.
|
| 74 |
+
* Model: openai/gpt-oss-120b.
|
| 75 |
+
* Embeddings: sentence-transformers/all-mpnet-base-v2 via Hugging Face.
|
| 76 |
+
|
| 77 |
+
### Data and Storage
|
| 78 |
+
|
| 79 |
+
* Vector DB: Qdrant Cloud.
|
| 80 |
+
* Web Scraping: BeautifulSoup4 and WebBaseLoader.
|
| 81 |
+
|
| 82 |
+
-----
|
| 83 |
+
|
| 84 |
+
## Installation and Environment Setup
|
| 85 |
+
|
| 86 |
+
### 1\. Repository Initialization
|
| 87 |
+
|
| 88 |
+
Clone the project and enter the application directory:
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
git clone <your-repository-url>
|
| 92 |
+
cd RAG-Based-Chatbot-main/app
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### 2\. Virtual Environment Creation
|
| 96 |
+
|
| 97 |
+
It is highly recommended to use a virtual environment to manage dependencies:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
python -m venv botenv
|
| 101 |
+
# On Windows:
|
| 102 |
+
botenv\Scripts\activate
|
| 103 |
+
# On macOS/Linux:
|
| 104 |
+
source botenv/bin/activate
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### 3\. Dependency Installation
|
| 108 |
+
|
| 109 |
+
Install the required packages listed in the requirements file:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
pip install -r requirements.txt
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
-----
|
| 116 |
+
|
| 117 |
+
## Configuration and API Integration
|
| 118 |
+
|
| 119 |
+
The system requires an environment file to manage secure credentials. Create a file named `.env` in the `app/` directory:
|
| 120 |
+
|
| 121 |
+
```env
|
| 122 |
+
# Hugging Face Access Token
|
| 123 |
+
HF_TOKEN=your_huggingface_token
|
| 124 |
+
|
| 125 |
+
# Groq Cloud API Key
|
| 126 |
+
GROQ_API_KEY=your_groq_api_key
|
| 127 |
+
|
| 128 |
+
# Qdrant Cloud Credentials
|
| 129 |
+
QDRANT_API_KEY=your_qdrant_api_key
|
| 130 |
+
QDRANT_URL=your_qdrant_cloud_url
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
-----
|
| 134 |
+
|
| 135 |
+
## Project Directory Structure
|
| 136 |
+
|
| 137 |
+
The project follows a modular "Zero-Hurdle" root structure to ensure all paths are resolved correctly during execution:
|
| 138 |
+
|
| 139 |
+
```text
|
| 140 |
+
app/
|
| 141 |
+
├── main.py # FastAPI server and API entry point
|
| 142 |
+
├── chain.py # RAG logic and conversation memory
|
| 143 |
+
├── ingest.py # Data crawling and vector ingestion
|
| 144 |
+
├── .env # Private API keys
|
| 145 |
+
├── requirements.txt # Project dependencies
|
| 146 |
+
├── Prompt/
|
| 147 |
+
│ └── Prompt.py # Custom representative persona
|
| 148 |
+
├── LLM/
|
| 149 |
+
│ └── LLM.py # Groq model configuration
|
| 150 |
+
├── VectorStores/
|
| 151 |
+
│ └── Vectorstores.py # Qdrant cloud connection logic
|
| 152 |
+
├── embeddings/
|
| 153 |
+
│ └── embedding.py # Hugging Face model setup
|
| 154 |
+
├── config/
|
| 155 |
+
│ └── config.py # Environment variable loader
|
| 156 |
+
├── static/ # Professional UI/UX assets
|
| 157 |
+
│ ├── css/
|
| 158 |
+
│ │ └── style.css # Theme and layout styling
|
| 159 |
+
│ ├── js/
|
| 160 |
+
│ │ └── chat.js # Interaction and animation logic
|
| 161 |
+
│ ├── templates/
|
| 162 |
+
│ │ └── index.html # Web structure
|
| 163 |
+
│ └── atomcamp_logo.png # Official organization logo
|
| 164 |
+
└── Extras/ # If Document and Other resource
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
-----
|
| 168 |
+
|
| 169 |
+
## Execution Workflow
|
| 170 |
+
|
| 171 |
+
### Step 1: Knowledge Base Ingestion
|
| 172 |
+
|
| 173 |
+
Before running the chatbot, you must populate the vector database with atomcamp's latest information:
|
| 174 |
+
|
| 175 |
+
```bash
|
| 176 |
+
python ingest.py
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
### Step 2: Launching the Platform
|
| 180 |
+
|
| 181 |
+
Start the FastAPI server to initialize the RAG chain and the web interface:
|
| 182 |
+
|
| 183 |
+
```bash
|
| 184 |
+
python main.py
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
### Step 3: Accessing the UI
|
| 188 |
+
|
| 189 |
+
Open your browser and navigate to the following address:
|
| 190 |
+
`http://localhost:8000`
|
| 191 |
+
|
| 192 |
+
-----
|
| 193 |
+
|
| 194 |
+
## Key Component Breakdown
|
| 195 |
+
|
| 196 |
+
### Advanced Prompt Engineering (Prompt.py)
|
| 197 |
+
|
| 198 |
+
The core of the system's intelligence lies in its specialized persona. The prompt explicitly forbids the use of robotic disclaimers like "According to the context" or "The information provided state." It instructs the AI to combine duplicate facts into sophisticated, high-intelligence sentences and strictly enforce the lowercase **atomcamp** branding.
|
| 199 |
+
|
| 200 |
+
### Retrieval Logic (chain.py)
|
| 201 |
+
|
| 202 |
+
The system utilizes a `RunnableParallel` architecture. When a question is received, the chain simultaneously retrieves relevant documents from Qdrant, pulls the last several turns of conversation from memory, and prepares the final prompt for the LLM. This parallel execution minimizes response latency.
|
| 203 |
+
|
| 204 |
+
-----
|
| 205 |
+
|
| 206 |
+
## Advanced UI/UX Implementation
|
| 207 |
+
|
| 208 |
+
* Professional Input Handling: The input bar is a custom textarea that supports `Enter` to send and `Shift + Enter` for new lines, behaving like a modern enterprise communication tool.
|
| 209 |
+
* Vertical Container Stability: The chat bubbles use advanced word-break rules to ensure that long strings of code or characters never break the horizontal container width.
|
| 210 |
+
* Thematic Link Management: Links are styled with a dynamic CSS variable (`--link-color`) that switches to a high-contrast orange in dark mode, ensuring email addresses and URLs are always readable.
|
| 211 |
+
|
| 212 |
+
-----
|
| 213 |
+
|
| 214 |
+
## Status and Versioning
|
| 215 |
+
|
| 216 |
+
* Version: 1.5.0
|
| 217 |
+
* Last Updated: July 2025
|
| 218 |
+
* Status: Production Ready
|
| 219 |
+
* Organization: atomcamp Official
|
app/LLM/LLM.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_groq import ChatGroq
|
| 2 |
+
from config.config import GROQ_API_KEY
|
| 3 |
+
|
| 4 |
+
llm = ChatGroq(
|
| 5 |
+
model="openai/gpt-oss-120b",
|
| 6 |
+
temperature=1.0,
|
| 7 |
+
max_tokens=None,
|
| 8 |
+
reasoning_format="parsed",
|
| 9 |
+
timeout=None,
|
| 10 |
+
max_retries=2,
|
| 11 |
+
api_key=GROQ_API_KEY,
|
| 12 |
+
)
|
app/Prompt/Prompt.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 2 |
+
|
| 3 |
+
prompt = ChatPromptTemplate.from_template("""
|
| 4 |
+
You are a highly intelligent and confident Official Representative of atomcamp.
|
| 5 |
+
|
| 6 |
+
CRITICAL GUIDELINES:
|
| 7 |
+
1. BRANDING: Always use "atomcamp" (lowercase 'a') when referring to the organization.
|
| 8 |
+
2. NO ROBOTIC META-TALK: Never use phrases like "based on the provided context," "according to the information," or "the context does not mention."
|
| 9 |
+
3. OWN THE KNOWLEDGE: Answer as if you inherently know this information. Use "We," "Our," and "The program is."
|
| 10 |
+
4. NO REPETITION: Combine duplicate facts into one clear, smart sentence.
|
| 11 |
+
5. FALLBACK: If the information is genuinely missing, simply say you don't have the specific details on that right now and suggest they contact the team at atomcamp.com.
|
| 12 |
+
|
| 13 |
+
Internal Knowledge Base:
|
| 14 |
+
{context}
|
| 15 |
+
|
| 16 |
+
Conversation History:
|
| 17 |
+
{chat_history}
|
| 18 |
+
|
| 19 |
+
User Inquiry:
|
| 20 |
+
{question}
|
| 21 |
+
|
| 22 |
+
Answer confidently, naturally, and professionally as a human representative would.
|
| 23 |
+
""")
|
app/VectorStores/Vectorstores.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_qdrant import QdrantVectorStore
|
| 2 |
+
from embeddings.embedding import get_embedding
|
| 3 |
+
from config.config import QDRANT_URL, QDRANT_API_KEY # Use config variables
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
def get_vectorstore():
|
| 8 |
+
return QdrantVectorStore.from_existing_collection(
|
| 9 |
+
embedding=get_embedding(),
|
| 10 |
+
collection_name="Ninesol_Technologies_Knowledge_Base",
|
| 11 |
+
prefer_grpc=True,
|
| 12 |
+
url=QDRANT_URL,
|
| 13 |
+
api_key=QDRANT_API_KEY
|
| 14 |
+
)
|
app/chain.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_core.runnables import RunnableParallel, RunnableLambda, RunnablePassthrough
|
| 2 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 3 |
+
from langchain_core.messages import get_buffer_string
|
| 4 |
+
from langchain_classic.memory import ConversationBufferMemory
|
| 5 |
+
from VectorStores.Vectorstores import get_vectorstore
|
| 6 |
+
from LLM.LLM import llm
|
| 7 |
+
from Prompt.Prompt import prompt
|
| 8 |
+
|
| 9 |
+
def join_docs(docs):
|
| 10 |
+
return "\n\n".join(doc.page_content for doc in docs)
|
| 11 |
+
|
| 12 |
+
memory = ConversationBufferMemory(
|
| 13 |
+
return_messages=True,
|
| 14 |
+
memory_key="chat_history"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
def create_rag_chain(memory):
|
| 18 |
+
retriever = get_vectorstore().as_retriever(
|
| 19 |
+
search_type="mmr",
|
| 20 |
+
search_kwargs={"k": 3, "fetch_k": 10}
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
RunnableParallel({
|
| 25 |
+
"context": retriever | RunnableLambda(join_docs),
|
| 26 |
+
"question": RunnablePassthrough(),
|
| 27 |
+
"chat_history": RunnableLambda(
|
| 28 |
+
lambda _: get_buffer_string(memory.chat_memory.messages)
|
| 29 |
+
)
|
| 30 |
+
})
|
| 31 |
+
| prompt
|
| 32 |
+
| llm
|
| 33 |
+
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
chain=create_rag_chain(memory)
|
| 37 |
+
# while True:
|
| 38 |
+
# user_input = input("Enter your query (or 'exit' to quit): ")
|
| 39 |
+
# print("User:", user_input)
|
| 40 |
+
# if user_input.lower() in ['q', 'exit']:
|
| 41 |
+
# break
|
| 42 |
+
|
| 43 |
+
# # .invoke() returns the final state dictionary directly
|
| 44 |
+
# result = create_rag_chain(memory).invoke(
|
| 45 |
+
# user_input
|
| 46 |
+
# )
|
| 47 |
+
# memory.chat_memory.add_user_message(user_input)
|
| 48 |
+
# memory.chat_memory.add_ai_message(result)
|
| 49 |
+
# AI_msg=result
|
| 50 |
+
# print("Bot:", AI_msg)
|
| 51 |
+
|
app/config/config.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
QDRANT_URL = os.getenv("QDRANT_URL")
|
| 7 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 8 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 9 |
+
HUGGINGFACEHUB_API_TOKEN = os.getenv("HUGGINGFACEHUB_API_TOKEN")
|
app/embeddings/embedding.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_huggingface import HuggingFaceEndpointEmbeddings
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from config.config import HUGGINGFACEHUB_API_TOKEN
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
def get_embedding():
|
| 7 |
+
embeddings = HuggingFaceEndpointEmbeddings(
|
| 8 |
+
repo_id="sentence-transformers/all-mpnet-base-v2",
|
| 9 |
+
task="feature-extraction",
|
| 10 |
+
huggingfacehub_api_token=HUGGINGFACEHUB_API_TOKEN
|
| 11 |
+
)
|
| 12 |
+
return embeddings
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# vector = embedding().embed_query("This is a cloud-based embedding.")
|
| 16 |
+
# print(vector)
|
app/index.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from chain import chain, memory # Direct import for simple root structure
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from fastapi.responses import HTMLResponse
|
| 6 |
+
from fastapi.staticfiles import StaticFiles
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
app = FastAPI()
|
| 10 |
+
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["*"],
|
| 14 |
+
allow_methods=["*"],
|
| 15 |
+
allow_headers=["*"],
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
class ChatRequest(BaseModel):
|
| 19 |
+
message: str
|
| 20 |
+
|
| 21 |
+
@app.post("/chat")
|
| 22 |
+
async def chat_endpoint(request: ChatRequest):
|
| 23 |
+
# Asynchronous response for better performance and responsiveness
|
| 24 |
+
response = await chain.ainvoke(request.message)
|
| 25 |
+
|
| 26 |
+
# Update conversation history manually for RAG context
|
| 27 |
+
memory.chat_memory.add_user_message(request.message)
|
| 28 |
+
memory.chat_memory.add_ai_message(response.content)
|
| 29 |
+
|
| 30 |
+
return {"response": response.content}
|
| 31 |
+
|
| 32 |
+
# Mount the static folder to serve CSS, JS, and the atomcamp logo
|
| 33 |
+
if os.path.exists("static"):
|
| 34 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 35 |
+
|
| 36 |
+
@app.get("/", response_class=HTMLResponse)
|
| 37 |
+
async def read_root():
|
| 38 |
+
# Serves the index.html from your organized static/templates directory
|
| 39 |
+
file_path = os.path.join("static", "templates", "index.html")
|
| 40 |
+
|
| 41 |
+
# Check for file existence to avoid internal server errors
|
| 42 |
+
if not os.path.exists(file_path):
|
| 43 |
+
return HTMLResponse(content=f"UI Error: File not found at {file_path}", status_code=404)
|
| 44 |
+
|
| 45 |
+
with open(file_path, "r") as f:
|
| 46 |
+
return f.read()
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
import uvicorn
|
| 50 |
+
# The server runs on localhost:8000
|
| 51 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
app/ingest.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import bs4 # Make sure to 'pip install beautifulsoup4'
|
| 3 |
+
from langchain_community.document_loaders import WebBaseLoader
|
| 4 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 5 |
+
from langchain_qdrant import QdrantVectorStore
|
| 6 |
+
from embeddings.embedding import get_embedding
|
| 7 |
+
from config.config import QDRANT_URL, QDRANT_API_KEY
|
| 8 |
+
|
| 9 |
+
# 1. Smarter Loading: Only grab the 'main' content to prevent repeating
|
| 10 |
+
# headers, footers, and menus in every single chunk.
|
| 11 |
+
loader = WebBaseLoader(
|
| 12 |
+
web_paths=[
|
| 13 |
+
"https://atomcamp.com/",
|
| 14 |
+
"https://www.atomcamp.com/course/",
|
| 15 |
+
"https://www.atomcamp.com/about-us/",
|
| 16 |
+
"https://www.atomcamp.com/events/",
|
| 17 |
+
"https://www.atomcamp.com/blogs/",
|
| 18 |
+
"https://www.atomcamp.com/news/",
|
| 19 |
+
"https://www.atomcamp.com/webinars/",
|
| 20 |
+
"https://www.atomcamp.com/publications/",
|
| 21 |
+
"https://www.atomcamp.com/ai-solutions/"
|
| 22 |
+
],
|
| 23 |
+
bs_kwargs=dict(
|
| 24 |
+
parse_only=bs4.SoupStrainer(["main", "article", "h1", "h2", "p"])
|
| 25 |
+
)
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
data = loader.load()
|
| 29 |
+
|
| 30 |
+
# 2. Split into clean chunks
|
| 31 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
|
| 32 |
+
docs = text_splitter.split_documents(data)
|
| 33 |
+
|
| 34 |
+
# 3. Safe Upload to Cloud
|
| 35 |
+
BATCH_SIZE = 20
|
| 36 |
+
|
| 37 |
+
print(f"Cleaning old data and starting ingestion of {len(docs)} documents...")
|
| 38 |
+
|
| 39 |
+
# NOTE: Using .from_documents will ADD to the collection.
|
| 40 |
+
# If you want to fix the repetition from previous runs,
|
| 41 |
+
# you should delete the collection in your Qdrant Dashboard first.
|
| 42 |
+
qdrant = QdrantVectorStore.from_documents(
|
| 43 |
+
docs,
|
| 44 |
+
get_embedding(),
|
| 45 |
+
url=QDRANT_URL,
|
| 46 |
+
api_key=QDRANT_API_KEY,
|
| 47 |
+
collection_name="atomcamp_knowledge_base",
|
| 48 |
+
batch_size=BATCH_SIZE,
|
| 49 |
+
timeout=120
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
print("Ingestion complete! atomcamp knowledge base is clean and ready.")
|
app/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
langchain==0.3.0
|
| 2 |
+
langchain-core==0.3.0
|
| 3 |
+
langchain-groq==0.2.0
|
| 4 |
+
langchain-huggingface==0.1.0
|
| 5 |
+
langchain-qdrant==0.2.0
|
| 6 |
+
qdrant-client==1.12.0
|
| 7 |
+
streamlit==1.40.2
|
| 8 |
+
python-dotenv==1.0.1
|
| 9 |
+
huggingface-hub==0.23.0
|
app/static/atomcamp_logo.png
ADDED
|
app/static/css/style.css
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 1. Global Variables & Themes */
|
| 2 |
+
:root {
|
| 3 |
+
--primary: #166534;
|
| 4 |
+
--accent: #22c55e;
|
| 5 |
+
--bg-body: #f1f5f9;
|
| 6 |
+
--bg-app: #ffffff;
|
| 7 |
+
--bg-chat: linear-gradient(to bottom, #ffffff, #f9fafb);
|
| 8 |
+
--bg-input: #f8fafc;
|
| 9 |
+
--text-main: #1e293b;
|
| 10 |
+
--text-light: #64748b;
|
| 11 |
+
--bot-bubble: #f1f5f9;
|
| 12 |
+
--border: #e2e8f0;
|
| 13 |
+
--icon-color: #64748b;
|
| 14 |
+
--link-color: #0d00ff; /* Professional Blue for Light Mode */
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
body.dark-mode {
|
| 18 |
+
--bg-body: #0f172a;
|
| 19 |
+
--bg-app: #1e293b;
|
| 20 |
+
--bg-chat: #1e293b;
|
| 21 |
+
--bg-input: #0f172a;
|
| 22 |
+
--text-main: #f8fafc;
|
| 23 |
+
--text-light: #94a3b8;
|
| 24 |
+
--bot-bubble: #334155;
|
| 25 |
+
--border: #334155;
|
| 26 |
+
--icon-color: #f8fafc;
|
| 27 |
+
--link-color: #ffa321; /* High-Visibility Orange for Dark Mode */
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* 2. Global Reset */
|
| 31 |
+
* {
|
| 32 |
+
box-sizing: border-box;
|
| 33 |
+
margin: 0;
|
| 34 |
+
padding: 0;
|
| 35 |
+
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
body {
|
| 39 |
+
font-family: 'Inter', sans-serif;
|
| 40 |
+
background: var(--bg-body);
|
| 41 |
+
color: var(--text-main);
|
| 42 |
+
display: flex;
|
| 43 |
+
height: 100vh;
|
| 44 |
+
justify-content: center;
|
| 45 |
+
overflow: hidden;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* 3. Main Layout */
|
| 49 |
+
.app-container {
|
| 50 |
+
width: 100%;
|
| 51 |
+
max-width: 850px;
|
| 52 |
+
height: 96vh;
|
| 53 |
+
margin-top: 2vh;
|
| 54 |
+
display: flex;
|
| 55 |
+
flex-direction: column;
|
| 56 |
+
background: var(--bg-app);
|
| 57 |
+
border-radius: 24px;
|
| 58 |
+
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
|
| 59 |
+
position: relative;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
header {
|
| 63 |
+
padding: 1.5rem;
|
| 64 |
+
border-bottom: 1px solid var(--border);
|
| 65 |
+
display: flex;
|
| 66 |
+
flex-direction: column;
|
| 67 |
+
align-items: center;
|
| 68 |
+
gap: 8px;
|
| 69 |
+
position: relative;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.logo-img {
|
| 73 |
+
height: 35px;
|
| 74 |
+
width: auto;
|
| 75 |
+
object-fit: contain;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
#theme-toggle {
|
| 79 |
+
position: absolute;
|
| 80 |
+
top: 1.5rem;
|
| 81 |
+
right: 1.5rem;
|
| 82 |
+
background: none;
|
| 83 |
+
border: none;
|
| 84 |
+
cursor: pointer;
|
| 85 |
+
width: 40px;
|
| 86 |
+
height: 40px;
|
| 87 |
+
border-radius: 12px;
|
| 88 |
+
display: flex;
|
| 89 |
+
align-items: center;
|
| 90 |
+
justify-content: center;
|
| 91 |
+
color: var(--icon-color);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
#theme-toggle:hover { background: var(--bg-body); }
|
| 95 |
+
#theme-toggle svg { width: 22px; height: 22px; stroke-width: 2; }
|
| 96 |
+
|
| 97 |
+
/* 4. Status Badge */
|
| 98 |
+
.status-badge {
|
| 99 |
+
display: flex;
|
| 100 |
+
align-items: center;
|
| 101 |
+
gap: 6px;
|
| 102 |
+
padding: 4px 12px;
|
| 103 |
+
background: rgba(16, 185, 129, 0.1);
|
| 104 |
+
border-radius: 20px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.status-dot { width: 6px; height: 6px; border-radius: 50%; }
|
| 108 |
+
.status-text {
|
| 109 |
+
font-size: 0.65rem;
|
| 110 |
+
font-weight: 700;
|
| 111 |
+
text-transform: uppercase;
|
| 112 |
+
letter-spacing: 0.05em;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.online .status-dot {
|
| 116 |
+
background: var(--accent);
|
| 117 |
+
box-shadow: 0 0 8px var(--accent);
|
| 118 |
+
}
|
| 119 |
+
.online .status-text { color: var(--accent); }
|
| 120 |
+
|
| 121 |
+
.typing .status-dot {
|
| 122 |
+
background: var(--text-light);
|
| 123 |
+
animation: pulse 1s infinite;
|
| 124 |
+
}
|
| 125 |
+
.typing .status-text { color: var(--text-light); }
|
| 126 |
+
|
| 127 |
+
/* 5. Chat History & Bubbles */
|
| 128 |
+
#chat-box {
|
| 129 |
+
flex: 1;
|
| 130 |
+
overflow-y: auto;
|
| 131 |
+
overflow-x: hidden;
|
| 132 |
+
padding: 2.5rem;
|
| 133 |
+
display: flex;
|
| 134 |
+
flex-direction: column;
|
| 135 |
+
gap: 1.5rem;
|
| 136 |
+
background: var(--bg-chat);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.message-row {
|
| 140 |
+
display: flex;
|
| 141 |
+
gap: 12px;
|
| 142 |
+
width: 100%;
|
| 143 |
+
max-width: 85%;
|
| 144 |
+
animation: slideUp 0.4s ease;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.message-row.user {
|
| 148 |
+
align-self: flex-end;
|
| 149 |
+
flex-direction: row-reverse;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.bubble {
|
| 153 |
+
padding: 1rem 1.25rem;
|
| 154 |
+
border-radius: 18px;
|
| 155 |
+
line-height: 1.6;
|
| 156 |
+
font-size: 0.95rem;
|
| 157 |
+
word-wrap: break-word;
|
| 158 |
+
overflow-wrap: break-word;
|
| 159 |
+
word-break: break-word;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/* Fix for Link Visibility */
|
| 163 |
+
.bubble a {
|
| 164 |
+
color: var(--link-color);
|
| 165 |
+
text-decoration: underline;
|
| 166 |
+
font-weight: 500;
|
| 167 |
+
transition: color 0.3s ease;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.bot .bubble {
|
| 171 |
+
background: var(--bot-bubble);
|
| 172 |
+
border: 1px solid var(--border);
|
| 173 |
+
border-top-left-radius: 4px;
|
| 174 |
+
color: var(--text-main);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.user .bubble {
|
| 178 |
+
background: var(--primary);
|
| 179 |
+
color: white;
|
| 180 |
+
border-top-right-radius: 4px;
|
| 181 |
+
box-shadow: 0 4px 12px rgba(22, 101, 52, 0.15);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.bubble ul { list-style-type: disc; margin: 10px 0 10px 20px; }
|
| 185 |
+
.bubble li { margin-bottom: 6px; }
|
| 186 |
+
|
| 187 |
+
/* 6. Typing Animations */
|
| 188 |
+
.loader { display: flex; gap: 4px; padding: 6px 0; }
|
| 189 |
+
.dot {
|
| 190 |
+
width: 5px;
|
| 191 |
+
height: 5px;
|
| 192 |
+
background: var(--text-light);
|
| 193 |
+
border-radius: 50%;
|
| 194 |
+
animation: bounce 1.4s infinite ease-in-out both;
|
| 195 |
+
}
|
| 196 |
+
.dot:nth-child(1) { animation-delay: -0.32s; }
|
| 197 |
+
.dot:nth-child(2) { animation-delay: -0.16s; }
|
| 198 |
+
|
| 199 |
+
/* 7. Input Section */
|
| 200 |
+
.input-wrapper {
|
| 201 |
+
padding: 1.5rem 2.5rem;
|
| 202 |
+
background: var(--bg-app);
|
| 203 |
+
border-top: 1px solid var(--border);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.input-box {
|
| 207 |
+
display: flex;
|
| 208 |
+
align-items: flex-end;
|
| 209 |
+
background: var(--bg-input);
|
| 210 |
+
border-radius: 16px;
|
| 211 |
+
padding: 8px 12px;
|
| 212 |
+
border: 1.5px solid var(--border);
|
| 213 |
+
transition: 0.3s;
|
| 214 |
+
min-height: 56px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.input-box:focus-within { border-color: var(--primary); }
|
| 218 |
+
|
| 219 |
+
#user-input {
|
| 220 |
+
flex: 1;
|
| 221 |
+
border: none;
|
| 222 |
+
background: transparent;
|
| 223 |
+
padding: 10px 4px;
|
| 224 |
+
outline: none;
|
| 225 |
+
font-size: 0.95rem;
|
| 226 |
+
color: var(--text-main);
|
| 227 |
+
font-family: inherit;
|
| 228 |
+
resize: none;
|
| 229 |
+
max-height: 150px;
|
| 230 |
+
overflow-y: auto;
|
| 231 |
+
line-height: 1.5;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
button#send-btn {
|
| 235 |
+
background: var(--primary);
|
| 236 |
+
color: white;
|
| 237 |
+
border: none;
|
| 238 |
+
padding: 12px 24px;
|
| 239 |
+
border-radius: 12px;
|
| 240 |
+
cursor: pointer;
|
| 241 |
+
font-weight: 600;
|
| 242 |
+
margin-left: 8px;
|
| 243 |
+
margin-bottom: 4px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* 8. Keyframes */
|
| 247 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
| 248 |
+
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
|
| 249 |
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
| 250 |
+
|
| 251 |
+
/* Scrollbar Styling */
|
| 252 |
+
::-webkit-scrollbar { width: 5px; }
|
| 253 |
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
|
app/static/js/chat.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 1. Elements Selection
|
| 2 |
+
const box = document.getElementById('chat-box');
|
| 3 |
+
const input = document.getElementById('user-input');
|
| 4 |
+
const btn = document.getElementById('send-btn');
|
| 5 |
+
const statusBadge = document.getElementById('status-badge');
|
| 6 |
+
const statusText = document.getElementById('status-text');
|
| 7 |
+
const themeToggle = document.getElementById('theme-toggle');
|
| 8 |
+
|
| 9 |
+
// 2. Professional SVG Icons
|
| 10 |
+
const sunIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`;
|
| 11 |
+
const moonIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`;
|
| 12 |
+
|
| 13 |
+
// 3. Theme Toggle Logic
|
| 14 |
+
function updateToggleIcon(isDark) {
|
| 15 |
+
themeToggle.innerHTML = isDark ? moonIcon : sunIcon;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
themeToggle.onclick = () => {
|
| 19 |
+
document.body.classList.toggle('dark-mode');
|
| 20 |
+
updateToggleIcon(document.body.classList.contains('dark-mode'));
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
// 4. Input Auto-Resize Logic
|
| 24 |
+
input.addEventListener('input', function() {
|
| 25 |
+
this.style.height = 'auto';
|
| 26 |
+
this.style.height = (this.scrollHeight) + 'px';
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
// 5. Messaging Logic
|
| 30 |
+
async function sendMessage() {
|
| 31 |
+
const msg = input.value.trim();
|
| 32 |
+
if (!msg) return;
|
| 33 |
+
|
| 34 |
+
// UI Reset
|
| 35 |
+
input.value = '';
|
| 36 |
+
input.style.height = 'auto';
|
| 37 |
+
addMessage(msg, 'user');
|
| 38 |
+
|
| 39 |
+
// Show "Typing..." Status
|
| 40 |
+
statusBadge.className = 'status-badge typing';
|
| 41 |
+
statusText.innerText = 'Typing...';
|
| 42 |
+
|
| 43 |
+
// Add Loading Animation Row
|
| 44 |
+
const loaderId = 'loader-' + Date.now();
|
| 45 |
+
const loaderRow = document.createElement('div');
|
| 46 |
+
loaderRow.className = 'message-row bot';
|
| 47 |
+
loaderRow.id = loaderId;
|
| 48 |
+
loaderRow.innerHTML = `<div class="bubble"><div class="loader"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div></div>`;
|
| 49 |
+
box.appendChild(loaderRow);
|
| 50 |
+
box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
|
| 51 |
+
|
| 52 |
+
try {
|
| 53 |
+
const res = await fetch('/chat', {
|
| 54 |
+
method: 'POST',
|
| 55 |
+
headers: { 'Content-Type': 'application/json' },
|
| 56 |
+
body: JSON.stringify({ message: msg })
|
| 57 |
+
});
|
| 58 |
+
const data = await res.json();
|
| 59 |
+
|
| 60 |
+
// Remove loader and display bot response
|
| 61 |
+
document.getElementById(loaderId).remove();
|
| 62 |
+
addMessage(data.response, 'bot');
|
| 63 |
+
} catch (e) {
|
| 64 |
+
if (document.getElementById(loaderId)) document.getElementById(loaderId).remove();
|
| 65 |
+
addMessage("Sorry, I'm having trouble connecting to the server.", 'bot');
|
| 66 |
+
} finally {
|
| 67 |
+
// Restore "Online" status
|
| 68 |
+
statusBadge.className = 'status-badge online';
|
| 69 |
+
statusText.innerText = 'Online';
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function addMessage(content, role) {
|
| 74 |
+
const div = document.createElement('div');
|
| 75 |
+
div.className = `message-row ${role}`;
|
| 76 |
+
// Use marked.js for professional Markdown rendering in bot responses
|
| 77 |
+
const html = role === 'bot' ? marked.parse(content) : content;
|
| 78 |
+
div.innerHTML = `<div class="bubble">${html}</div>`;
|
| 79 |
+
box.appendChild(div);
|
| 80 |
+
box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// 6. Event Listeners
|
| 84 |
+
btn.onclick = sendMessage;
|
| 85 |
+
|
| 86 |
+
input.onkeydown = (e) => {
|
| 87 |
+
// Enter sends message, Shift+Enter adds new line
|
| 88 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 89 |
+
e.preventDefault();
|
| 90 |
+
sendMessage();
|
| 91 |
+
}
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
// 7. Initial State
|
| 95 |
+
updateToggleIcon(false);
|
| 96 |
+
// Pre-parse the initial welcome message
|
| 97 |
+
const initialBotMsg = box.querySelector('.bot .bubble');
|
| 98 |
+
if (initialBotMsg) initialBotMsg.innerHTML = marked.parse(initialBotMsg.innerHTML);
|
app/static/templates/index.html
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>atomcamp AI</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="app-container">
|
| 12 |
+
<header>
|
| 13 |
+
<img src="/static/atomcamp_logo.png" alt="atomcamp" class="logo-img">
|
| 14 |
+
|
| 15 |
+
<button id="theme-toggle" title="Toggle Theme"></button>
|
| 16 |
+
|
| 17 |
+
<div id="status-badge" class="status-badge online">
|
| 18 |
+
<span class="status-dot"></span>
|
| 19 |
+
<span id="status-text" class="status-text">Online</span>
|
| 20 |
+
</div>
|
| 21 |
+
</header>
|
| 22 |
+
|
| 23 |
+
<div id="chat-box">
|
| 24 |
+
<div class="message-row bot">
|
| 25 |
+
<div class="bubble">Hello! I'm the **atomcamp** AI. How can I assist you with our programs today?</div>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<div class="input-wrapper">
|
| 30 |
+
<div class="input-box">
|
| 31 |
+
<textarea id="user-input" placeholder="Type a message..." autocomplete="off" rows="1"></textarea>
|
| 32 |
+
<button id="send-btn">Send</button>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<script src="/static/js/chat.js"></script>
|
| 38 |
+
</body>
|
| 39 |
+
</html>
|