Spaces:
Sleeping
Sleeping
Commit ·
efc5eea
1
Parent(s): fc6949a
Updated project with LFS for PDFs
Browse files- .dockerignore +6 -0
- .env +3 -0
- .gitattributes +1 -0
- Dockerfile +11 -0
- app/api/endpoints/chat.py +51 -0
- app/core/config.py +11 -0
- app/main.py +31 -0
- app/services/llm.py +45 -0
- app/services/rag.py +112 -0
- docker-compose.yml +34 -0
- dstv-explora-quick-guide.pdf +3 -0
- dstvhd6s_quickguide_v22_e_dec2020.pdf +3 -0
- frontend/.gitignore +24 -0
- frontend/README.md +73 -0
- frontend/eslint.config.js +23 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +33 -0
- frontend/postcss.config.js +6 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.tsx +11 -0
- frontend/src/assets/logo.jpeg +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/components/ChatInterface.tsx +153 -0
- frontend/src/index.css +19 -0
- frontend/src/main.tsx +10 -0
- frontend/tailwind.config.js +18 -0
- frontend/tsconfig.app.json +28 -0
- frontend/tsconfig.json +7 -0
- frontend/tsconfig.node.json +26 -0
- frontend/vite.config.ts +7 -0
- logo.jpeg +0 -0
- requirements.txt +17 -0
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
frontend/
|
| 2 |
+
.git/
|
| 3 |
+
.gitignore
|
| 4 |
+
data/chroma/
|
| 5 |
+
*.pdf
|
| 6 |
+
node_modules/
|
.env
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
XAI_API_KEY=xai-Q6zYpn9C8ReN8bGZmp8QIgvyv8ZNee9UusbqUFpT4zPQpblvd1RTerx1mgRtu6L1n8DkdHV5lnzGi23D
|
| 2 |
+
REDIS_HOST=redis
|
| 3 |
+
REDIS_PORT=6379
|
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.pdf filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
app/api/endpoints/chat.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from fastapi.responses import StreamingResponse
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import List
|
| 5 |
+
from app.services.llm import llm_service
|
| 6 |
+
from app.services.rag import rag_service
|
| 7 |
+
|
| 8 |
+
router = APIRouter()
|
| 9 |
+
|
| 10 |
+
class Message(BaseModel):
|
| 11 |
+
role: str
|
| 12 |
+
content: str
|
| 13 |
+
|
| 14 |
+
class ChatRequest(BaseModel):
|
| 15 |
+
messages: List[Message]
|
| 16 |
+
|
| 17 |
+
@router.post("/chat")
|
| 18 |
+
async def chat_endpoint(request: ChatRequest):
|
| 19 |
+
try:
|
| 20 |
+
# 1. Extract latest query
|
| 21 |
+
if not request.messages:
|
| 22 |
+
raise HTTPException(status_code=400, detail="No messages provided")
|
| 23 |
+
|
| 24 |
+
last_message = request.messages[-1]
|
| 25 |
+
user_query = last_message.content
|
| 26 |
+
|
| 27 |
+
# 2. Retrieve Context (RAG)
|
| 28 |
+
# Note: In a real system, we'd only RAG on user queries, not history,
|
| 29 |
+
# but here we keep it simple.
|
| 30 |
+
context_docs = rag_service.query(user_query)
|
| 31 |
+
context_text = "\n\n".join(context_docs) # context_docs is list of strings
|
| 32 |
+
|
| 33 |
+
# 3. Stream Response
|
| 34 |
+
# Convert pydantic messages to dict
|
| 35 |
+
messages_dict = [{"role": m.role, "content": m.content} for m in request.messages]
|
| 36 |
+
|
| 37 |
+
return StreamingResponse(
|
| 38 |
+
llm_service.chat_stream(messages_dict, context=context_text),
|
| 39 |
+
media_type="text/event-stream"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"Error in chat endpoint: {e}")
|
| 44 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 45 |
+
|
| 46 |
+
@router.post("/ingest")
|
| 47 |
+
async def ingest_endpoint(url: str):
|
| 48 |
+
success = rag_service.scrape_and_ingest(url)
|
| 49 |
+
if success:
|
| 50 |
+
return {"status": "success", "message": f"Ingested {url}"}
|
| 51 |
+
raise HTTPException(status_code=400, detail="Failed to ingest URL")
|
app/core/config.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
XAI_API_KEY: str
|
| 5 |
+
REDIS_HOST: str = "redis"
|
| 6 |
+
REDIS_PORT: int = 6379
|
| 7 |
+
|
| 8 |
+
class Config:
|
| 9 |
+
env_file = ".env"
|
| 10 |
+
|
| 11 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.core.config import settings
|
| 4 |
+
from app.api.endpoints import chat
|
| 5 |
+
|
| 6 |
+
app = FastAPI(title="DStv AI Support")
|
| 7 |
+
|
| 8 |
+
# CORS
|
| 9 |
+
origins = [
|
| 10 |
+
"http://localhost:5173",
|
| 11 |
+
"http://localhost:80",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
CORSMiddleware,
|
| 16 |
+
allow_origins=origins,
|
| 17 |
+
allow_credentials=True,
|
| 18 |
+
allow_methods=["*"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
@app.get("/health")
|
| 23 |
+
def health_check():
|
| 24 |
+
return {"status": "ok", "service": "DStv AI Support Backend"}
|
| 25 |
+
|
| 26 |
+
@app.get("/")
|
| 27 |
+
def root():
|
| 28 |
+
return {"message": "DStv AI Support API is running"}
|
| 29 |
+
|
| 30 |
+
app.include_router(chat.router, prefix="/api")
|
| 31 |
+
|
app/services/llm.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openai import OpenAI
|
| 2 |
+
from app.core.config import settings
|
| 3 |
+
from typing import List, Dict, Generator
|
| 4 |
+
|
| 5 |
+
class LLMService:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.client = OpenAI(
|
| 8 |
+
api_key=settings.XAI_API_KEY,
|
| 9 |
+
base_url="https://api.x.ai/v1"
|
| 10 |
+
)
|
| 11 |
+
self.model = "grok-4-1-fast-reasoning" # Actually check if this is the exact string. Assuming yes based on prompt.
|
| 12 |
+
|
| 13 |
+
def get_system_prompt(self, context: str = "") -> str:
|
| 14 |
+
base_prompt = """You are DStv AI Support, a friendly and helpful customer service agent for DStv.
|
| 15 |
+
Your goal is to assist customers with subscriptions, packages, payments, decoder issues, signal problems, installations, and account management.
|
| 16 |
+
|
| 17 |
+
Brand Voice:
|
| 18 |
+
- Friendly, practical, and clear.
|
| 19 |
+
- Do NOT say "I am an AI", "Based on the context", or "Internal system details".
|
| 20 |
+
- Speak like a real human agent.
|
| 21 |
+
|
| 22 |
+
Context from Knowledge Base:
|
| 23 |
+
{context}
|
| 24 |
+
|
| 25 |
+
If the context helps, use it. If not, use your general knowledge but be careful not to hallucinate DStv specifics.
|
| 26 |
+
If you don't know, suggest they check self-service on dstv.com or contact our call center.
|
| 27 |
+
"""
|
| 28 |
+
return base_prompt.replace("{context}", context)
|
| 29 |
+
|
| 30 |
+
def chat_stream(self, messages: List[Dict[str, str]], context: str = "") -> Generator[str, None, None]:
|
| 31 |
+
system_msg = {"role": "system", "content": self.get_system_prompt(context)}
|
| 32 |
+
full_messages = [system_msg] + messages
|
| 33 |
+
|
| 34 |
+
stream = self.client.chat.completions.create(
|
| 35 |
+
model=self.model,
|
| 36 |
+
messages=full_messages,
|
| 37 |
+
stream=True,
|
| 38 |
+
temperature=0.4 # Lower temperature for support accuracy
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
for chunk in stream:
|
| 42 |
+
if chunk.choices[0].delta.content:
|
| 43 |
+
yield chunk.choices[0].delta.content
|
| 44 |
+
|
| 45 |
+
llm_service = LLMService()
|
app/services/rag.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chromadb
|
| 2 |
+
from chromadb.utils import embedding_functions
|
| 3 |
+
import requests
|
| 4 |
+
from bs4 import BeautifulSoup
|
| 5 |
+
import uuid
|
| 6 |
+
import os
|
| 7 |
+
from pydantic_settings import BaseSettings
|
| 8 |
+
from pypdf import PdfReader
|
| 9 |
+
|
| 10 |
+
class RAGService:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
# Persistent storage in ./data/chroma (relative to root now)
|
| 13 |
+
self.client = chromadb.PersistentClient(path="./data/chroma")
|
| 14 |
+
|
| 15 |
+
self.embedding_fn = embedding_functions.DefaultEmbeddingFunction()
|
| 16 |
+
|
| 17 |
+
self.collection = self.client.get_or_create_collection(
|
| 18 |
+
name="dstv_knowledge",
|
| 19 |
+
embedding_function=self.embedding_fn
|
| 20 |
+
)
|
| 21 |
+
# Auto ingest PDFs on startup
|
| 22 |
+
self.ingest_all_pdfs()
|
| 23 |
+
|
| 24 |
+
def ingest_pdf(self, file_path: str):
|
| 25 |
+
try:
|
| 26 |
+
print(f"Ingesting PDF: {file_path}")
|
| 27 |
+
reader = PdfReader(file_path)
|
| 28 |
+
text_chunks = []
|
| 29 |
+
|
| 30 |
+
for i, page in enumerate(reader.pages):
|
| 31 |
+
text = page.extract_text()
|
| 32 |
+
if text and len(text) > 50:
|
| 33 |
+
# In a real app, we'd split large pages.
|
| 34 |
+
# Assuming quick guides have less dense text per page or acceptable length.
|
| 35 |
+
text_chunks.append(text)
|
| 36 |
+
|
| 37 |
+
if not text_chunks:
|
| 38 |
+
return False
|
| 39 |
+
|
| 40 |
+
ids = [f"{os.path.basename(file_path)}_page_{i}" for i in range(len(text_chunks))]
|
| 41 |
+
metadatas = [{"source": file_path, "page": i} for i in range(len(text_chunks))]
|
| 42 |
+
|
| 43 |
+
self.collection.add(
|
| 44 |
+
documents=text_chunks,
|
| 45 |
+
ids=ids,
|
| 46 |
+
metadatas=metadatas
|
| 47 |
+
)
|
| 48 |
+
print(f"Ingested {len(text_chunks)} pages from {file_path}")
|
| 49 |
+
return True
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"Error ingesting PDF {file_path}: {e}")
|
| 52 |
+
return False
|
| 53 |
+
|
| 54 |
+
def ingest_all_pdfs(self, root_dir: str = "."):
|
| 55 |
+
for file in os.listdir(root_dir):
|
| 56 |
+
if file.lower().endswith(".pdf"):
|
| 57 |
+
# Check if already ingested (naive check by ID of page 0)
|
| 58 |
+
try:
|
| 59 |
+
existing = self.collection.get(ids=[f"{file}_page_0"])
|
| 60 |
+
if existing and existing['ids']:
|
| 61 |
+
print(f"PDF {file} already found in DB.")
|
| 62 |
+
continue
|
| 63 |
+
except:
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
self.ingest_pdf(os.path.join(root_dir, file))
|
| 67 |
+
|
| 68 |
+
def scrape_and_ingest(self, url: str):
|
| 69 |
+
try:
|
| 70 |
+
print(f"Scraping {url}...")
|
| 71 |
+
headers = {'User-Agent': 'Mozilla/5.0 (compatible; DStvBot/1.0)'}
|
| 72 |
+
response = requests.get(url, headers=headers, timeout=10)
|
| 73 |
+
if response.status_code != 200:
|
| 74 |
+
print(f"Failed to fetch {url}: {response.status_code}")
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 78 |
+
|
| 79 |
+
text_blocks = []
|
| 80 |
+
for tag in soup.find_all(['p', 'h1', 'h2', 'h3', 'li']):
|
| 81 |
+
text = tag.get_text(strip=True)
|
| 82 |
+
if len(text) > 30:
|
| 83 |
+
text_blocks.append(text)
|
| 84 |
+
|
| 85 |
+
if not text_blocks:
|
| 86 |
+
return False
|
| 87 |
+
|
| 88 |
+
ids = [str(uuid.uuid4()) for _ in text_blocks]
|
| 89 |
+
metadatas = [{"source": url} for _ in text_blocks]
|
| 90 |
+
|
| 91 |
+
self.collection.add(
|
| 92 |
+
documents=text_blocks,
|
| 93 |
+
ids=ids,
|
| 94 |
+
metadatas=metadatas
|
| 95 |
+
)
|
| 96 |
+
print(f"Ingested {len(text_blocks)} chunks from {url}")
|
| 97 |
+
return True
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"Error ingesting {url}: {e}")
|
| 101 |
+
return False
|
| 102 |
+
|
| 103 |
+
def query(self, query_text: str, n_results: int = 3):
|
| 104 |
+
results = self.collection.query(
|
| 105 |
+
query_texts=[query_text],
|
| 106 |
+
n_results=n_results
|
| 107 |
+
)
|
| 108 |
+
if results["documents"]:
|
| 109 |
+
return results["documents"][0]
|
| 110 |
+
return []
|
| 111 |
+
|
| 112 |
+
rag_service = RAGService()
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
backend:
|
| 5 |
+
build: .
|
| 6 |
+
container_name: dstv_backend
|
| 7 |
+
ports:
|
| 8 |
+
- "8000:8000"
|
| 9 |
+
volumes:
|
| 10 |
+
- .:/app
|
| 11 |
+
environment:
|
| 12 |
+
- XAI_API_KEY=${XAI_API_KEY}
|
| 13 |
+
- REDIS_HOST=redis
|
| 14 |
+
depends_on:
|
| 15 |
+
- redis
|
| 16 |
+
|
| 17 |
+
frontend:
|
| 18 |
+
build:
|
| 19 |
+
context: ./frontend
|
| 20 |
+
dockerfile: Dockerfile
|
| 21 |
+
container_name: dstv_frontend
|
| 22 |
+
ports:
|
| 23 |
+
- "5173:5173"
|
| 24 |
+
volumes:
|
| 25 |
+
- ./frontend:/app
|
| 26 |
+
- /app/node_modules
|
| 27 |
+
environment:
|
| 28 |
+
- VITE_API_URL=http://localhost:8000
|
| 29 |
+
|
| 30 |
+
redis:
|
| 31 |
+
image: redis:alpine
|
| 32 |
+
container_name: dstv_redis
|
| 33 |
+
ports:
|
| 34 |
+
- "6379:6379"
|
dstv-explora-quick-guide.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:195cdfb3e1dae4e9c717a27966ba4d68c68bf53a3e566f5bc277f061e7dddae8
|
| 3 |
+
size 1380743
|
dstvhd6s_quickguide_v22_e_dec2020.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6910dec0288a4c3698d3fde6ba93cdd5b48c859282f8530bb2fbafd54a7398fa
|
| 3 |
+
size 1012623
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + TypeScript + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
| 17 |
+
|
| 18 |
+
```js
|
| 19 |
+
export default defineConfig([
|
| 20 |
+
globalIgnores(['dist']),
|
| 21 |
+
{
|
| 22 |
+
files: ['**/*.{ts,tsx}'],
|
| 23 |
+
extends: [
|
| 24 |
+
// Other configs...
|
| 25 |
+
|
| 26 |
+
// Remove tseslint.configs.recommended and replace with this
|
| 27 |
+
tseslint.configs.recommendedTypeChecked,
|
| 28 |
+
// Alternatively, use this for stricter rules
|
| 29 |
+
tseslint.configs.strictTypeChecked,
|
| 30 |
+
// Optionally, add this for stylistic rules
|
| 31 |
+
tseslint.configs.stylisticTypeChecked,
|
| 32 |
+
|
| 33 |
+
// Other configs...
|
| 34 |
+
],
|
| 35 |
+
languageOptions: {
|
| 36 |
+
parserOptions: {
|
| 37 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 38 |
+
tsconfigRootDir: import.meta.dirname,
|
| 39 |
+
},
|
| 40 |
+
// other options...
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
])
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
| 47 |
+
|
| 48 |
+
```js
|
| 49 |
+
// eslint.config.js
|
| 50 |
+
import reactX from 'eslint-plugin-react-x'
|
| 51 |
+
import reactDom from 'eslint-plugin-react-dom'
|
| 52 |
+
|
| 53 |
+
export default defineConfig([
|
| 54 |
+
globalIgnores(['dist']),
|
| 55 |
+
{
|
| 56 |
+
files: ['**/*.{ts,tsx}'],
|
| 57 |
+
extends: [
|
| 58 |
+
// Other configs...
|
| 59 |
+
// Enable lint rules for React
|
| 60 |
+
reactX.configs['recommended-typescript'],
|
| 61 |
+
// Enable lint rules for React DOM
|
| 62 |
+
reactDom.configs.recommended,
|
| 63 |
+
],
|
| 64 |
+
languageOptions: {
|
| 65 |
+
parserOptions: {
|
| 66 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 67 |
+
tsconfigRootDir: import.meta.dirname,
|
| 68 |
+
},
|
| 69 |
+
// other options...
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
])
|
| 73 |
+
```
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default defineConfig([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs.flat.recommended,
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>frontend</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^19.2.0",
|
| 14 |
+
"react-dom": "^19.2.0"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@eslint/js": "^9.39.1",
|
| 18 |
+
"@types/node": "^24.10.1",
|
| 19 |
+
"@types/react": "^19.2.5",
|
| 20 |
+
"@types/react-dom": "^19.2.3",
|
| 21 |
+
"@vitejs/plugin-react": "^5.1.1",
|
| 22 |
+
"autoprefixer": "^10.4.23",
|
| 23 |
+
"eslint": "^9.39.1",
|
| 24 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 25 |
+
"eslint-plugin-react-refresh": "^0.4.24",
|
| 26 |
+
"globals": "^16.5.0",
|
| 27 |
+
"postcss": "^8.5.6",
|
| 28 |
+
"tailwindcss": "^4.1.18",
|
| 29 |
+
"typescript": "~5.9.3",
|
| 30 |
+
"typescript-eslint": "^8.46.4",
|
| 31 |
+
"vite": "^7.2.4"
|
| 32 |
+
}
|
| 33 |
+
}
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
frontend/public/vite.svg
ADDED
|
|
frontend/src/App.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ChatInterface from './components/ChatInterface';
|
| 2 |
+
|
| 3 |
+
function App() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="App w-full h-full bg-dstv-black">
|
| 6 |
+
<ChatInterface />
|
| 7 |
+
</div>
|
| 8 |
+
);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default App;
|
frontend/src/assets/logo.jpeg
ADDED
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/components/ChatInterface.tsx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import logo from '../assets/logo.jpeg';
|
| 3 |
+
|
| 4 |
+
interface Message {
|
| 5 |
+
id: string;
|
| 6 |
+
role: 'user' | 'assistant';
|
| 7 |
+
content: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function ChatInterface() {
|
| 11 |
+
const [messages, setMessages] = useState<Message[]>([
|
| 12 |
+
{ id: '1', role: 'assistant', content: 'Hello! I\'m your DStv guide. I can help with subscriptions, payments, technical issues, and more. How can I assist you today?' }
|
| 13 |
+
]);
|
| 14 |
+
const [input, setInput] = useState('');
|
| 15 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 16 |
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 17 |
+
|
| 18 |
+
const scrollToBottom = () => {
|
| 19 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
scrollToBottom();
|
| 24 |
+
}, [messages]);
|
| 25 |
+
|
| 26 |
+
const sendMessage = async () => {
|
| 27 |
+
if (!input.trim()) return;
|
| 28 |
+
|
| 29 |
+
const userMsg: Message = { id: Date.now().toString(), role: 'user', content: input };
|
| 30 |
+
setMessages(prev => [...prev, userMsg]);
|
| 31 |
+
setInput('');
|
| 32 |
+
setIsLoading(true);
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000'}/api/chat`, {
|
| 36 |
+
method: 'POST',
|
| 37 |
+
headers: {
|
| 38 |
+
'Content-Type': 'application/json',
|
| 39 |
+
},
|
| 40 |
+
body: JSON.stringify({
|
| 41 |
+
messages: [...messages, userMsg].map(m => ({ role: m.role, content: m.content }))
|
| 42 |
+
}),
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
if (!response.ok) throw new Error('Network response was not ok');
|
| 46 |
+
if (!response.body) return;
|
| 47 |
+
|
| 48 |
+
const reader = response.body.getReader();
|
| 49 |
+
const decoder = new TextDecoder();
|
| 50 |
+
const aiMsgId = (Date.now() + 1).toString();
|
| 51 |
+
|
| 52 |
+
// Add empty AI message first
|
| 53 |
+
setMessages(prev => [...prev, { id: aiMsgId, role: 'assistant', content: '' }]);
|
| 54 |
+
|
| 55 |
+
let accumulatedContent = '';
|
| 56 |
+
|
| 57 |
+
while (true) {
|
| 58 |
+
const { done, value } = await reader.read();
|
| 59 |
+
if (done) break;
|
| 60 |
+
|
| 61 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 62 |
+
accumulatedContent += chunk;
|
| 63 |
+
|
| 64 |
+
setMessages(prev => prev.map(msg =>
|
| 65 |
+
msg.id === aiMsgId ? { ...msg, content: accumulatedContent } : msg
|
| 66 |
+
));
|
| 67 |
+
}
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.error('Error sending message:', error);
|
| 70 |
+
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: "Sorry, I'm having trouble connecting to the server. Please try again later." }]);
|
| 71 |
+
} finally {
|
| 72 |
+
setIsLoading(false);
|
| 73 |
+
}
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
return (
|
| 77 |
+
<div className="flex flex-col h-screen bg-dstv-black text-white font-sans max-w-md mx-auto shadow-2xl overflow-hidden border-x border-dstv-dark-gray sm:max-w-2xl md:max-w-4xl lg:max-w-6xl xl:max-w-full">
|
| 78 |
+
{/* Header */}
|
| 79 |
+
<header className="bg-dstv-blue p-4 flex items-center shadow-lg z-10">
|
| 80 |
+
<img src={logo} alt="DStv Logo" className="h-8 md:h-10 mr-3 rounded-sm" />
|
| 81 |
+
<div>
|
| 82 |
+
<h1 className="font-bold text-lg md:text-xl tracking-wide">DStv Support</h1>
|
| 83 |
+
<span className="text-xs text-blue-100 opacity-90 block">Always here for you</span>
|
| 84 |
+
</div>
|
| 85 |
+
</header>
|
| 86 |
+
|
| 87 |
+
{/* Chat Area */}
|
| 88 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-dstv-black to-dstv-dark-gray backdrop-blur-sm">
|
| 89 |
+
{messages.map((msg) => (
|
| 90 |
+
<div
|
| 91 |
+
key={msg.id}
|
| 92 |
+
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} animate-fadeIn`}
|
| 93 |
+
>
|
| 94 |
+
<div
|
| 95 |
+
className={`max-w-[80%] md:max-w-[70%] p-3 md:p-4 rounded-2xl text-sm md:text-base leading-relaxed shadow-sm ${msg.role === 'user'
|
| 96 |
+
? 'bg-dstv-blue text-white rounded-br-none'
|
| 97 |
+
: 'bg-zinc-800 text-gray-100 rounded-bl-none border border-zinc-700'
|
| 98 |
+
}`}
|
| 99 |
+
>
|
| 100 |
+
{msg.content.split('\n').map((paragraph, idx) => (
|
| 101 |
+
<p key={idx} className={idx > 0 ? 'mt-3' : ''}>{paragraph}</p>
|
| 102 |
+
))}
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
))}
|
| 106 |
+
{isLoading && (
|
| 107 |
+
<div className="flex justify-start">
|
| 108 |
+
<div className="bg-zinc-800 p-3 rounded-2xl rounded-bl-none border border-zinc-700 flex space-x-2 items-center">
|
| 109 |
+
<div className="w-2 h-2 bg-dstv-blue rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
| 110 |
+
<div className="w-2 h-2 bg-dstv-blue rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
| 111 |
+
<div className="w-2 h-2 bg-dstv-blue rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
)}
|
| 115 |
+
<div ref={messagesEndRef} />
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
{/* Input Area */}
|
| 119 |
+
<div className="p-4 bg-dstv-dark-gray border-t border-zinc-800">
|
| 120 |
+
<div className="flex items-end space-x-2 bg-zinc-900 p-2 rounded-xl border border-zinc-700 focus-within:border-dstv-blue transition-colors">
|
| 121 |
+
<textarea
|
| 122 |
+
value={input}
|
| 123 |
+
onChange={(e) => setInput(e.target.value)}
|
| 124 |
+
onKeyDown={(e) => {
|
| 125 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 126 |
+
e.preventDefault();
|
| 127 |
+
sendMessage();
|
| 128 |
+
}
|
| 129 |
+
}}
|
| 130 |
+
placeholder="Type your message..."
|
| 131 |
+
className="flex-1 bg-transparent border-none text-white focus:ring-0 resize-none max-h-32 min-h-[44px] py-1 px-2 placeholder-zinc-500"
|
| 132 |
+
rows={1}
|
| 133 |
+
/>
|
| 134 |
+
<button
|
| 135 |
+
onClick={sendMessage}
|
| 136 |
+
disabled={!input.trim() || isLoading}
|
| 137 |
+
className={`p-2 rounded-lg transition-all duration-200 ${input.trim() && !isLoading
|
| 138 |
+
? 'bg-dstv-blue text-white hover:bg-blue-600 shadow-lg shadow-blue-500/20'
|
| 139 |
+
: 'bg-zinc-700 text-zinc-500 cursor-not-allowed'
|
| 140 |
+
}`}
|
| 141 |
+
>
|
| 142 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 transform rotate-[-45deg] relative left-0.5 top-[-2px]">
|
| 143 |
+
<path d="M3.478 2.404a.75.75 0 00-.926.941l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.404z" />
|
| 144 |
+
</svg>
|
| 145 |
+
</button>
|
| 146 |
+
</div>
|
| 147 |
+
<div className="text-center mt-2">
|
| 148 |
+
<p className="text-[10px] text-zinc-500">AI responses may vary. Check DStv.com for official info.</p>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
);
|
| 153 |
+
}
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@layer base {
|
| 6 |
+
body {
|
| 7 |
+
@apply bg-dstv-black text-white font-sans;
|
| 8 |
+
}
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
@layer utilities {
|
| 12 |
+
.scrollbar-hide::-webkit-scrollbar {
|
| 13 |
+
display: none;
|
| 14 |
+
}
|
| 15 |
+
.scrollbar-hide {
|
| 16 |
+
-ms-overflow-style: none;
|
| 17 |
+
scrollbar-width: none;
|
| 18 |
+
}
|
| 19 |
+
}
|
frontend/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.tsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontend/tailwind.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
'dstv-blue': '#1B8DC6',
|
| 11 |
+
'dstv-black': '#000000',
|
| 12 |
+
'dstv-dark-gray': '#121212',
|
| 13 |
+
'dstv-light-gray': '#E5E5E5',
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
},
|
| 17 |
+
plugins: [],
|
| 18 |
+
}
|
frontend/tsconfig.app.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"types": ["vite/client"],
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
|
| 11 |
+
/* Bundler mode */
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"verbatimModuleSyntax": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"erasableSyntaxOnly": true,
|
| 24 |
+
"noFallthroughCasesInSwitch": true,
|
| 25 |
+
"noUncheckedSideEffectImports": true
|
| 26 |
+
},
|
| 27 |
+
"include": ["src"]
|
| 28 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
frontend/tsconfig.node.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"types": ["node"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true,
|
| 23 |
+
"noUncheckedSideEffectImports": true
|
| 24 |
+
},
|
| 25 |
+
"include": ["vite.config.ts"]
|
| 26 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
})
|
logo.jpeg
ADDED
|
requirements.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
python-multipart
|
| 4 |
+
# Networking
|
| 5 |
+
requests
|
| 6 |
+
httpx
|
| 7 |
+
# AI / LLM
|
| 8 |
+
openai
|
| 9 |
+
# Database / Vector Store
|
| 10 |
+
chromadb
|
| 11 |
+
redis
|
| 12 |
+
# Scraper
|
| 13 |
+
beautifulsoup4
|
| 14 |
+
# Utils
|
| 15 |
+
pydantic-settings
|
| 16 |
+
python-dotenv
|
| 17 |
+
pypdf
|