Spaces:
Sleeping
Sleeping
GitHub Copilot commited on
Commit ·
a654c7f
1
Parent(s): 96b8200
Deploy Docker Space package (no binary assets)
Browse files- .gitattributes +0 -35
- Dockerfile +34 -0
- README.md +60 -7
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/rag_utils.cpython-311.pyc +0 -0
- app.py +212 -0
- data/krce_college_data.jsonl +0 -0
- data/krce_college_data_clean.jsonl +58 -0
- rag_utils.py +667 -0
- requirements.txt +9 -0
- static/index.html +1804 -0
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install build dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
cmake \
|
| 9 |
+
git \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Upgrade pip
|
| 13 |
+
RUN pip install --upgrade pip
|
| 14 |
+
|
| 15 |
+
# Copy requirements
|
| 16 |
+
COPY requirements.txt .
|
| 17 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 18 |
+
|
| 19 |
+
# Install llama-cpp-python - try multiple repos in order
|
| 20 |
+
# First try jllllll's CPU wheels, then abetlen's
|
| 21 |
+
RUN pip install --no-cache-dir llama-cpp-python==0.2.85 \
|
| 22 |
+
--extra-index-url https://jllllll.github.io/llama-cpp-python-cuBLAS-wheels/basic/cpu \
|
| 23 |
+
|| pip install --no-cache-dir llama-cpp-python==0.2.85 \
|
| 24 |
+
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \
|
| 25 |
+
|| pip install --no-cache-dir llama-cpp-python==0.2.85
|
| 26 |
+
|
| 27 |
+
# Copy application
|
| 28 |
+
COPY . .
|
| 29 |
+
|
| 30 |
+
# Expose port
|
| 31 |
+
EXPOSE 7860
|
| 32 |
+
|
| 33 |
+
# Run
|
| 34 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,12 +1,65 @@
|
|
| 1 |
---
|
| 2 |
-
title: Krish Mind
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
-
license: apache-2.0
|
| 9 |
-
short_description: Explore Krish Mind , Your Lightning fast Model
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Krish Mind Phi Mini Chat
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: cyan
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Krish Mind Phi Mini - Hugging Face Space Deployment
|
| 12 |
+
|
| 13 |
+
This folder is a ready-to-upload Hugging Face Space package using the mobile GGUF model.
|
| 14 |
+
|
| 15 |
+
## Model Source
|
| 16 |
+
- Repo: Krishkanth/krish-mind-mobile
|
| 17 |
+
- File: krish-mind-mobile.gguf
|
| 18 |
+
- The model is downloaded automatically at startup using huggingface_hub.
|
| 19 |
+
|
| 20 |
+
## What Is Included
|
| 21 |
+
- app.py: FastAPI backend (llama-cpp, KRCE mode, normal mode, history-aware prompts)
|
| 22 |
+
- rag_utils.py: KRCE retrieval and response guards
|
| 23 |
+
- static/: Frontend UI
|
| 24 |
+
- data/: KRCE knowledge data (clean and legacy)
|
| 25 |
+
- Dockerfile: Space runtime image
|
| 26 |
+
- requirements.txt: Python dependencies
|
| 27 |
+
|
| 28 |
+
## Create Space
|
| 29 |
+
1. Open Hugging Face and create a new Space.
|
| 30 |
+
2. Choose SDK: Docker.
|
| 31 |
+
3. Use CPU Basic (free) or better.
|
| 32 |
+
4. Create the Space.
|
| 33 |
+
|
| 34 |
+
## Upload Files
|
| 35 |
+
Upload all files and folders from this folder:
|
| 36 |
+
- Dockerfile
|
| 37 |
+
- requirements.txt
|
| 38 |
+
- app.py
|
| 39 |
+
- rag_utils.py
|
| 40 |
+
- static/
|
| 41 |
+
- data/
|
| 42 |
+
- README.md
|
| 43 |
+
|
| 44 |
+
## Deploy Using Git
|
| 45 |
+
1. Clone your Space repository.
|
| 46 |
+
2. Copy all files from this deployment_hf_mobile folder into the cloned Space folder.
|
| 47 |
+
3. Commit and push.
|
| 48 |
+
|
| 49 |
+
## Environment Variables (Optional)
|
| 50 |
+
You can set these in Space Settings if needed:
|
| 51 |
+
- HF_HOME=/tmp/huggingface
|
| 52 |
+
- TRANSFORMERS_CACHE=/tmp/huggingface
|
| 53 |
+
|
| 54 |
+
## Runtime Notes
|
| 55 |
+
- KRCE mode ON: answers only from KRCE data and abstains for non-KRCE questions.
|
| 56 |
+
- KRCE mode OFF: normal model answers using mobile model.
|
| 57 |
+
- Backend supports history from frontend to improve answer continuity.
|
| 58 |
+
|
| 59 |
+
## Local Smoke Test
|
| 60 |
+
Run from this folder:
|
| 61 |
+
|
| 62 |
+
python -m uvicorn app:app --host 0.0.0.0 --port 7860
|
| 63 |
+
|
| 64 |
+
Then open:
|
| 65 |
+
http://127.0.0.1:7860
|
__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
__pycache__/rag_utils.cpython-311.pyc
ADDED
|
Binary file (30.4 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import urllib.parse
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Any
|
| 6 |
+
from fastapi import FastAPI
|
| 7 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
+
from fastapi.staticfiles import StaticFiles
|
| 9 |
+
from fastapi.responses import FileResponse
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
import uvicorn
|
| 12 |
+
|
| 13 |
+
# --- Core dependencies ---
|
| 14 |
+
try:
|
| 15 |
+
from llama_cpp import Llama
|
| 16 |
+
print("✅ llama-cpp-python")
|
| 17 |
+
except ImportError:
|
| 18 |
+
print("❌ Run: pip install llama-cpp-python")
|
| 19 |
+
sys.exit(1)
|
| 20 |
+
|
| 21 |
+
from rag_utils import (
|
| 22 |
+
ABSTAIN_MESSAGE,
|
| 23 |
+
build_general_system_prompt,
|
| 24 |
+
build_hybrid_system_prompt,
|
| 25 |
+
build_system_prompt,
|
| 26 |
+
compose_krce_response,
|
| 27 |
+
finalize_general_response,
|
| 28 |
+
finalize_krce_response,
|
| 29 |
+
load_rag_index,
|
| 30 |
+
search_krce,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# --- Config ---
|
| 34 |
+
# Model settings
|
| 35 |
+
REPO_ID = "Krishkanth/krish-mind-mobile"
|
| 36 |
+
MODEL_FILENAME = "krish-mind-mobile.gguf"
|
| 37 |
+
BASE_DIR = os.path.dirname(__file__)
|
| 38 |
+
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
| 39 |
+
default_clean_data = os.path.join(BASE_DIR, "data", "krce_college_data_clean.jsonl")
|
| 40 |
+
default_legacy_data = os.path.join(BASE_DIR, "data", "krce_college_data.jsonl")
|
| 41 |
+
DATA_FILE = default_clean_data if os.path.exists(default_clean_data) else default_legacy_data
|
| 42 |
+
|
| 43 |
+
# --- Load GGUF Model ---
|
| 44 |
+
print(f"\n⏳ Downloading/Loading model from {REPO_ID}...")
|
| 45 |
+
try:
|
| 46 |
+
from huggingface_hub import hf_hub_download
|
| 47 |
+
|
| 48 |
+
# Download model (cached)
|
| 49 |
+
model_path = hf_hub_download(
|
| 50 |
+
repo_id=REPO_ID,
|
| 51 |
+
filename=MODEL_FILENAME,
|
| 52 |
+
local_dir="model", # Download to local folder
|
| 53 |
+
local_dir_use_symlinks=False
|
| 54 |
+
)
|
| 55 |
+
print(f"✅ Model downloaded to: {model_path}")
|
| 56 |
+
|
| 57 |
+
model = Llama(
|
| 58 |
+
model_path=model_path,
|
| 59 |
+
n_ctx=4096,
|
| 60 |
+
n_gpu_layers=0, # CPU only for free tier
|
| 61 |
+
verbose=False
|
| 62 |
+
)
|
| 63 |
+
print("✅ Model loaded!")
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"❌ Model error: {e}")
|
| 67 |
+
model = None
|
| 68 |
+
|
| 69 |
+
# --- RAG SETUP ---
|
| 70 |
+
print("📚 Indexing Knowledge Base...")
|
| 71 |
+
rag_index = load_rag_index(DATA_FILE)
|
| 72 |
+
if rag_index.model is not None and rag_index.records:
|
| 73 |
+
print(f"✅ Indexed {len(rag_index.records)} KRCE facts.")
|
| 74 |
+
else:
|
| 75 |
+
print("⚠️ Data file not found or embedding model unavailable. RAG disabled.")
|
| 76 |
+
|
| 77 |
+
# --- FastAPI ---
|
| 78 |
+
app = FastAPI()
|
| 79 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
| 80 |
+
|
| 81 |
+
# Serve Static Files
|
| 82 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 83 |
+
|
| 84 |
+
class ChatRequest(BaseModel):
|
| 85 |
+
message: str
|
| 86 |
+
max_tokens: int = 1024
|
| 87 |
+
temperature: float = 0.1
|
| 88 |
+
krce_mode: bool = False
|
| 89 |
+
history: list[dict[str, Any]] = Field(default_factory=list)
|
| 90 |
+
|
| 91 |
+
@app.get("/")
|
| 92 |
+
async def root():
|
| 93 |
+
# Serve index.html at root
|
| 94 |
+
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
|
| 95 |
+
|
| 96 |
+
@app.get("/logo.png")
|
| 97 |
+
async def logo():
|
| 98 |
+
# Serve logo.png at root (frontend expects it here)
|
| 99 |
+
return FileResponse(os.path.join(STATIC_DIR, "logo.png"))
|
| 100 |
+
|
| 101 |
+
@app.post("/chat")
|
| 102 |
+
async def chat(request: ChatRequest):
|
| 103 |
+
if not model:
|
| 104 |
+
return {"response": "Error: Model not loaded. Please check server logs."}
|
| 105 |
+
|
| 106 |
+
user_input = request.message
|
| 107 |
+
|
| 108 |
+
# Image Generation Hook
|
| 109 |
+
if any(t in user_input.lower() for t in ["generate image", "create image", "draw", "imagine"]):
|
| 110 |
+
prompt = user_input.replace("generate image", "").strip()
|
| 111 |
+
url = f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}"
|
| 112 |
+
return {"response": f"Here's your image of **{prompt}**:\n\n"}
|
| 113 |
+
|
| 114 |
+
# Frontend controls route explicitly:
|
| 115 |
+
# - KRCE mode ON: strict grounded KRCE answers only
|
| 116 |
+
# - KRCE mode OFF: normal model chat without RAG retrieval
|
| 117 |
+
route = "krce" if bool(request.krce_mode) else "general"
|
| 118 |
+
rag_result = {
|
| 119 |
+
"context": "",
|
| 120 |
+
"hits": [],
|
| 121 |
+
"should_abstain": False,
|
| 122 |
+
"confidence": 0.0,
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
if route in {"krce", "hybrid"}:
|
| 126 |
+
rag_result = search_krce(user_input, rag_index)
|
| 127 |
+
if rag_result["context"]:
|
| 128 |
+
print(f"\n[📦 RAG CONTEXT FOUND]\n{rag_result['context']}\n")
|
| 129 |
+
|
| 130 |
+
if route == "krce" and rag_result["should_abstain"]:
|
| 131 |
+
return {"response": ABSTAIN_MESSAGE}
|
| 132 |
+
|
| 133 |
+
if route == "krce" and rag_result.get("hits"):
|
| 134 |
+
response_text = compose_krce_response(user_input, rag_result)
|
| 135 |
+
return {"response": finalize_krce_response(user_input, response_text, rag_result)}
|
| 136 |
+
|
| 137 |
+
now = datetime.now().strftime("%A, %B %d, %Y")
|
| 138 |
+
if route == "hybrid":
|
| 139 |
+
sys_prompt = build_hybrid_system_prompt(now, rag_result)
|
| 140 |
+
elif route == "general":
|
| 141 |
+
sys_prompt = build_general_system_prompt(now)
|
| 142 |
+
else:
|
| 143 |
+
sys_prompt = build_system_prompt(now, user_input, rag_result)
|
| 144 |
+
|
| 145 |
+
prompt_text = user_input
|
| 146 |
+
if route == "general" and request.history:
|
| 147 |
+
compact_turns: list[str] = []
|
| 148 |
+
for turn in request.history[-8:]:
|
| 149 |
+
role = str(turn.get("role", "")).strip().lower()
|
| 150 |
+
content = str(turn.get("content", "")).strip()
|
| 151 |
+
if role not in {"user", "assistant"} or not content:
|
| 152 |
+
continue
|
| 153 |
+
if len(content) > 1200:
|
| 154 |
+
content = content[:1200].rstrip() + " ..."
|
| 155 |
+
speaker = "User" if role == "user" else "Assistant"
|
| 156 |
+
compact_turns.append(f"{speaker}: {content}")
|
| 157 |
+
if compact_turns:
|
| 158 |
+
prompt_text = (
|
| 159 |
+
"Conversation context (most recent turns):\n"
|
| 160 |
+
+ "\n".join(compact_turns)
|
| 161 |
+
+ "\n\nUser: "
|
| 162 |
+
+ user_input
|
| 163 |
+
+ "\nAssistant:"
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
full_prompt = f"<|system|>\n{sys_prompt}<|end|>\n<|user|>\n{prompt_text}<|end|>\n<|assistant|>\n"
|
| 167 |
+
|
| 168 |
+
# Enforce strict stop tokens to prevent the model from hallucinating user prompts or looping
|
| 169 |
+
stop_tokens = ["<|end|>", "<|endoftext|>", "<|user|>", "<|system|>"]
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
max_allowed = 420 if route == "krce" else 1200
|
| 173 |
+
effective_tokens = max(64, min(int(request.max_tokens), max_allowed))
|
| 174 |
+
effective_temp = min(request.temperature, 0.1) if route == "krce" else min(max(request.temperature, 0.2), 0.6)
|
| 175 |
+
|
| 176 |
+
output = model(
|
| 177 |
+
full_prompt,
|
| 178 |
+
max_tokens=effective_tokens,
|
| 179 |
+
temperature=effective_temp,
|
| 180 |
+
repeat_penalty=1.15, # Prevents text repeating/gibberish loops
|
| 181 |
+
stop=stop_tokens,
|
| 182 |
+
echo=False
|
| 183 |
+
)
|
| 184 |
+
response_text = output["choices"][0]["text"].strip()
|
| 185 |
+
|
| 186 |
+
finish_reason = str(output["choices"][0].get("finish_reason", "")).lower()
|
| 187 |
+
if route == "general" and finish_reason == "length" and response_text:
|
| 188 |
+
continue_prompt = (
|
| 189 |
+
f"{full_prompt}{response_text}\n"
|
| 190 |
+
"Continue from where it stopped. Do not repeat previous lines. "
|
| 191 |
+
"Finish the answer clearly."
|
| 192 |
+
)
|
| 193 |
+
cont = model(
|
| 194 |
+
continue_prompt,
|
| 195 |
+
max_tokens=min(400, max_allowed),
|
| 196 |
+
temperature=max(0.15, min(effective_temp, 0.4)),
|
| 197 |
+
repeat_penalty=1.12,
|
| 198 |
+
stop=stop_tokens,
|
| 199 |
+
echo=False,
|
| 200 |
+
)
|
| 201 |
+
extra = cont["choices"][0]["text"].strip()
|
| 202 |
+
if extra:
|
| 203 |
+
response_text = (response_text + "\n" + extra).strip()
|
| 204 |
+
|
| 205 |
+
if route == "krce":
|
| 206 |
+
return {"response": finalize_krce_response(user_input, response_text, rag_result)}
|
| 207 |
+
return {"response": finalize_general_response(user_input, response_text)}
|
| 208 |
+
except Exception as e:
|
| 209 |
+
return {"response": f"Error: {e}"}
|
| 210 |
+
|
| 211 |
+
if __name__ == "__main__":
|
| 212 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
data/krce_college_data.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/krce_college_data_clean.jsonl
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"instruction":"What is KRCE full name and location?","input":"","output":"K. Ramakrishnan College of Engineering (KRCE) is located at Samayapuram, Tiruchirappalli - 621112.","category":"college_overview"}
|
| 2 |
+
{"instruction":"Tell me KRCE affiliation and accreditation","input":"","output":"KRCE is affiliated to Anna University, Chennai (Autonomous), accredited with NAAC Grade A, and NBA accredited.","category":"college_overview"}
|
| 3 |
+
{"instruction":"Give KRCE main contact details","input":"","output":"Main landline: 0431-2670699. Principal email: principal@krce.ac.in. General email: info@krce.ac.in. Admissions email: admissions@krce.ac.in.","category":"contacts"}
|
| 4 |
+
{"instruction":"Who is the chairman of KRCE?","input":"","output":"Dr. K. Ramakrishnan is the Founder and Chairman of KRCE.","category":"leadership"}
|
| 5 |
+
{"instruction":"Who is the principal of KRCE?","input":"","output":"Dr. D. Srinivasan is the Principal and Head of Institution at KRCE.","category":"leadership"}
|
| 6 |
+
{"instruction":"Who is vice principal in KRCE?","input":"","output":"Dr. T. M. Nithya is the Vice Principal of KRCE.","category":"leadership"}
|
| 7 |
+
{"instruction":"Who is controller of examinations in KRCE?","input":"","output":"Dr. R. Kamaraj is the Controller of Examinations at KRCE.","category":"leadership"}
|
| 8 |
+
{"instruction":"Who is deputy controller of examinations in KRCE?","input":"","output":"Dr. S. Muthuperumal is the Deputy Controller of Examinations at KRCE.","category":"leadership"}
|
| 9 |
+
{"instruction":"Who is head of student affairs in KRCE?","input":"","output":"Dr. K. Lenin is the Head of Student Affairs at KRCE.","category":"leadership"}
|
| 10 |
+
{"instruction":"Who is head of academic affairs in KRCE?","input":"","output":"Dr. K. Dhayalini is the Head of Academic Affairs at KRCE.","category":"leadership"}
|
| 11 |
+
{"instruction":"Who is dean research and placement in KRCE?","input":"","output":"Dr. S. Titus is the Dean (Research and Placement) at KRCE.","category":"leadership"}
|
| 12 |
+
{"instruction":"Who is IQAC coordinator in KRCE?","input":"","output":"Dr. K. Priyadarshini is the IQAC Coordinator at KRCE.","category":"leadership"}
|
| 13 |
+
{"instruction":"What are KRCE working hours?","input":"","output":"KRCE working hours are 8:45 AM to 4:40 PM.","category":"timings"}
|
| 14 |
+
{"instruction":"What are odd semester break timings in KRCE?","input":"","output":"Odd semester breaks: Morning snack 10:45 AM-11:00 AM, Lunch 12:00 PM-1:00 PM, Evening snack 2:50 PM-3:00 PM.","category":"timings"}
|
| 15 |
+
{"instruction":"What are even semester break timings in KRCE?","input":"","output":"Even semester breaks: Morning snack 11:00 AM-11:15 AM, Lunch 1:00 PM-2:00 PM, No evening break.","category":"timings"}
|
| 16 |
+
{"instruction":"What time do KRCE buses leave campus?","input":"","output":"Bus departure: 5:15 PM on normal working days and 5:30 PM on event days.","category":"transport"}
|
| 17 |
+
{"instruction":"Where is CSE department in KRCE?","input":"","output":"The CSE Department is on the Second Floor at KRCE.","category":"floor_map"}
|
| 18 |
+
{"instruction":"Where is EEE department in KRCE?","input":"","output":"The EEE Department is on the Second Floor at KRCE.","category":"floor_map"}
|
| 19 |
+
{"instruction":"Where is ECE department in KRCE?","input":"","output":"The ECE Department is on the Third Floor at KRCE.","category":"floor_map"}
|
| 20 |
+
{"instruction":"Where is AI and DS department in KRCE?","input":"","output":"The AI and DS Department is on the Fourth Floor at KRCE.","category":"floor_map"}
|
| 21 |
+
{"instruction":"Where is IT department in KRCE?","input":"","output":"The IT Department is on the Fifth Floor at KRCE.","category":"floor_map"}
|
| 22 |
+
{"instruction":"Where is CSBS department in KRCE?","input":"","output":"The CSBS Department is on the Fifth Floor at KRCE.","category":"floor_map"}
|
| 23 |
+
{"instruction":"Where is central library in KRCE?","input":"","output":"The Central Library is on the Ground Floor at KRCE.","category":"floor_map"}
|
| 24 |
+
{"instruction":"Lift availability in KRCE","input":"","output":"Lift is available from Ground Floor up to Fifth Floor.","category":"facilities"}
|
| 25 |
+
{"instruction":"Where are washrooms in KRCE floors?","input":"","output":"Ground Floor: Boys washroom. First Floor: Girls washroom near CoE office and boys washroom near Mathematics department. Second Floor and Third Floor: both boys and girls washrooms available.","category":"facilities"}
|
| 26 |
+
{"instruction":"Who is HOD of CSE in KRCE?","input":"","output":"Dr. Sasikumar Rajendran is the HOD of CSE (Associate Professor).","category":"departments"}
|
| 27 |
+
{"instruction":"Who is HOD of AI and DS in KRCE?","input":"","output":"Dr. B. Kiran Bala is the HOD of AI and DS.","category":"departments"}
|
| 28 |
+
{"instruction":"Who is HOD of ECE in KRCE?","input":"","output":"Dr. M. Maheswari is the HOD of ECE.","category":"departments"}
|
| 29 |
+
{"instruction":"Who is HOD of EEE in KRCE?","input":"","output":"Mr. G. Gabriel Santhosh Kumar is the HOD of EEE.","category":"departments"}
|
| 30 |
+
{"instruction":"Who is HOD of Mechanical in KRCE?","input":"","output":"Dr. H. Ramakrishnan is the HOD of Mechanical Engineering.","category":"departments"}
|
| 31 |
+
{"instruction":"Who is HOD of IT in KRCE?","input":"","output":"Dr. S. Manikandan is the HOD of IT.","category":"departments"}
|
| 32 |
+
{"instruction":"Who is HOD of CSBS in KRCE?","input":"","output":"Dr. J. Sasidevi is the HOD of CSBS.","category":"departments"}
|
| 33 |
+
{"instruction":"List staffs of CSE department KRCE","input":"","output":"Dr. Sasikumar Rajendran, Dr. T. M. Nithya, Dr. R. Sridevi, Dr. R. Latha, Dr. L. Amudha, Ms. R. Abirami, Mr. I. Infant Raj, Mrs. R. Nalini, Mr. K. S. Guruprakash, Mr. T. John Peter, Mrs. M. Subashree, Mrs. M. Ruba, Mrs. V. Sathana, Ms. D. Swathi, Mr. V. Kumararaja, Mrs. G. Surya, Mrs. S. Saranya, Mrs. M. Kavitha, Mr. K. Swaminathan, Mrs. R. Nandhini, Mrs. K. Revathi, Mr. P. Kasthuri Rengan, Mrs. N. Nithya, Mrs. S. Rajeswari, Mr. Y. Arockia Jeusraj.","category":"staff"}
|
| 34 |
+
{"instruction":"List staffs of AI and DS department KRCE","input":"","output":"Dr. B. Kiran Bala, Mrs. V. Sankari, Ms. R. Dhaaraani, Mrs. J. Chitra, Mrs. P. Geetha, Dr. T. Suresh, Ms. E. Elamathi, Mrs. S. Jagadeeswari, Mr. M. Karthik, Mr. P. Nagaraj, Mr. M. K. Mohammed Faizal, Mrs. R. K. Ananthi, Mr. M. Ponnivalavan, Mrs. C. Rani, Mrs. M. Kavitha, Mrs. C. Muthuselvi, Mrs. P. Bhavani, Mrs. S. Gayathri, Mrs. R. Kirthiga.","category":"staff"}
|
| 35 |
+
{"instruction":"List staffs of ECE department KRCE","input":"","output":"Dr. M. Maheswari, Dr. C. Jeyalakshmi, Dr. K. Priyadarshini, Dr. B. Viswanathan, Dr. T. Muruganantham, Mr. N. R. Nagarajan, Dr. R. Samson Daniel, Dr. P. Sathees Lingam, Dr. G. Kalpanadevi, Dr. H. Sudarsan, Dr. M. Vinoth, Ms. N. Radha, Mr. R. Balamurugan, Mr. P. Muralikrishnan, Mr. A. Bala Kumar, Mrs. S. Janupriya, Mr. S. Syed Husain, Mr. K. Vigneshwaran, Ms. S. Anusuya, Ms. P. Malini, Ms. R. Mahima, Ms. S. Rajapriya, Dr. S. Stephe, Mr. M. Karthick, Mrs. J. Roselin Suganthi, Mrs. M. Kalaivani.","category":"staff"}
|
| 36 |
+
{"instruction":"List staffs of EEE department KRCE","input":"","output":"Mr. G. Gabriel Santhosh Kumar, Dr. K. Dhayalini, Dr. S. Titus, Dr. R. Ilango, Dr. R. Manivasagam, Dr. R. Arulraj, Mr. A. Jainulafdeen, Mr. A. Subramaniya Siva, Ms. A. Durgadevi, Mr. A. Prabhu, Mr. P. Vigneshwaran, Mr. S. P. Richard, Mr. V. Ashokkumar, Mr. U. Ramani, Mr. P. Parthasarathy, Mr. T. Vadivelan, Mr. M. Senthilkumar.","category":"staff"}
|
| 37 |
+
{"instruction":"List staffs of Mechanical department KRCE","input":"","output":"Dr. H. Ramakrishnan, Dr. K. Lenin, Dr. M. Ravichandran, Dr. D. Jafrey Daniel James, Dr. R. Naveen Kumar, Dr. K. Chellamuthu, Mr. N. Karthikeyan, Mr. B. Prakash, Mr. S. Nandhagopan, Mr. A. Mohana Krishnan, Mr. B. Veluchamy, Mr. M. Manimaran, Mr. S. Raghuvaran, Mr. S. Rajaram, Mr. P. C. Santhosh Kumar, Mr. S. Sivananthan, Mr. V. Venkadesh, Mr. S. Balamurugan, Mr. M. Infant Sebastin Prabu.","category":"staff"}
|
| 38 |
+
{"instruction":"List staffs of IT department KRCE","input":"","output":"Dr. S. Manikandan, Mrs. R. Kamalitta, Dr. S. Kavitha, Mrs. J. Priyadharshini, Ms. N. Pragathi, Ms. C. Soundarya, Mr. R. Arunraj, Mr. M. Santhosh Kumar, Mrs. S. Sathya Priya, Ms. A. Pavithra, Mrs. V. Jayashree, Mrs. F. Vincy Leena, Mrs. B. Juliet Celine Mary, Mrs. A. Jenitha Princy, Mr. B. Senthil Raja Manokar, Ms. B. Rama.","category":"staff"}
|
| 39 |
+
{"instruction":"List staffs of CSBS department KRCE","input":"","output":"Dr. J. Sasidevi, Dr. P. Shanmuga Priya, Mrs. P. Sivamalar, Mrs. M. R. Nithya, Mrs. B. Sathiya, Mrs. N. G. Gayathri, Mrs. P. Umamaheswari, Mrs. M. Kirithika Devi, Mrs. M. Karthika, Mr. P. Ranjith.","category":"staff"}
|
| 40 |
+
{"instruction":"Who is admission coordinator in KRCE?","input":"","output":"Dr. T. Muruganantham is the Admission Coordinator. Contact: 90038 29977.","category":"admin"}
|
| 41 |
+
{"instruction":"Who is librarian in KRCE?","input":"","output":"Mr. P. Krishnamoorthy is the Librarian at KRCE.","category":"admin"}
|
| 42 |
+
{"instruction":"Who is transport manager in KRCE?","input":"","output":"Mr. K. Panneerselvam is the Transport Manager.","category":"admin"}
|
| 43 |
+
{"instruction":"Who is public relations officer in KRCE?","input":"","output":"Mr. S. Prasanna is the Public Relations Officer.","category":"admin"}
|
| 44 |
+
{"instruction":"KRCE anti ragging contact details","input":"","output":"Anti-ragging emergency contact: 98429 91377. Email: principal@krce.ac.in. Chairman: Dr. D. Srinivasan.","category":"safety"}
|
| 45 |
+
{"instruction":"KRCE dress code rules","input":"","output":"Formal dress is mandatory on Wednesdays. Boys: formal shirt tucked in, formal pants, formal shoes. Girls: chudidar with pinned dupatta or saree. Violations can lead to ID card seizure and Rs.100 fine or advisor permission letter.","category":"rules"}
|
| 46 |
+
{"instruction":"Are mobile phones allowed in KRCE campus?","input":"","output":"No. Mobile phones are strictly prohibited inside KRCE campus.","category":"rules"}
|
| 47 |
+
{"instruction":"KRCE outpass and exit rules","input":"","output":"Hostel students need official outpass before leaving campus. Students exiting after semester exams through front gate must submit permission letter. KRCT entrance exit is allowed as per internal movement norms.","category":"rules"}
|
| 48 |
+
{"instruction":"Minimum attendance required in KRCE","input":"","output":"Minimum 75 percent attendance is mandatory to appear for examinations.","category":"rules"}
|
| 49 |
+
{"instruction":"Where is CoE office in KRCE?","input":"","output":"Controller of Examinations office is on the First Floor; students are not permitted to enter directly.","category":"floor_map"}
|
| 50 |
+
{"instruction":"What labs are in CSE floor at KRCE?","input":"","output":"On Second Floor CSE side: Industry Oriented Lab, PG Lab, Internet Lab (Vice Principal area), Compiler Lab, Operating Systems Lab, Data Structures Lab, and Computer Practices Lab.","category":"labs"}
|
| 51 |
+
{"instruction":"What is on ground floor in KRCE?","input":"","output":"Ground Floor includes Chemistry Department and Chemistry labs, Central Library, Mechanical Department, drinking water near library, boys washroom, and staircases near Chemistry and washroom areas.","category":"floor_map"}
|
| 52 |
+
{"instruction":"What is on first floor in KRCE?","input":"","output":"First Floor includes College Office, CoE office, Mathematics Department, Placement and Training, Patent Cell, girls washroom near CoE office, and boys washroom near Mathematics department.","category":"floor_map"}
|
| 53 |
+
{"instruction":"What is on third floor in KRCE?","input":"","output":"Third Floor serves First Year, including Physics and Chemistry (First Year) with Physics Lab; also ECE department and labs, with both boys and girls washrooms.","category":"floor_map"}
|
| 54 |
+
{"instruction":"What is on fourth floor in KRCE?","input":"","output":"Fourth Floor includes AI and DS / AI and ML areas, mechanical classrooms and staff rooms, three AI/DS labs, conference hall, and IT staff rooms.","category":"floor_map"}
|
| 55 |
+
{"instruction":"What is on fifth floor in KRCE?","input":"","output":"Fifth Floor includes IT Department and CSBS Department along with their labs.","category":"floor_map"}
|
| 56 |
+
{"instruction":"KRCE principal contact","input":"","output":"Principal: Dr. D. Srinivasan. Contact: 98429 91377. Email: principal@krce.ac.in.","category":"contacts"}
|
| 57 |
+
{"instruction":"KRCE office and admissions contacts","input":"","output":"College Office main: 98429 91377; Landline: 0431 2670699; Alternate: 73732 84777; Admissions contact: 90038 29977; Email: admissions@krce.ac.in.","category":"contacts"}
|
| 58 |
+
{"instruction":"KRCE exam office contact","input":"","output":"Controller of Examinations: Dr. R. Kamaraj. CoE office contact uses college office number: 98429 91377.","category":"contacts"}
|
rag_utils.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from functools import lru_cache
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
from sentence_transformers import SentenceTransformer
|
| 13 |
+
|
| 14 |
+
DEFAULT_DATA_FILE = os.path.join(os.path.dirname(__file__), "data", "krce_college_data.jsonl")
|
| 15 |
+
DEFAULT_EMBEDDING_MODEL = "all-MiniLM-L6-v2"
|
| 16 |
+
ABSTAIN_MESSAGE = "I don't know from the KRCE knowledge base."
|
| 17 |
+
|
| 18 |
+
# Keep this simple: only a minimal relevance threshold.
|
| 19 |
+
MIN_CONFIDENCE = 0.25
|
| 20 |
+
TOP_K = 3
|
| 21 |
+
|
| 22 |
+
SEARCH_STOPWORDS = {
|
| 23 |
+
"a", "an", "and", "are", "at", "be", "for", "from", "how", "in", "is", "it", "of", "on", "or",
|
| 24 |
+
"the", "to", "what", "when", "where", "who", "with", "your", "please", "tell", "me", "about",
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
# Lightweight post-generation safety net.
|
| 28 |
+
HALLUCINATION_MARKERS = (
|
| 29 |
+
"created by",
|
| 30 |
+
"created independently",
|
| 31 |
+
"created after leaving",
|
| 32 |
+
"des created me",
|
| 33 |
+
"i was created",
|
| 34 |
+
"krish cs my creator",
|
| 35 |
+
"my creator",
|
| 36 |
+
"my founder",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
GENERAL_KNOWLEDGE_MARKERS = (
|
| 40 |
+
"algorithm",
|
| 41 |
+
"array",
|
| 42 |
+
"binary tree",
|
| 43 |
+
"coding",
|
| 44 |
+
"computer science",
|
| 45 |
+
"data structure",
|
| 46 |
+
"debug",
|
| 47 |
+
"explain",
|
| 48 |
+
"merge sort",
|
| 49 |
+
"python",
|
| 50 |
+
"quick sort",
|
| 51 |
+
"sorting",
|
| 52 |
+
"stack",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
LIST_QUERY_MARKERS = (
|
| 56 |
+
"all",
|
| 57 |
+
"boys",
|
| 58 |
+
"faculty",
|
| 59 |
+
"faculties",
|
| 60 |
+
"girls",
|
| 61 |
+
"list",
|
| 62 |
+
"members",
|
| 63 |
+
"restroom",
|
| 64 |
+
"restrooms",
|
| 65 |
+
"staff",
|
| 66 |
+
"staffs",
|
| 67 |
+
"washroom",
|
| 68 |
+
"washrooms",
|
| 69 |
+
"who are",
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
TRAILING_QUERY_NOISE_MARKERS = (
|
| 73 |
+
", tell me about ",
|
| 74 |
+
", who are ",
|
| 75 |
+
", who is ",
|
| 76 |
+
", how many ",
|
| 77 |
+
", i m a cse student",
|
| 78 |
+
", i am a cse student",
|
| 79 |
+
", is dr ",
|
| 80 |
+
", krce cse",
|
| 81 |
+
", my hod if",
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
NAME_PATTERN = re.compile(r"\b(?:Dr|Mr|Mrs|Ms)\.\s*[A-Za-z][A-Za-z\s.]{1,70}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@dataclass(frozen=True)
|
| 88 |
+
class RagIndex:
|
| 89 |
+
model: SentenceTransformer | None
|
| 90 |
+
records: list[dict[str, str]]
|
| 91 |
+
documents: list[str]
|
| 92 |
+
embeddings: np.ndarray | None
|
| 93 |
+
tokenized_documents: list[list[str]]
|
| 94 |
+
idf: dict[str, float]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def normalize_text(text: str) -> str:
|
| 98 |
+
text = text.lower().replace("'", " ").replace("/", " ").replace("-", " ")
|
| 99 |
+
text = re.sub(r"[^a-z0-9\s.]+", " ", text)
|
| 100 |
+
text = text.replace(".", " ")
|
| 101 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _tokenize_for_search(text: str) -> list[str]:
|
| 105 |
+
normalized = normalize_text(text)
|
| 106 |
+
tokens = [token for token in normalized.split() if token and token not in SEARCH_STOPWORDS]
|
| 107 |
+
return tokens
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _build_idf(tokenized_documents: list[list[str]]) -> dict[str, float]:
|
| 111 |
+
if not tokenized_documents:
|
| 112 |
+
return {}
|
| 113 |
+
|
| 114 |
+
doc_freq: dict[str, int] = {}
|
| 115 |
+
total_docs = len(tokenized_documents)
|
| 116 |
+
for tokens in tokenized_documents:
|
| 117 |
+
unique_tokens = set(tokens)
|
| 118 |
+
for token in unique_tokens:
|
| 119 |
+
doc_freq[token] = doc_freq.get(token, 0) + 1
|
| 120 |
+
|
| 121 |
+
idf: dict[str, float] = {}
|
| 122 |
+
for token, freq in doc_freq.items():
|
| 123 |
+
idf[token] = math.log((total_docs + 1.0) / (freq + 1.0)) + 1.0
|
| 124 |
+
return idf
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _lexical_score(query_tokens: list[str], doc_tokens: list[str], idf: dict[str, float]) -> float:
|
| 128 |
+
if not query_tokens or not doc_tokens:
|
| 129 |
+
return 0.0
|
| 130 |
+
|
| 131 |
+
doc_set = set(doc_tokens)
|
| 132 |
+
weighted_overlap = sum(idf.get(token, 1.0) for token in query_tokens if token in doc_set)
|
| 133 |
+
weighted_total = sum(idf.get(token, 1.0) for token in query_tokens)
|
| 134 |
+
if weighted_total <= 0:
|
| 135 |
+
return 0.0
|
| 136 |
+
return weighted_overlap / weighted_total
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _clean_output_text(output: str) -> str:
|
| 140 |
+
cleaned = output.strip()
|
| 141 |
+
lowered = cleaned.lower()
|
| 142 |
+
|
| 143 |
+
cut_positions = []
|
| 144 |
+
for marker in TRAILING_QUERY_NOISE_MARKERS:
|
| 145 |
+
pos = lowered.find(marker)
|
| 146 |
+
if pos != -1:
|
| 147 |
+
cut_positions.append(pos)
|
| 148 |
+
|
| 149 |
+
if cut_positions:
|
| 150 |
+
cleaned = cleaned[: min(cut_positions)].rstrip(" ,;")
|
| 151 |
+
|
| 152 |
+
return cleaned
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def is_krce_scope_query(query: str) -> bool:
|
| 156 |
+
normalized = normalize_text(query)
|
| 157 |
+
# Minimal scope check to decide when to force abstain on low confidence.
|
| 158 |
+
krce_terms = (
|
| 159 |
+
"krce",
|
| 160 |
+
"k ramakrishnan",
|
| 161 |
+
"college",
|
| 162 |
+
"department",
|
| 163 |
+
"faculty",
|
| 164 |
+
"hod",
|
| 165 |
+
"principal",
|
| 166 |
+
"professor",
|
| 167 |
+
"cse",
|
| 168 |
+
"ece",
|
| 169 |
+
"eee",
|
| 170 |
+
"ai ds",
|
| 171 |
+
"aids",
|
| 172 |
+
"csbs",
|
| 173 |
+
)
|
| 174 |
+
return any(term in normalized for term in krce_terms)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def classify_query_route(query: str) -> str:
|
| 178 |
+
normalized = normalize_text(query)
|
| 179 |
+
krce_scope = is_krce_scope_query(query)
|
| 180 |
+
general_scope = any(marker in normalized for marker in GENERAL_KNOWLEDGE_MARKERS)
|
| 181 |
+
|
| 182 |
+
if krce_scope and general_scope:
|
| 183 |
+
return "hybrid"
|
| 184 |
+
if krce_scope:
|
| 185 |
+
return "krce"
|
| 186 |
+
return "general"
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _load_records(data_file: str) -> list[dict[str, str]]:
|
| 190 |
+
records: list[dict[str, str]] = []
|
| 191 |
+
with open(data_file, "r", encoding="utf-8") as handle:
|
| 192 |
+
for line in handle:
|
| 193 |
+
if not line.strip():
|
| 194 |
+
continue
|
| 195 |
+
try:
|
| 196 |
+
item = json.loads(line)
|
| 197 |
+
except json.JSONDecodeError:
|
| 198 |
+
continue
|
| 199 |
+
|
| 200 |
+
instruction = str(item.get("instruction", "")).strip()
|
| 201 |
+
output = _clean_output_text(str(item.get("output", "")))
|
| 202 |
+
if not instruction and not output:
|
| 203 |
+
continue
|
| 204 |
+
|
| 205 |
+
records.append(
|
| 206 |
+
{
|
| 207 |
+
"instruction": instruction,
|
| 208 |
+
"output": output,
|
| 209 |
+
}
|
| 210 |
+
)
|
| 211 |
+
return records
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@lru_cache(maxsize=2)
|
| 215 |
+
def load_rag_index(data_file: str = DEFAULT_DATA_FILE, embedding_model: str = DEFAULT_EMBEDDING_MODEL) -> RagIndex:
|
| 216 |
+
if not os.path.exists(data_file):
|
| 217 |
+
return RagIndex(model=None, records=[], documents=[], embeddings=None, tokenized_documents=[], idf={})
|
| 218 |
+
|
| 219 |
+
try:
|
| 220 |
+
model = SentenceTransformer(embedding_model)
|
| 221 |
+
except Exception:
|
| 222 |
+
return RagIndex(model=None, records=[], documents=[], embeddings=None, tokenized_documents=[], idf={})
|
| 223 |
+
|
| 224 |
+
records = _load_records(data_file)
|
| 225 |
+
documents = [f"{record['instruction']}\n{record['output']}".strip() for record in records]
|
| 226 |
+
|
| 227 |
+
if documents:
|
| 228 |
+
embeddings = model.encode(documents, normalize_embeddings=True, convert_to_numpy=True)
|
| 229 |
+
else:
|
| 230 |
+
embeddings = np.empty((0, 0), dtype=np.float32)
|
| 231 |
+
|
| 232 |
+
tokenized_documents = [_tokenize_for_search(doc) for doc in documents]
|
| 233 |
+
idf = _build_idf(tokenized_documents)
|
| 234 |
+
|
| 235 |
+
return RagIndex(
|
| 236 |
+
model=model,
|
| 237 |
+
records=records,
|
| 238 |
+
documents=documents,
|
| 239 |
+
embeddings=embeddings,
|
| 240 |
+
tokenized_documents=tokenized_documents,
|
| 241 |
+
idf=idf,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def search_krce(query: str, rag_index: RagIndex, top_k: int = TOP_K) -> dict[str, Any]:
|
| 246 |
+
if rag_index.model is None or rag_index.embeddings is None or not rag_index.records:
|
| 247 |
+
return {
|
| 248 |
+
"query": query,
|
| 249 |
+
"context": "",
|
| 250 |
+
"hits": [],
|
| 251 |
+
"confidence": 0.0,
|
| 252 |
+
"should_abstain": True,
|
| 253 |
+
"abstain_reason": "RAG index is unavailable.",
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
query_embedding = rag_index.model.encode([query], normalize_embeddings=True, convert_to_numpy=True)[0]
|
| 257 |
+
vector_scores = np.dot(rag_index.embeddings, query_embedding).astype(float)
|
| 258 |
+
|
| 259 |
+
query_tokens = _tokenize_for_search(query)
|
| 260 |
+
lexical_scores = np.array(
|
| 261 |
+
[_lexical_score(query_tokens, doc_tokens, rag_index.idf) for doc_tokens in rag_index.tokenized_documents],
|
| 262 |
+
dtype=float,
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
# Hybrid ranking: dense similarity for semantics + lexical overlap for exact KRCE entities.
|
| 266 |
+
scores = (0.78 * vector_scores) + (0.22 * lexical_scores)
|
| 267 |
+
|
| 268 |
+
if scores.size == 0:
|
| 269 |
+
return {
|
| 270 |
+
"query": query,
|
| 271 |
+
"context": "",
|
| 272 |
+
"hits": [],
|
| 273 |
+
"confidence": 0.0,
|
| 274 |
+
"should_abstain": True,
|
| 275 |
+
"abstain_reason": ABSTAIN_MESSAGE,
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
ranked_indices = scores.argsort()[::-1]
|
| 279 |
+
best_score = float(scores[ranked_indices[0]])
|
| 280 |
+
|
| 281 |
+
if best_score < MIN_CONFIDENCE:
|
| 282 |
+
return {
|
| 283 |
+
"query": query,
|
| 284 |
+
"context": "",
|
| 285 |
+
"hits": [],
|
| 286 |
+
"confidence": best_score,
|
| 287 |
+
"should_abstain": True,
|
| 288 |
+
"abstain_reason": ABSTAIN_MESSAGE,
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
selected_indices = ranked_indices[: max(top_k, 5)]
|
| 292 |
+
hits: list[dict[str, Any]] = []
|
| 293 |
+
blocks: list[str] = []
|
| 294 |
+
|
| 295 |
+
for rank, idx in enumerate(selected_indices, start=1):
|
| 296 |
+
score = float(scores[idx])
|
| 297 |
+
vector_score = float(vector_scores[idx])
|
| 298 |
+
lexical_score = float(lexical_scores[idx])
|
| 299 |
+
record = rag_index.records[int(idx)]
|
| 300 |
+
hits.append(
|
| 301 |
+
{
|
| 302 |
+
"rank": rank,
|
| 303 |
+
"instruction": record["instruction"],
|
| 304 |
+
"output": record["output"],
|
| 305 |
+
"combined_score": score,
|
| 306 |
+
"vector_score": vector_score,
|
| 307 |
+
"lexical_score": lexical_score,
|
| 308 |
+
"specific_overlap": 0.0,
|
| 309 |
+
"role_overlap": 0.0,
|
| 310 |
+
}
|
| 311 |
+
)
|
| 312 |
+
blocks.append(
|
| 313 |
+
f"[KB-{rank} | score={score:.3f}]\n"
|
| 314 |
+
f"Question: {record['instruction']}\n"
|
| 315 |
+
f"Answer: {record['output']}"
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
return {
|
| 319 |
+
"query": query,
|
| 320 |
+
"context": "\n\n".join(blocks),
|
| 321 |
+
"hits": hits,
|
| 322 |
+
"confidence": best_score,
|
| 323 |
+
"should_abstain": False,
|
| 324 |
+
"abstain_reason": "",
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def build_system_prompt(now: str, query: str, rag_result: dict[str, Any] | None) -> str:
|
| 329 |
+
prompt = (
|
| 330 |
+
f"You are Krish Mind, a grounded assistant for KRCE.\n"
|
| 331 |
+
f"CURRENT TIME: {now}\n\n"
|
| 332 |
+
"RULES:\n"
|
| 333 |
+
"- For KRCE facts, answer only from the KRCE evidence block.\n"
|
| 334 |
+
"- Synthesize the final answer in your own words; do not copy long raw blocks.\n"
|
| 335 |
+
"- Remove duplicates and repeated names.\n"
|
| 336 |
+
"- For list-style queries, return a clean bullet list.\n"
|
| 337 |
+
"- If the evidence does not directly answer, reply exactly: I don't know from the KRCE knowledge base.\n"
|
| 338 |
+
"- Do not invent people, roles, creator/founder claims, or hidden details.\n"
|
| 339 |
+
"- Keep the answer short and factual.\n"
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
if rag_result and rag_result.get("context"):
|
| 343 |
+
prompt += (
|
| 344 |
+
f"\n[KRCE EVIDENCE]\n{rag_result['context']}\n[END KRCE EVIDENCE]\n"
|
| 345 |
+
"Use this evidence only."
|
| 346 |
+
)
|
| 347 |
+
else:
|
| 348 |
+
prompt += "\nNo KRCE evidence was retrieved."
|
| 349 |
+
|
| 350 |
+
return prompt
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def build_general_system_prompt(now: str) -> str:
|
| 354 |
+
return (
|
| 355 |
+
f"You are Krish Mind, a helpful AI assistant.\n"
|
| 356 |
+
f"CURRENT TIME: {now}\n\n"
|
| 357 |
+
"RULES:\n"
|
| 358 |
+
"- Answer clearly and accurately using your own knowledge.\n"
|
| 359 |
+
"- Keep replies compact by default (typically 4-10 lines unless user asks for full detail).\n"
|
| 360 |
+
"- Use clean Markdown: short paragraphs, bullets for lists, fenced code blocks for code.\n"
|
| 361 |
+
"- Avoid very long single lines; wrap explanations into readable short lines.\n"
|
| 362 |
+
"- Do not mention creator/founder identity unless the user explicitly asks about it.\n"
|
| 363 |
+
"- Do not claim personal origin stories that are not asked by the user.\n"
|
| 364 |
+
"- Keep answers concise and structured.\n"
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def build_hybrid_system_prompt(now: str, rag_result: dict[str, Any] | None) -> str:
|
| 369 |
+
prompt = (
|
| 370 |
+
f"You are Krish Mind, a helpful AI assistant for KRCE-related questions.\n"
|
| 371 |
+
f"CURRENT TIME: {now}\n\n"
|
| 372 |
+
"RULES:\n"
|
| 373 |
+
"- Use KRCE evidence when available for college-specific facts.\n"
|
| 374 |
+
"- For general explanation details not present in KRCE evidence, use your own knowledge.\n"
|
| 375 |
+
"- Do not invent creator/founder identity claims.\n"
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
if rag_result and rag_result.get("context"):
|
| 379 |
+
prompt += f"\n[KRCE EVIDENCE]\n{rag_result['context']}\n[END KRCE EVIDENCE]\n"
|
| 380 |
+
|
| 381 |
+
return prompt
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def looks_like_hallucinated_identity_claim(text: str) -> bool:
|
| 385 |
+
normalized = normalize_text(text)
|
| 386 |
+
return any(marker in normalized for marker in HALLUCINATION_MARKERS)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def _contains_code_content(text: str) -> bool:
|
| 390 |
+
lowered = text.lower()
|
| 391 |
+
if "```" in text:
|
| 392 |
+
return True
|
| 393 |
+
code_markers = (
|
| 394 |
+
"def ",
|
| 395 |
+
"class ",
|
| 396 |
+
"#include",
|
| 397 |
+
"public static void main",
|
| 398 |
+
"void ",
|
| 399 |
+
"int main",
|
| 400 |
+
)
|
| 401 |
+
return any(marker in lowered for marker in code_markers)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def _remove_identity_lines(text: str) -> str:
|
| 405 |
+
lines = text.splitlines()
|
| 406 |
+
kept = []
|
| 407 |
+
for line in lines:
|
| 408 |
+
if looks_like_hallucinated_identity_claim(line):
|
| 409 |
+
continue
|
| 410 |
+
kept.append(line)
|
| 411 |
+
cleaned = "\n".join(kept).strip()
|
| 412 |
+
return cleaned
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def _is_generic_self_intro(text: str) -> bool:
|
| 416 |
+
normalized = normalize_text(text)
|
| 417 |
+
if not normalized:
|
| 418 |
+
return False
|
| 419 |
+
intro_prefixes = (
|
| 420 |
+
"i am krish mind",
|
| 421 |
+
"i m krish mind",
|
| 422 |
+
"hello i am krish mind",
|
| 423 |
+
"hi i am krish mind",
|
| 424 |
+
)
|
| 425 |
+
return any(normalized.startswith(prefix) for prefix in intro_prefixes)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def is_generic_self_intro(text: str) -> bool:
|
| 429 |
+
return _is_generic_self_intro(text)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def is_intro_or_identity_query(query: str) -> bool:
|
| 433 |
+
normalized = normalize_text(query)
|
| 434 |
+
intro_markers = (
|
| 435 |
+
"hi",
|
| 436 |
+
"hello",
|
| 437 |
+
"hey",
|
| 438 |
+
"good morning",
|
| 439 |
+
"good afternoon",
|
| 440 |
+
"good evening",
|
| 441 |
+
"who are you",
|
| 442 |
+
"introduce yourself",
|
| 443 |
+
"your name",
|
| 444 |
+
"tell me about yourself",
|
| 445 |
+
)
|
| 446 |
+
return any(marker in normalized for marker in intro_markers)
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def _extract_people_names(text: str) -> list[str]:
|
| 450 |
+
found = NAME_PATTERN.findall(text)
|
| 451 |
+
cleaned: list[str] = []
|
| 452 |
+
seen = set()
|
| 453 |
+
for item in found:
|
| 454 |
+
name = re.sub(r"\s+", " ", item).strip(" ,.;")
|
| 455 |
+
name = re.sub(r"\s+(at|in)\s+krce\b", "", name, flags=re.IGNORECASE)
|
| 456 |
+
name = re.sub(r"\s+in\s+(cse|ece|eee|it|csbs|aids)\b", "", name, flags=re.IGNORECASE)
|
| 457 |
+
name = re.sub(r"\.(\s*(professors?|labs?|department).*)$", "", name, flags=re.IGNORECASE)
|
| 458 |
+
name = name.strip(" ,.;")
|
| 459 |
+
key = normalize_text(name)
|
| 460 |
+
if len(name) < 6:
|
| 461 |
+
continue
|
| 462 |
+
if any(bad in key for bad in ("professor", "lab", "department", "krce", "tell me", "who are")):
|
| 463 |
+
continue
|
| 464 |
+
if "tell me about" in key or "who are" in key:
|
| 465 |
+
continue
|
| 466 |
+
if key in seen:
|
| 467 |
+
continue
|
| 468 |
+
seen.add(key)
|
| 469 |
+
cleaned.append(name)
|
| 470 |
+
return cleaned
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def build_deterministic_krce_answer(query: str, rag_result: dict[str, Any]) -> str:
|
| 474 |
+
normalized_query = normalize_text(query)
|
| 475 |
+
location_intent = ("where" in normalized_query and "department" in normalized_query)
|
| 476 |
+
list_intent = any(marker in normalized_query for marker in ("staff", "staffs", "faculty", "members", "list"))
|
| 477 |
+
factual_direct_intent = any(
|
| 478 |
+
token in normalized_query
|
| 479 |
+
for token in (
|
| 480 |
+
"who is",
|
| 481 |
+
"principal",
|
| 482 |
+
"chairman",
|
| 483 |
+
"vice principal",
|
| 484 |
+
"controller of examinations",
|
| 485 |
+
"deputy controller",
|
| 486 |
+
"hod",
|
| 487 |
+
"coordinator",
|
| 488 |
+
"contact",
|
| 489 |
+
"email",
|
| 490 |
+
"working hours",
|
| 491 |
+
"bus",
|
| 492 |
+
"attendance",
|
| 493 |
+
"mobile phone",
|
| 494 |
+
"dress code",
|
| 495 |
+
)
|
| 496 |
+
)
|
| 497 |
+
if not list_intent and not location_intent and not factual_direct_intent:
|
| 498 |
+
return ""
|
| 499 |
+
|
| 500 |
+
hits = rag_result.get("hits") or []
|
| 501 |
+
if not hits:
|
| 502 |
+
return ""
|
| 503 |
+
|
| 504 |
+
department_key = ""
|
| 505 |
+
for dep in ("cse", "ece", "eee", "it", "csbs", "ai ds", "aids"):
|
| 506 |
+
if re.search(rf"\b{re.escape(dep)}\b", normalized_query):
|
| 507 |
+
department_key = dep
|
| 508 |
+
break
|
| 509 |
+
|
| 510 |
+
filtered_hits = hits
|
| 511 |
+
if department_key:
|
| 512 |
+
scoped_hits = []
|
| 513 |
+
for hit in hits:
|
| 514 |
+
merged = f"{hit.get('instruction', '')} {hit.get('output', '')}"
|
| 515 |
+
if re.search(rf"\b{re.escape(department_key)}\b", normalize_text(merged)):
|
| 516 |
+
scoped_hits.append(hit)
|
| 517 |
+
if scoped_hits:
|
| 518 |
+
filtered_hits = scoped_hits
|
| 519 |
+
|
| 520 |
+
if factual_direct_intent and not list_intent and not location_intent:
|
| 521 |
+
if filtered_hits:
|
| 522 |
+
first = str(filtered_hits[0].get("output", "")).strip()
|
| 523 |
+
if first:
|
| 524 |
+
return first
|
| 525 |
+
|
| 526 |
+
if location_intent:
|
| 527 |
+
floor_pattern = re.compile(r"\b(ground|first|second|third|fourth|fifth)\s+floor\b", re.IGNORECASE)
|
| 528 |
+
for hit in filtered_hits:
|
| 529 |
+
output = str(hit.get("output", ""))
|
| 530 |
+
floor_match = floor_pattern.search(output)
|
| 531 |
+
if floor_match:
|
| 532 |
+
sentence = output.strip().split(".")[0].strip()
|
| 533 |
+
if sentence:
|
| 534 |
+
return sentence + "."
|
| 535 |
+
|
| 536 |
+
all_names: list[str] = []
|
| 537 |
+
seen = set()
|
| 538 |
+
for hit in filtered_hits:
|
| 539 |
+
output = str(hit.get("output", ""))
|
| 540 |
+
for name in _extract_people_names(output):
|
| 541 |
+
key = normalize_text(name)
|
| 542 |
+
if key in seen:
|
| 543 |
+
continue
|
| 544 |
+
seen.add(key)
|
| 545 |
+
all_names.append(name)
|
| 546 |
+
|
| 547 |
+
if not all_names:
|
| 548 |
+
return ""
|
| 549 |
+
|
| 550 |
+
if re.search(r"\b(male|boys|boy)\b", normalized_query):
|
| 551 |
+
filtered = [name for name in all_names if name.startswith(("Mr.",))]
|
| 552 |
+
if filtered:
|
| 553 |
+
all_names = filtered
|
| 554 |
+
elif re.search(r"\b(female|girls|girl)\b", normalized_query):
|
| 555 |
+
filtered = [name for name in all_names if name.startswith(("Mrs.", "Ms."))]
|
| 556 |
+
if filtered:
|
| 557 |
+
all_names = filtered
|
| 558 |
+
|
| 559 |
+
department = ""
|
| 560 |
+
for dep in ("cse", "ece", "eee", "it", "csbs", "ai ds", "aids"):
|
| 561 |
+
if dep in normalized_query:
|
| 562 |
+
department = dep.upper()
|
| 563 |
+
break
|
| 564 |
+
|
| 565 |
+
heading = f"{department} staff list:" if department else "Staff list:"
|
| 566 |
+
bullet_lines = "\n".join(f"- {name}" for name in all_names[:60])
|
| 567 |
+
return f"{heading}\n{bullet_lines}"
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def compose_krce_response(query: str, rag_result: dict[str, Any]) -> str:
|
| 571 |
+
hits = rag_result.get("hits") or []
|
| 572 |
+
if not hits:
|
| 573 |
+
return ABSTAIN_MESSAGE
|
| 574 |
+
|
| 575 |
+
normalized_query = normalize_text(query)
|
| 576 |
+
is_list_query = any(marker in normalized_query for marker in LIST_QUERY_MARKERS)
|
| 577 |
+
|
| 578 |
+
if not is_list_query:
|
| 579 |
+
return str(hits[0].get("output", "")).strip() or ABSTAIN_MESSAGE
|
| 580 |
+
|
| 581 |
+
unique_outputs: list[str] = []
|
| 582 |
+
seen = set()
|
| 583 |
+
for hit in hits:
|
| 584 |
+
output = str(hit.get("output", "")).strip()
|
| 585 |
+
if not output:
|
| 586 |
+
continue
|
| 587 |
+
key = normalize_text(output)
|
| 588 |
+
if key in seen:
|
| 589 |
+
continue
|
| 590 |
+
seen.add(key)
|
| 591 |
+
unique_outputs.append(output)
|
| 592 |
+
|
| 593 |
+
if not unique_outputs:
|
| 594 |
+
return ABSTAIN_MESSAGE
|
| 595 |
+
|
| 596 |
+
if len(unique_outputs) == 1:
|
| 597 |
+
return unique_outputs[0]
|
| 598 |
+
|
| 599 |
+
return "\n".join(f"- {line}" for line in unique_outputs)
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def finalize_krce_response(query: str, response_text: str, rag_result: dict[str, Any] | None) -> str:
|
| 603 |
+
if not response_text:
|
| 604 |
+
return ABSTAIN_MESSAGE if is_krce_scope_query(query) else response_text
|
| 605 |
+
|
| 606 |
+
if is_krce_scope_query(query):
|
| 607 |
+
if looks_like_hallucinated_identity_claim(response_text):
|
| 608 |
+
return ABSTAIN_MESSAGE
|
| 609 |
+
|
| 610 |
+
if rag_result and rag_result.get("should_abstain"):
|
| 611 |
+
return ABSTAIN_MESSAGE
|
| 612 |
+
|
| 613 |
+
return response_text
|
| 614 |
+
|
| 615 |
+
|
| 616 |
+
def finalize_general_response(query: str, response_text: str) -> str:
|
| 617 |
+
if not response_text:
|
| 618 |
+
return response_text
|
| 619 |
+
|
| 620 |
+
normalized_query = normalize_text(query)
|
| 621 |
+
identity_query = any(token in normalized_query for token in ("who created", "creator", "founder", "who are you"))
|
| 622 |
+
intro_query = is_intro_or_identity_query(query)
|
| 623 |
+
if identity_query:
|
| 624 |
+
return response_text
|
| 625 |
+
|
| 626 |
+
if intro_query:
|
| 627 |
+
return response_text
|
| 628 |
+
|
| 629 |
+
# For code answers, do not aggressively trim the full response.
|
| 630 |
+
if _contains_code_content(response_text):
|
| 631 |
+
cleaned_code_answer = _remove_identity_lines(response_text)
|
| 632 |
+
return cleaned_code_answer or response_text
|
| 633 |
+
|
| 634 |
+
if looks_like_hallucinated_identity_claim(response_text):
|
| 635 |
+
cleaned = response_text
|
| 636 |
+
lowered = normalize_text(response_text)
|
| 637 |
+
cut_positions = [lowered.find(marker) for marker in HALLUCINATION_MARKERS if lowered.find(marker) != -1]
|
| 638 |
+
if cut_positions:
|
| 639 |
+
cut = min(cut_positions)
|
| 640 |
+
cleaned = response_text[:cut].rstrip(" ,.;")
|
| 641 |
+
if cleaned:
|
| 642 |
+
return cleaned
|
| 643 |
+
return "I can help with this topic. Please ask the question directly and I will answer clearly."
|
| 644 |
+
|
| 645 |
+
return response_text
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def needs_general_retry(query: str, response_text: str) -> bool:
|
| 649 |
+
if not response_text:
|
| 650 |
+
return True
|
| 651 |
+
|
| 652 |
+
normalized_query = normalize_text(query)
|
| 653 |
+
identity_query = any(token in normalized_query for token in ("who created", "creator", "founder", "who are you"))
|
| 654 |
+
if identity_query:
|
| 655 |
+
return False
|
| 656 |
+
|
| 657 |
+
if is_intro_or_identity_query(query):
|
| 658 |
+
return False
|
| 659 |
+
|
| 660 |
+
if _is_generic_self_intro(response_text):
|
| 661 |
+
return True
|
| 662 |
+
|
| 663 |
+
# Avoid forcing retries for long-form coding answers; retries can degrade code quality.
|
| 664 |
+
if _contains_code_content(response_text):
|
| 665 |
+
return False
|
| 666 |
+
|
| 667 |
+
return looks_like_hallucinated_identity_claim(response_text)
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn==0.27.0
|
| 3 |
+
pydantic==2.6.0
|
| 4 |
+
sentence-transformers>=2.2.2
|
| 5 |
+
numpy>=1.26.0
|
| 6 |
+
scikit-learn
|
| 7 |
+
duckduckgo-search>=5.0
|
| 8 |
+
python-multipart
|
| 9 |
+
huggingface_hub>=0.20.0
|
static/index.html
ADDED
|
@@ -0,0 +1,1804 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>Krish Mind AI</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
| 10 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
| 11 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 12 |
+
<style>
|
| 13 |
+
:root {
|
| 14 |
+
/* Light Theme */
|
| 15 |
+
--bg-primary: #ffffff;
|
| 16 |
+
--bg-secondary: #f7f7f8;
|
| 17 |
+
--bg-tertiary: #ececf1;
|
| 18 |
+
--bg-sidebar: #f9f9f9;
|
| 19 |
+
--bg-hover: #f0f0f5;
|
| 20 |
+
--bg-input: #ffffff;
|
| 21 |
+
--text-primary: #1a1a1a;
|
| 22 |
+
--text-secondary: #5d5d5d;
|
| 23 |
+
--text-muted: #8e8ea0;
|
| 24 |
+
--border: #e5e5e5;
|
| 25 |
+
--border-light: #f0f0f0;
|
| 26 |
+
--accent: #0066cc;
|
| 27 |
+
--accent-gradient: linear-gradient(135deg, #1e3a5f 0%, #2d8cbe 50%, #40c9c9 100%);
|
| 28 |
+
--user-bg: #f7f7f8;
|
| 29 |
+
--assistant-bg: #ffffff;
|
| 30 |
+
--code-bg: #1e1e1e;
|
| 31 |
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
| 32 |
+
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
[data-theme="dark"] {
|
| 36 |
+
--bg-primary: #212121;
|
| 37 |
+
--bg-secondary: #2f2f2f;
|
| 38 |
+
--bg-tertiary: #3a3a3a;
|
| 39 |
+
--bg-sidebar: #171717;
|
| 40 |
+
--bg-hover: #3a3a3a;
|
| 41 |
+
--bg-input: #2f2f2f;
|
| 42 |
+
--text-primary: #ececec;
|
| 43 |
+
--text-secondary: #b4b4b4;
|
| 44 |
+
--text-muted: #8e8ea0;
|
| 45 |
+
--border: #3a3a3a;
|
| 46 |
+
--border-light: #2f2f2f;
|
| 47 |
+
--accent: #40c9c9;
|
| 48 |
+
--user-bg: #2f2f2f;
|
| 49 |
+
--assistant-bg: #212121;
|
| 50 |
+
--code-bg: #0d0d0d;
|
| 51 |
+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
| 52 |
+
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
* {
|
| 56 |
+
margin: 0;
|
| 57 |
+
padding: 0;
|
| 58 |
+
box-sizing: border-box;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
body {
|
| 62 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 63 |
+
background: var(--bg-primary);
|
| 64 |
+
color: var(--text-primary);
|
| 65 |
+
height: 100vh;
|
| 66 |
+
display: flex;
|
| 67 |
+
transition: background 0.3s, color 0.3s;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* Sidebar */
|
| 71 |
+
.sidebar {
|
| 72 |
+
width: 260px;
|
| 73 |
+
background: var(--bg-sidebar);
|
| 74 |
+
border-right: 1px solid var(--border);
|
| 75 |
+
display: flex;
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
transition: transform 0.3s, background 0.3s;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.sidebar-header {
|
| 81 |
+
padding: 12px;
|
| 82 |
+
border-bottom: 1px solid var(--border);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.new-chat-btn {
|
| 86 |
+
width: 100%;
|
| 87 |
+
padding: 12px 16px;
|
| 88 |
+
background: transparent;
|
| 89 |
+
border: 1px solid var(--border);
|
| 90 |
+
border-radius: 8px;
|
| 91 |
+
color: var(--text-primary);
|
| 92 |
+
font-size: 14px;
|
| 93 |
+
font-weight: 500;
|
| 94 |
+
cursor: pointer;
|
| 95 |
+
display: flex;
|
| 96 |
+
align-items: center;
|
| 97 |
+
gap: 10px;
|
| 98 |
+
transition: background 0.2s;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.new-chat-btn:hover {
|
| 102 |
+
background: var(--bg-hover);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.chat-history {
|
| 106 |
+
flex: 1;
|
| 107 |
+
overflow-y: auto;
|
| 108 |
+
padding: 8px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.chat-history-item {
|
| 112 |
+
padding: 10px 12px;
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
font-size: 14px;
|
| 116 |
+
color: var(--text-secondary);
|
| 117 |
+
white-space: nowrap;
|
| 118 |
+
overflow: hidden;
|
| 119 |
+
text-overflow: ellipsis;
|
| 120 |
+
transition: background 0.2s;
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
gap: 8px;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.chat-history-item:hover {
|
| 127 |
+
background: var(--bg-hover);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.chat-history-item.active {
|
| 131 |
+
background: var(--bg-tertiary);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.sidebar-footer {
|
| 135 |
+
padding: 12px;
|
| 136 |
+
border-top: 1px solid var(--border);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.connection-status {
|
| 140 |
+
display: flex;
|
| 141 |
+
align-items: center;
|
| 142 |
+
gap: 8px;
|
| 143 |
+
padding: 10px 12px;
|
| 144 |
+
background: var(--bg-secondary);
|
| 145 |
+
border-radius: 8px;
|
| 146 |
+
font-size: 13px;
|
| 147 |
+
color: var(--text-secondary);
|
| 148 |
+
cursor: pointer;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.status-dot {
|
| 152 |
+
width: 8px;
|
| 153 |
+
height: 8px;
|
| 154 |
+
border-radius: 50%;
|
| 155 |
+
background: #ef4444;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.status-dot.online {
|
| 159 |
+
background: #22c55e;
|
| 160 |
+
animation: pulse 2s infinite;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
@keyframes pulse {
|
| 164 |
+
|
| 165 |
+
0%,
|
| 166 |
+
100% {
|
| 167 |
+
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
50% {
|
| 171 |
+
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Main Content */
|
| 176 |
+
.main {
|
| 177 |
+
flex: 1;
|
| 178 |
+
display: flex;
|
| 179 |
+
flex-direction: column;
|
| 180 |
+
min-width: 0;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* Header */
|
| 184 |
+
.header {
|
| 185 |
+
padding: 12px 20px;
|
| 186 |
+
border-bottom: 1px solid var(--border);
|
| 187 |
+
display: flex;
|
| 188 |
+
align-items: center;
|
| 189 |
+
justify-content: space-between;
|
| 190 |
+
background: var(--bg-primary);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.logo-container {
|
| 194 |
+
display: flex;
|
| 195 |
+
align-items: center;
|
| 196 |
+
gap: 12px;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.logo-img {
|
| 200 |
+
height: 32px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.model-selector {
|
| 204 |
+
padding: 8px 12px;
|
| 205 |
+
background: var(--bg-secondary);
|
| 206 |
+
border: 1px solid var(--border);
|
| 207 |
+
border-radius: 8px;
|
| 208 |
+
color: var(--text-primary);
|
| 209 |
+
font-size: 13px;
|
| 210 |
+
cursor: pointer;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.header-actions {
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 8px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.icon-btn {
|
| 220 |
+
width: 36px;
|
| 221 |
+
height: 36px;
|
| 222 |
+
display: flex;
|
| 223 |
+
align-items: center;
|
| 224 |
+
justify-content: center;
|
| 225 |
+
background: transparent;
|
| 226 |
+
border: none;
|
| 227 |
+
border-radius: 8px;
|
| 228 |
+
color: var(--text-secondary);
|
| 229 |
+
cursor: pointer;
|
| 230 |
+
font-size: 18px;
|
| 231 |
+
transition: background 0.2s, color 0.2s;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.icon-btn:hover {
|
| 235 |
+
background: var(--bg-hover);
|
| 236 |
+
color: var(--text-primary);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Chat Container */
|
| 240 |
+
.chat-container {
|
| 241 |
+
flex: 1;
|
| 242 |
+
overflow-y: auto;
|
| 243 |
+
overflow-x: hidden;
|
| 244 |
+
padding: 0;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.chat-messages {
|
| 248 |
+
max-width: 768px;
|
| 249 |
+
margin: 0 auto;
|
| 250 |
+
padding: 24px;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Messages */
|
| 254 |
+
.message {
|
| 255 |
+
margin-bottom: 24px;
|
| 256 |
+
animation: fadeIn 0.3s ease;
|
| 257 |
+
display: flex;
|
| 258 |
+
gap: 12px;
|
| 259 |
+
max-width: 78%;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.message.user {
|
| 263 |
+
margin-left: auto;
|
| 264 |
+
flex-direction: row-reverse;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.message.assistant {
|
| 268 |
+
margin-right: auto;
|
| 269 |
+
flex-direction: row;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
@keyframes fadeIn {
|
| 273 |
+
from {
|
| 274 |
+
opacity: 0;
|
| 275 |
+
transform: translateY(8px);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
to {
|
| 279 |
+
opacity: 1;
|
| 280 |
+
transform: translateY(0);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/* Removed .message-header styles as we are restructuring */
|
| 285 |
+
|
| 286 |
+
/* Avatar styles removed */
|
| 287 |
+
|
| 288 |
+
.message-role {
|
| 289 |
+
display: none;
|
| 290 |
+
/* Hide names */
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.message-content {
|
| 294 |
+
padding: 12px 16px;
|
| 295 |
+
border-radius: 16px;
|
| 296 |
+
line-height: 1.6;
|
| 297 |
+
color: var(--text-primary);
|
| 298 |
+
overflow-wrap: break-word;
|
| 299 |
+
word-wrap: break-word;
|
| 300 |
+
word-break: break-word;
|
| 301 |
+
white-space: normal;
|
| 302 |
+
min-width: 0;
|
| 303 |
+
max-width: 100%;
|
| 304 |
+
overflow-x: hidden;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.message.user .message-content {
|
| 308 |
+
background: var(--accent-gradient);
|
| 309 |
+
color: white;
|
| 310 |
+
border-top-right-radius: 4px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.message.assistant .message-content {
|
| 314 |
+
background: var(--bg-secondary);
|
| 315 |
+
border-top-left-radius: 4px;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* Text color overrides for user bubble */
|
| 319 |
+
.message.user .message-content p,
|
| 320 |
+
.message.user .message-content li,
|
| 321 |
+
.message.user .message-content strong,
|
| 322 |
+
.message.user .message-content span:not(.hljs) {
|
| 323 |
+
color: white;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.message-content p {
|
| 327 |
+
margin-bottom: 8px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.message-content p:last-child {
|
| 331 |
+
margin-bottom: 0;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/* Lists inside message bubbles */
|
| 335 |
+
.message-content ul,
|
| 336 |
+
.message-content ol {
|
| 337 |
+
margin: 8px 0;
|
| 338 |
+
padding-left: 24px;
|
| 339 |
+
list-style-position: inside;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.message-content li {
|
| 343 |
+
margin-bottom: 4px;
|
| 344 |
+
word-wrap: break-word;
|
| 345 |
+
overflow-wrap: break-word;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.message-content li:last-child {
|
| 349 |
+
margin-bottom: 0;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* Numbered lists */
|
| 353 |
+
.message-content ol {
|
| 354 |
+
list-style-type: decimal;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* Bullet lists */
|
| 358 |
+
.message-content ul {
|
| 359 |
+
list-style-type: disc;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
/* Message Actions */
|
| 363 |
+
.message-wrapper {
|
| 364 |
+
display: flex;
|
| 365 |
+
flex-direction: column;
|
| 366 |
+
gap: 4px;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.message-actions {
|
| 370 |
+
display: flex;
|
| 371 |
+
gap: 4px;
|
| 372 |
+
opacity: 0;
|
| 373 |
+
transition: opacity 0.2s;
|
| 374 |
+
padding: 4px 0;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.message:hover .message-actions,
|
| 378 |
+
.message-actions:focus-within {
|
| 379 |
+
opacity: 1;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.message.user .message-actions {
|
| 383 |
+
justify-content: flex-end;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.action-btn {
|
| 387 |
+
width: 28px;
|
| 388 |
+
height: 28px;
|
| 389 |
+
display: flex;
|
| 390 |
+
align-items: center;
|
| 391 |
+
justify-content: center;
|
| 392 |
+
background: var(--bg-secondary);
|
| 393 |
+
border: 1px solid var(--border);
|
| 394 |
+
border-radius: 6px;
|
| 395 |
+
color: var(--text-muted);
|
| 396 |
+
cursor: pointer;
|
| 397 |
+
transition: all 0.2s;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.action-btn:hover {
|
| 401 |
+
background: var(--bg-hover);
|
| 402 |
+
color: var(--text-primary);
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.action-btn svg {
|
| 406 |
+
width: 14px;
|
| 407 |
+
height: 14px;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
/* Image Container */
|
| 411 |
+
.image-container {
|
| 412 |
+
position: relative;
|
| 413 |
+
display: inline-block;
|
| 414 |
+
max-width: 100%;
|
| 415 |
+
margin: 8px 0;
|
| 416 |
+
border-radius: 12px;
|
| 417 |
+
overflow: hidden;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.image-container img {
|
| 421 |
+
display: block;
|
| 422 |
+
max-width: 100%;
|
| 423 |
+
border-radius: 12px;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.image-container.loading {
|
| 427 |
+
min-height: 280px;
|
| 428 |
+
min-width: 280px;
|
| 429 |
+
background: #000;
|
| 430 |
+
display: flex;
|
| 431 |
+
align-items: center;
|
| 432 |
+
justify-content: center;
|
| 433 |
+
position: relative;
|
| 434 |
+
overflow: hidden;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
/* Movie-like Rotating Gradient */
|
| 438 |
+
.image-container.loading::before {
|
| 439 |
+
content: '';
|
| 440 |
+
position: absolute;
|
| 441 |
+
inset: -50%;
|
| 442 |
+
background: conic-gradient(from 0deg,
|
| 443 |
+
transparent 0deg,
|
| 444 |
+
#00d4aa 60deg,
|
| 445 |
+
#00bcd4 120deg,
|
| 446 |
+
#2d8cbe 180deg,
|
| 447 |
+
transparent 240deg);
|
| 448 |
+
animation: rotate-gradient 4s linear infinite;
|
| 449 |
+
opacity: 0.8;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/* Dark overlay to make text readable */
|
| 453 |
+
.image-container.loading::after {
|
| 454 |
+
content: 'Creating image...';
|
| 455 |
+
position: absolute;
|
| 456 |
+
inset: 4px;
|
| 457 |
+
background: #1a1a2e;
|
| 458 |
+
display: flex;
|
| 459 |
+
align-items: center;
|
| 460 |
+
justify-content: center;
|
| 461 |
+
color: rgba(255, 255, 255, 0.9);
|
| 462 |
+
font-size: 14px;
|
| 463 |
+
font-weight: 500;
|
| 464 |
+
border-radius: 8px;
|
| 465 |
+
animation: pulse-text 2s infinite ease-in-out;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
@keyframes rotate-gradient {
|
| 469 |
+
0% {
|
| 470 |
+
transform: rotate(0deg);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
100% {
|
| 474 |
+
transform: rotate(360deg);
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
@keyframes pulse-text {
|
| 479 |
+
|
| 480 |
+
0%,
|
| 481 |
+
100% {
|
| 482 |
+
color: rgba(255, 255, 255, 0.7);
|
| 483 |
+
transform: scale(0.98);
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
50% {
|
| 487 |
+
color: rgba(255, 255, 255, 1);
|
| 488 |
+
transform: scale(1);
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.image-actions {
|
| 493 |
+
position: absolute;
|
| 494 |
+
bottom: 8px;
|
| 495 |
+
right: 8px;
|
| 496 |
+
display: flex;
|
| 497 |
+
gap: 4px;
|
| 498 |
+
opacity: 0;
|
| 499 |
+
transition: opacity 0.2s;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.image-container:hover .image-actions {
|
| 503 |
+
opacity: 1;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.image-download-btn {
|
| 507 |
+
padding: 6px 12px;
|
| 508 |
+
background: rgba(0, 0, 0, 0.7);
|
| 509 |
+
border: none;
|
| 510 |
+
border-radius: 6px;
|
| 511 |
+
color: white;
|
| 512 |
+
font-size: 12px;
|
| 513 |
+
cursor: pointer;
|
| 514 |
+
display: flex;
|
| 515 |
+
align-items: center;
|
| 516 |
+
gap: 4px;
|
| 517 |
+
transition: background 0.2s;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.image-download-btn:hover {
|
| 521 |
+
background: rgba(0, 0, 0, 0.9);
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/* Code Blocks */
|
| 525 |
+
.code-block {
|
| 526 |
+
margin: 16px 0;
|
| 527 |
+
border-radius: 8px;
|
| 528 |
+
overflow: hidden;
|
| 529 |
+
background: var(--code-bg);
|
| 530 |
+
max-width: 100%;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.code-header {
|
| 534 |
+
display: flex;
|
| 535 |
+
align-items: center;
|
| 536 |
+
justify-content: space-between;
|
| 537 |
+
padding: 8px 16px;
|
| 538 |
+
background: rgba(255, 255, 255, 0.05);
|
| 539 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.code-lang {
|
| 543 |
+
font-size: 12px;
|
| 544 |
+
color: #8b8b8b;
|
| 545 |
+
text-transform: lowercase;
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.copy-btn {
|
| 549 |
+
display: flex;
|
| 550 |
+
align-items: center;
|
| 551 |
+
gap: 4px;
|
| 552 |
+
padding: 4px 8px;
|
| 553 |
+
background: transparent;
|
| 554 |
+
border: none;
|
| 555 |
+
color: #8b8b8b;
|
| 556 |
+
font-size: 12px;
|
| 557 |
+
cursor: pointer;
|
| 558 |
+
border-radius: 4px;
|
| 559 |
+
transition: background 0.2s, color 0.2s;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.copy-btn:hover {
|
| 563 |
+
background: rgba(255, 255, 255, 0.1);
|
| 564 |
+
color: #fff;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.code-block pre {
|
| 568 |
+
margin: 0;
|
| 569 |
+
padding: 16px;
|
| 570 |
+
overflow-x: auto;
|
| 571 |
+
max-width: 100%;
|
| 572 |
+
white-space: pre-wrap;
|
| 573 |
+
overflow-wrap: anywhere;
|
| 574 |
+
word-break: break-word;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.code-block code {
|
| 578 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 579 |
+
font-size: 13px;
|
| 580 |
+
line-height: 1.5;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/* Inline code */
|
| 584 |
+
.message-content code:not(.hljs) {
|
| 585 |
+
background: var(--bg-tertiary);
|
| 586 |
+
padding: 2px 6px;
|
| 587 |
+
border-radius: 4px;
|
| 588 |
+
font-family: 'JetBrains Mono', monospace;
|
| 589 |
+
font-size: 13px;
|
| 590 |
+
overflow-wrap: anywhere;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
/* Thinking Animation */
|
| 594 |
+
.thinking {
|
| 595 |
+
display: flex;
|
| 596 |
+
align-items: center;
|
| 597 |
+
gap: 8px;
|
| 598 |
+
padding: 12px 0;
|
| 599 |
+
color: var(--text-muted);
|
| 600 |
+
font-size: 14px;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.thinking-dots {
|
| 604 |
+
display: flex;
|
| 605 |
+
gap: 4px;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.thinking-dots span {
|
| 609 |
+
width: 6px;
|
| 610 |
+
height: 6px;
|
| 611 |
+
background: var(--accent);
|
| 612 |
+
border-radius: 50%;
|
| 613 |
+
animation: thinking 1.4s infinite ease-in-out;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.thinking-dots span:nth-child(1) {
|
| 617 |
+
animation-delay: -0.32s;
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.thinking-dots span:nth-child(2) {
|
| 621 |
+
animation-delay: -0.16s;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
@keyframes thinking {
|
| 625 |
+
|
| 626 |
+
0%,
|
| 627 |
+
80%,
|
| 628 |
+
100% {
|
| 629 |
+
transform: scale(0.6);
|
| 630 |
+
opacity: 0.5;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
40% {
|
| 634 |
+
transform: scale(1);
|
| 635 |
+
opacity: 1;
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
/* Typing effect */
|
| 640 |
+
.typing-cursor::after {
|
| 641 |
+
content: 'â–‹';
|
| 642 |
+
animation: blink 1s infinite;
|
| 643 |
+
color: var(--accent);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
@keyframes blink {
|
| 647 |
+
|
| 648 |
+
0%,
|
| 649 |
+
50% {
|
| 650 |
+
opacity: 1;
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
51%,
|
| 654 |
+
100% {
|
| 655 |
+
opacity: 0;
|
| 656 |
+
}
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
/* Welcome Screen */
|
| 660 |
+
.welcome-screen {
|
| 661 |
+
display: flex;
|
| 662 |
+
flex-direction: column;
|
| 663 |
+
align-items: center;
|
| 664 |
+
justify-content: center;
|
| 665 |
+
height: 100%;
|
| 666 |
+
padding: 40px;
|
| 667 |
+
text-align: center;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.welcome-logo {
|
| 671 |
+
height: 60px;
|
| 672 |
+
width: auto;
|
| 673 |
+
margin-bottom: 24px;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.welcome-title {
|
| 677 |
+
font-size: 28px;
|
| 678 |
+
font-weight: 600;
|
| 679 |
+
margin-bottom: 8px;
|
| 680 |
+
background: var(--accent-gradient);
|
| 681 |
+
-webkit-background-clip: text;
|
| 682 |
+
-webkit-text-fill-color: transparent;
|
| 683 |
+
background-clip: text;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.welcome-subtitle {
|
| 687 |
+
font-size: 16px;
|
| 688 |
+
color: var(--text-muted);
|
| 689 |
+
margin-bottom: 32px;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
.suggestions-grid {
|
| 693 |
+
display: grid;
|
| 694 |
+
grid-template-columns: repeat(2, 1fr);
|
| 695 |
+
gap: 12px;
|
| 696 |
+
max-width: 600px;
|
| 697 |
+
width: 100%;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.suggestion-card {
|
| 701 |
+
padding: 16px;
|
| 702 |
+
background: var(--bg-secondary);
|
| 703 |
+
border: 1px solid var(--border);
|
| 704 |
+
border-radius: 12px;
|
| 705 |
+
text-align: left;
|
| 706 |
+
cursor: pointer;
|
| 707 |
+
transition: all 0.2s;
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
.suggestion-card:hover {
|
| 711 |
+
border-color: var(--accent);
|
| 712 |
+
box-shadow: var(--shadow);
|
| 713 |
+
transform: translateY(-2px);
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
.suggestion-icon {
|
| 717 |
+
font-size: 20px;
|
| 718 |
+
margin-bottom: 8px;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.suggestion-text {
|
| 722 |
+
font-size: 14px;
|
| 723 |
+
color: var(--text-secondary);
|
| 724 |
+
line-height: 1.4;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
/* Input Area */
|
| 728 |
+
.input-area {
|
| 729 |
+
padding: 16px 24px 24px;
|
| 730 |
+
background: var(--bg-primary);
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
.input-container {
|
| 734 |
+
max-width: 768px;
|
| 735 |
+
margin: 0 auto;
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
.input-wrapper {
|
| 739 |
+
display: flex;
|
| 740 |
+
align-items: flex-end;
|
| 741 |
+
gap: 12px;
|
| 742 |
+
padding: 12px 16px;
|
| 743 |
+
background: var(--bg-input);
|
| 744 |
+
border: 1px solid var(--border);
|
| 745 |
+
border-radius: 16px;
|
| 746 |
+
box-shadow: var(--shadow);
|
| 747 |
+
transition: border-color 0.2s, box-shadow 0.2s;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.input-wrapper:focus-within {
|
| 751 |
+
border-color: var(--accent);
|
| 752 |
+
box-shadow: 0 0 0 2px rgba(45, 140, 190, 0.2);
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.input-wrapper textarea {
|
| 756 |
+
flex: 1;
|
| 757 |
+
padding: 4px 0;
|
| 758 |
+
background: transparent;
|
| 759 |
+
border: none;
|
| 760 |
+
color: var(--text-primary);
|
| 761 |
+
font-size: 15px;
|
| 762 |
+
font-family: inherit;
|
| 763 |
+
resize: none;
|
| 764 |
+
max-height: 200px;
|
| 765 |
+
line-height: 1.5;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.input-wrapper textarea::placeholder {
|
| 769 |
+
color: var(--text-muted);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.input-wrapper textarea:focus {
|
| 773 |
+
outline: none;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.send-btn {
|
| 777 |
+
width: 32px;
|
| 778 |
+
height: 32px;
|
| 779 |
+
display: flex;
|
| 780 |
+
align-items: center;
|
| 781 |
+
justify-content: center;
|
| 782 |
+
background: var(--accent);
|
| 783 |
+
border: none;
|
| 784 |
+
border-radius: 8px;
|
| 785 |
+
color: white;
|
| 786 |
+
cursor: pointer;
|
| 787 |
+
transition: opacity 0.2s, transform 0.2s;
|
| 788 |
+
flex-shrink: 0;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
.send-btn:hover:not(:disabled) {
|
| 792 |
+
transform: scale(1.05);
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
.send-btn:disabled {
|
| 796 |
+
opacity: 0.4;
|
| 797 |
+
cursor: not-allowed;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
/* Tools Button */
|
| 801 |
+
.tools-area {
|
| 802 |
+
display: flex;
|
| 803 |
+
align-items: center;
|
| 804 |
+
gap: 8px;
|
| 805 |
+
margin-bottom: 8px;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.tool-btn {
|
| 809 |
+
display: flex;
|
| 810 |
+
align-items: center;
|
| 811 |
+
gap: 6px;
|
| 812 |
+
padding: 8px 14px;
|
| 813 |
+
background: var(--bg-secondary);
|
| 814 |
+
border: 1px solid var(--border);
|
| 815 |
+
border-radius: 20px;
|
| 816 |
+
color: var(--text-primary);
|
| 817 |
+
font-size: 13px;
|
| 818 |
+
cursor: pointer;
|
| 819 |
+
transition: all 0.2s;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.tool-btn:hover {
|
| 823 |
+
background: var(--bg-hover);
|
| 824 |
+
border-color: var(--accent);
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
.tool-btn.active {
|
| 828 |
+
background: var(--accent);
|
| 829 |
+
color: white;
|
| 830 |
+
border-color: var(--accent);
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
.tool-btn svg {
|
| 834 |
+
width: 16px;
|
| 835 |
+
height: 16px;
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
/* Copy Feedback */
|
| 839 |
+
.action-btn.copied {
|
| 840 |
+
color: #10b981;
|
| 841 |
+
border-color: #10b981;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
.action-btn .check-icon {
|
| 845 |
+
display: none;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
.action-btn.copied .copy-icon {
|
| 849 |
+
display: none;
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
.action-btn.copied .check-icon {
|
| 853 |
+
display: block;
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
.input-footer {
|
| 857 |
+
display: flex;
|
| 858 |
+
justify-content: center;
|
| 859 |
+
margin-top: 8px;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
.input-footer-text {
|
| 863 |
+
font-size: 12px;
|
| 864 |
+
color: var(--text-muted);
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
/* Connection Modal */
|
| 868 |
+
.modal-overlay {
|
| 869 |
+
position: fixed;
|
| 870 |
+
inset: 0;
|
| 871 |
+
background: rgba(0, 0, 0, 0.5);
|
| 872 |
+
display: flex;
|
| 873 |
+
align-items: center;
|
| 874 |
+
justify-content: center;
|
| 875 |
+
z-index: 1000;
|
| 876 |
+
opacity: 0;
|
| 877 |
+
visibility: hidden;
|
| 878 |
+
transition: opacity 0.2s, visibility 0.2s;
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
.modal-overlay.show {
|
| 882 |
+
opacity: 1;
|
| 883 |
+
visibility: visible;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
.modal {
|
| 887 |
+
background: var(--bg-primary);
|
| 888 |
+
border-radius: 16px;
|
| 889 |
+
padding: 24px;
|
| 890 |
+
width: 90%;
|
| 891 |
+
max-width: 440px;
|
| 892 |
+
box-shadow: var(--shadow-lg);
|
| 893 |
+
transform: scale(0.95);
|
| 894 |
+
transition: transform 0.2s;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
.modal-overlay.show .modal {
|
| 898 |
+
transform: scale(1);
|
| 899 |
+
}
|
| 900 |
+
|
| 901 |
+
.modal-title {
|
| 902 |
+
font-size: 18px;
|
| 903 |
+
font-weight: 600;
|
| 904 |
+
margin-bottom: 8px;
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
.modal-subtitle {
|
| 908 |
+
font-size: 14px;
|
| 909 |
+
color: var(--text-muted);
|
| 910 |
+
margin-bottom: 20px;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.modal-input {
|
| 914 |
+
width: 100%;
|
| 915 |
+
padding: 12px 16px;
|
| 916 |
+
background: var(--bg-secondary);
|
| 917 |
+
border: 1px solid var(--border);
|
| 918 |
+
border-radius: 8px;
|
| 919 |
+
color: var(--text-primary);
|
| 920 |
+
font-size: 14px;
|
| 921 |
+
margin-bottom: 16px;
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
.modal-input:focus {
|
| 925 |
+
outline: none;
|
| 926 |
+
border-color: var(--accent);
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
.modal-actions {
|
| 930 |
+
display: flex;
|
| 931 |
+
gap: 12px;
|
| 932 |
+
justify-content: flex-end;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
.modal-btn {
|
| 936 |
+
padding: 10px 20px;
|
| 937 |
+
border-radius: 8px;
|
| 938 |
+
font-size: 14px;
|
| 939 |
+
font-weight: 500;
|
| 940 |
+
cursor: pointer;
|
| 941 |
+
transition: all 0.2s;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.modal-btn.secondary {
|
| 945 |
+
background: transparent;
|
| 946 |
+
border: 1px solid var(--border);
|
| 947 |
+
color: var(--text-primary);
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
.modal-btn.primary {
|
| 951 |
+
background: var(--accent);
|
| 952 |
+
border: none;
|
| 953 |
+
color: white;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
.modal-btn:hover {
|
| 957 |
+
opacity: 0.9;
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
/* Responsive */
|
| 961 |
+
@media (max-width: 768px) {
|
| 962 |
+
.sidebar {
|
| 963 |
+
position: fixed;
|
| 964 |
+
left: 0;
|
| 965 |
+
top: 0;
|
| 966 |
+
bottom: 0;
|
| 967 |
+
z-index: 100;
|
| 968 |
+
transform: translateX(-100%);
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
.sidebar.open {
|
| 972 |
+
transform: translateX(0);
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
.suggestions-grid {
|
| 976 |
+
grid-template-columns: 1fr;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
.message {
|
| 980 |
+
max-width: 92%;
|
| 981 |
+
}
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
/* Scrollbar */
|
| 985 |
+
::-webkit-scrollbar {
|
| 986 |
+
width: 6px;
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
::-webkit-scrollbar-track {
|
| 990 |
+
background: transparent;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
::-webkit-scrollbar-thumb {
|
| 994 |
+
background: var(--border);
|
| 995 |
+
border-radius: 3px;
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
::-webkit-scrollbar-thumb:hover {
|
| 999 |
+
background: var(--text-muted);
|
| 1000 |
+
}
|
| 1001 |
+
</style>
|
| 1002 |
+
</head>
|
| 1003 |
+
|
| 1004 |
+
<body>
|
| 1005 |
+
<!-- Sidebar -->
|
| 1006 |
+
<aside class="sidebar" id="sidebar">
|
| 1007 |
+
<div class="sidebar-header">
|
| 1008 |
+
<button class="new-chat-btn" onclick="newChat()">
|
| 1009 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1010 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
| 1011 |
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
| 1012 |
+
</svg>
|
| 1013 |
+
New chat
|
| 1014 |
+
</button>
|
| 1015 |
+
</div>
|
| 1016 |
+
<div class="chat-history" id="chatHistory">
|
| 1017 |
+
<!-- Chat history items will be added here -->
|
| 1018 |
+
</div>
|
| 1019 |
+
<div class="sidebar-footer">
|
| 1020 |
+
<div class="connection-status">
|
| 1021 |
+
<div class="status-dot online" id="statusDot"></div>
|
| 1022 |
+
<span id="connectionText">Running Locally</span>
|
| 1023 |
+
</div>
|
| 1024 |
+
</div>
|
| 1025 |
+
</aside>
|
| 1026 |
+
|
| 1027 |
+
<!-- Main Content -->
|
| 1028 |
+
<main class="main">
|
| 1029 |
+
<!-- Header -->
|
| 1030 |
+
<header class="header">
|
| 1031 |
+
<div class="logo-container">
|
| 1032 |
+
<button class="icon-btn" onclick="toggleSidebar()" id="menuBtn">
|
| 1033 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1034 |
+
<line x1="3" y1="12" x2="21" y2="12"></line>
|
| 1035 |
+
<line x1="3" y1="6" x2="21" y2="6"></line>
|
| 1036 |
+
<line x1="3" y1="18" x2="21" y2="18"></line>
|
| 1037 |
+
</svg>
|
| 1038 |
+
</button>
|
| 1039 |
+
<div class="logo-text">Krish Mind</div>
|
| 1040 |
+
</div>
|
| 1041 |
+
<div class="header-actions">
|
| 1042 |
+
<button class="icon-btn" onclick="toggleTheme()" title="Toggle theme">
|
| 1043 |
+
<span id="themeIcon">🌙</span>
|
| 1044 |
+
</button>
|
| 1045 |
+
<button class="icon-btn" onclick="showConnectModal()" title="Settings">
|
| 1046 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1047 |
+
<circle cx="12" cy="12" r="3"></circle>
|
| 1048 |
+
<path
|
| 1049 |
+
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
|
| 1050 |
+
</path>
|
| 1051 |
+
</svg>
|
| 1052 |
+
</button>
|
| 1053 |
+
</div>
|
| 1054 |
+
</header>
|
| 1055 |
+
|
| 1056 |
+
<!-- Chat Container -->
|
| 1057 |
+
<div class="chat-container" id="chatContainer">
|
| 1058 |
+
<!-- Welcome Screen -->
|
| 1059 |
+
<div class="welcome-screen" id="welcomeScreen">
|
| 1060 |
+
<div class="welcome-logo-text">Krish Mind</div>
|
| 1061 |
+
<h1 class="welcome-title">Hello! I'm Krish Mind</h1>
|
| 1062 |
+
<p class="welcome-subtitle">Your AI assistant, developed by Krish CS</p>
|
| 1063 |
+
<div class="suggestions-grid">
|
| 1064 |
+
<div class="suggestion-card" onclick="sendSuggestion('Explain quantum computing in simple terms')">
|
| 1065 |
+
<div class="suggestion-icon">💡</div>
|
| 1066 |
+
<div class="suggestion-text">Explain quantum computing in simple terms</div>
|
| 1067 |
+
</div>
|
| 1068 |
+
<div class="suggestion-card" onclick="sendSuggestion('Write a Python function to sort a list')">
|
| 1069 |
+
<div class="suggestion-icon">💻</div>
|
| 1070 |
+
<div class="suggestion-text">Write a Python function to sort a list</div>
|
| 1071 |
+
</div>
|
| 1072 |
+
<div class="suggestion-card" onclick="sendSuggestion('What makes you different from other AIs?')">
|
| 1073 |
+
<div class="suggestion-icon">✨</div>
|
| 1074 |
+
<div class="suggestion-text">What makes you different from other AIs?</div>
|
| 1075 |
+
</div>
|
| 1076 |
+
<div class="suggestion-card" onclick="sendSuggestion('Help me learn a new programming language')">
|
| 1077 |
+
<div class="suggestion-icon">📚</div>
|
| 1078 |
+
<div class="suggestion-text">Help me learn a new programming language</div>
|
| 1079 |
+
</div>
|
| 1080 |
+
</div>
|
| 1081 |
+
</div>
|
| 1082 |
+
|
| 1083 |
+
<!-- Messages -->
|
| 1084 |
+
<div class="chat-messages" id="chatMessages" style="display: none;"></div>
|
| 1085 |
+
</div>
|
| 1086 |
+
|
| 1087 |
+
<!-- Input Area -->
|
| 1088 |
+
<div class="input-area">
|
| 1089 |
+
<div class="input-container">
|
| 1090 |
+
<div class="tools-area">
|
| 1091 |
+
<button class="tool-btn" id="imageGenBtn" onclick="toggleImageGen()">
|
| 1092 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1093 |
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
| 1094 |
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
| 1095 |
+
<polyline points="21 15 16 10 5 21"></polyline>
|
| 1096 |
+
</svg>
|
| 1097 |
+
Create Image
|
| 1098 |
+
</button>
|
| 1099 |
+
<button class="tool-btn" id="krceModeBtn" onclick="toggleKrceMode()">
|
| 1100 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1101 |
+
<path d="M4 19h16"></path>
|
| 1102 |
+
<path d="M5 19V8l7-4 7 4v11"></path>
|
| 1103 |
+
<path d="M9 10h6"></path>
|
| 1104 |
+
<path d="M9 14h6"></path>
|
| 1105 |
+
</svg>
|
| 1106 |
+
KRCE Mode
|
| 1107 |
+
</button>
|
| 1108 |
+
</div>
|
| 1109 |
+
<div class="input-wrapper">
|
| 1110 |
+
<textarea id="messageInput" placeholder="Message Krish Mind..." rows="1"
|
| 1111 |
+
onkeydown="handleKeyDown(event)" oninput="autoResize(this)"></textarea>
|
| 1112 |
+
<button class="send-btn" id="sendBtn" onclick="sendMessage()" disabled>
|
| 1113 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
|
| 1114 |
+
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
|
| 1115 |
+
</svg>
|
| 1116 |
+
</button>
|
| 1117 |
+
</div>
|
| 1118 |
+
<div class="input-footer">
|
| 1119 |
+
<span class="input-footer-text">Krish Mind AI • Developed by Krish CS</span>
|
| 1120 |
+
</div>
|
| 1121 |
+
</div>
|
| 1122 |
+
</div>
|
| 1123 |
+
</main>
|
| 1124 |
+
|
| 1125 |
+
<!-- Connection Modal -->
|
| 1126 |
+
<div class="modal-overlay" id="connectModal">
|
| 1127 |
+
<div class="modal">
|
| 1128 |
+
<h3 class="modal-title">Connect to Krish Mind Server</h3>
|
| 1129 |
+
<p class="modal-subtitle">Enter your Colab ngrok URL to start chatting</p>
|
| 1130 |
+
<input type="text" class="modal-input" id="serverUrlInput"
|
| 1131 |
+
placeholder="https://xxxx-xx-xx-xxx-xx.ngrok-free.app">
|
| 1132 |
+
<div class="modal-actions">
|
| 1133 |
+
<button class="modal-btn secondary" onclick="hideConnectModal()">Cancel</button>
|
| 1134 |
+
<button class="modal-btn primary" onclick="connectToServer()">Connect</button>
|
| 1135 |
+
</div>
|
| 1136 |
+
</div>
|
| 1137 |
+
</div>
|
| 1138 |
+
|
| 1139 |
+
<script>
|
| 1140 |
+
// State
|
| 1141 |
+
let serverUrl = ''; // Empty = relative URL (works on HF Spaces or any host)
|
| 1142 |
+
let isConnected = true; // Always connected when hosted on same server
|
| 1143 |
+
let isGenerating = false; // Prevent multiple requests
|
| 1144 |
+
let imageGenMode = false; // Image generation tool toggle
|
| 1145 |
+
let krceMode = false; // KRCE-only answer mode toggle
|
| 1146 |
+
let messages = [];
|
| 1147 |
+
let chatHistory = JSON.parse(sessionStorage.getItem('krishMindChatHistory') || '[]');
|
| 1148 |
+
let chatConversations = JSON.parse(sessionStorage.getItem('krishMindConversations') || '{}');
|
| 1149 |
+
let currentChatId = null;
|
| 1150 |
+
|
| 1151 |
+
// Icons
|
| 1152 |
+
const SEND_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg>';
|
| 1153 |
+
const STOP_ICON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="white"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>';
|
| 1154 |
+
const COPY_ICON = '<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg><svg class="check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
| 1155 |
+
|
| 1156 |
+
// Initialize
|
| 1157 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1158 |
+
// Configure marked to preserve line breaks
|
| 1159 |
+
marked.use({ breaks: true, gfm: true });
|
| 1160 |
+
|
| 1161 |
+
// Load theme
|
| 1162 |
+
const theme = localStorage.getItem('krishMindTheme') || 'dark';
|
| 1163 |
+
document.documentElement.setAttribute('data-theme', theme);
|
| 1164 |
+
updateThemeIcon();
|
| 1165 |
+
|
| 1166 |
+
// Load chat history
|
| 1167 |
+
renderChatHistory();
|
| 1168 |
+
if (chatHistory.length > 0) {
|
| 1169 |
+
loadChat(chatHistory[0].id);
|
| 1170 |
+
}
|
| 1171 |
+
|
| 1172 |
+
// Load KRCE mode preference
|
| 1173 |
+
krceMode = localStorage.getItem('krceMode') === 'true';
|
| 1174 |
+
updateKrceModeUI();
|
| 1175 |
+
|
| 1176 |
+
// Auto-check connection (optional)
|
| 1177 |
+
checkConnection();
|
| 1178 |
+
|
| 1179 |
+
// Enable send button on input (only if not generating)
|
| 1180 |
+
document.getElementById('messageInput').addEventListener('input', (e) => {
|
| 1181 |
+
if (!isGenerating) {
|
| 1182 |
+
document.getElementById('sendBtn').disabled = !e.target.value.trim();
|
| 1183 |
+
}
|
| 1184 |
+
});
|
| 1185 |
+
});
|
| 1186 |
+
|
| 1187 |
+
// Theme toggle
|
| 1188 |
+
function toggleTheme() {
|
| 1189 |
+
const current = document.documentElement.getAttribute('data-theme');
|
| 1190 |
+
const next = current === 'dark' ? 'light' : 'dark';
|
| 1191 |
+
document.documentElement.setAttribute('data-theme', next);
|
| 1192 |
+
localStorage.setItem('krishMindTheme', next);
|
| 1193 |
+
updateThemeIcon();
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
function updateThemeIcon() {
|
| 1197 |
+
const theme = document.documentElement.getAttribute('data-theme');
|
| 1198 |
+
document.getElementById('themeIcon').textContent = theme === 'dark' ? '☀ï¸' : '🌙';
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
// Sidebar toggle
|
| 1202 |
+
function toggleSidebar() {
|
| 1203 |
+
document.getElementById('sidebar').classList.toggle('open');
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
// Close sidebar when clicking outside (mobile)
|
| 1207 |
+
document.addEventListener('click', (e) => {
|
| 1208 |
+
const sidebar = document.getElementById('sidebar');
|
| 1209 |
+
const menuBtn = document.getElementById('menuBtn');
|
| 1210 |
+
if (sidebar.classList.contains('open') &&
|
| 1211 |
+
!sidebar.contains(e.target) &&
|
| 1212 |
+
!menuBtn.contains(e.target)) {
|
| 1213 |
+
sidebar.classList.remove('open');
|
| 1214 |
+
}
|
| 1215 |
+
});
|
| 1216 |
+
|
| 1217 |
+
// Check local connection
|
| 1218 |
+
async function checkConnection() {
|
| 1219 |
+
try {
|
| 1220 |
+
const res = await fetch(serverUrl);
|
| 1221 |
+
if (!res.ok) {
|
| 1222 |
+
throw new Error(`HTTP ${res.status}`);
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
// Backend root serves HTML in this app; avoid forcing JSON parse.
|
| 1226 |
+
console.log("✅ Connected to Local Server");
|
| 1227 |
+
} catch (err) {
|
| 1228 |
+
console.error("⌠Local Server Not Found. Run: python scripts/server_local.py");
|
| 1229 |
+
addMessage('assistant', '**âš ï¸ Local Server Not Running!**\n\nPlease run this command in your terminal:\n`python scripts/server_local.py`');
|
| 1230 |
+
}
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
// New chat
|
| 1234 |
+
function newChat() {
|
| 1235 |
+
currentChatId = Date.now();
|
| 1236 |
+
messages = [];
|
| 1237 |
+
chatConversations[String(currentChatId)] = [];
|
| 1238 |
+
sessionStorage.setItem('krishMindConversations', JSON.stringify(chatConversations));
|
| 1239 |
+
document.getElementById('welcomeScreen').style.display = 'flex';
|
| 1240 |
+
document.getElementById('chatMessages').style.display = 'none';
|
| 1241 |
+
document.getElementById('chatMessages').innerHTML = '';
|
| 1242 |
+
renderChatHistory();
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
// Render chat history
|
| 1246 |
+
function renderChatHistory() {
|
| 1247 |
+
const container = document.getElementById('chatHistory');
|
| 1248 |
+
container.innerHTML = chatHistory.map(chat => `
|
| 1249 |
+
<div class="chat-history-item ${chat.id === currentChatId ? 'active' : ''}" onclick="loadChat(${chat.id})">
|
| 1250 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1251 |
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
| 1252 |
+
</svg>
|
| 1253 |
+
${chat.title}
|
| 1254 |
+
</div>
|
| 1255 |
+
`).join('');
|
| 1256 |
+
}
|
| 1257 |
+
|
| 1258 |
+
function persistConversationState() {
|
| 1259 |
+
if (!currentChatId) return;
|
| 1260 |
+
chatConversations[String(currentChatId)] = messages;
|
| 1261 |
+
sessionStorage.setItem('krishMindConversations', JSON.stringify(chatConversations));
|
| 1262 |
+
sessionStorage.setItem('krishMindChatHistory', JSON.stringify(chatHistory.slice(0, 20)));
|
| 1263 |
+
}
|
| 1264 |
+
|
| 1265 |
+
function loadChat(chatId) {
|
| 1266 |
+
const key = String(chatId);
|
| 1267 |
+
currentChatId = chatId;
|
| 1268 |
+
const stored = Array.isArray(chatConversations[key]) ? chatConversations[key] : [];
|
| 1269 |
+
|
| 1270 |
+
messages = [];
|
| 1271 |
+
const chatMessagesEl = document.getElementById('chatMessages');
|
| 1272 |
+
chatMessagesEl.innerHTML = '';
|
| 1273 |
+
|
| 1274 |
+
if (stored.length === 0) {
|
| 1275 |
+
document.getElementById('welcomeScreen').style.display = 'flex';
|
| 1276 |
+
document.getElementById('chatMessages').style.display = 'none';
|
| 1277 |
+
} else {
|
| 1278 |
+
document.getElementById('welcomeScreen').style.display = 'none';
|
| 1279 |
+
document.getElementById('chatMessages').style.display = 'block';
|
| 1280 |
+
stored.forEach(msg => addMessage(msg.role, msg.content));
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
renderChatHistory();
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
function buildHistoryForBackend() {
|
| 1287 |
+
return messages
|
| 1288 |
+
.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string')
|
| 1289 |
+
.slice(-8)
|
| 1290 |
+
.map(m => ({
|
| 1291 |
+
role: m.role,
|
| 1292 |
+
content: m.content.length > 1200 ? `${m.content.slice(0, 1200)} ...` : m.content
|
| 1293 |
+
}));
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
// Send message
|
| 1297 |
+
async function sendMessage() {
|
| 1298 |
+
const input = document.getElementById('messageInput');
|
| 1299 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 1300 |
+
const message = input.value.trim();
|
| 1301 |
+
const historyForBackend = buildHistoryForBackend();
|
| 1302 |
+
|
| 1303 |
+
if (!message) return;
|
| 1304 |
+
if (isGenerating) return; // Block if already generating
|
| 1305 |
+
if (!isConnected) {
|
| 1306 |
+
showConnectModal();
|
| 1307 |
+
return;
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
// Set generating state
|
| 1311 |
+
isGenerating = true;
|
| 1312 |
+
sendBtn.innerHTML = STOP_ICON;
|
| 1313 |
+
sendBtn.disabled = false;
|
| 1314 |
+
|
| 1315 |
+
// Hide welcome screen
|
| 1316 |
+
document.getElementById('welcomeScreen').style.display = 'none';
|
| 1317 |
+
document.getElementById('chatMessages').style.display = 'block';
|
| 1318 |
+
|
| 1319 |
+
// Clear input
|
| 1320 |
+
input.value = '';
|
| 1321 |
+
input.style.height = 'auto';
|
| 1322 |
+
|
| 1323 |
+
// Add user message
|
| 1324 |
+
addMessage('user', message);
|
| 1325 |
+
|
| 1326 |
+
// Scroll to bottom after adding user message
|
| 1327 |
+
const chatContainer = document.getElementById('chatContainer');
|
| 1328 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1329 |
+
|
| 1330 |
+
// Auto-detect image generation requests
|
| 1331 |
+
const imageKeywords = [
|
| 1332 |
+
/generate\s+(an?\s+)?image/i,
|
| 1333 |
+
/create\s+(an?\s+)?image/i,
|
| 1334 |
+
/make\s+(an?\s+)?image/i,
|
| 1335 |
+
/give\s+(me\s+)?(an?\s+)?image/i,
|
| 1336 |
+
/draw\s+(me\s+)?(an?\s+)?/i,
|
| 1337 |
+
/picture\s+(of|for)/i,
|
| 1338 |
+
/image\s+(of|for)/i,
|
| 1339 |
+
/photo\s+(of|for)/i,
|
| 1340 |
+
/generate\s+me/i,
|
| 1341 |
+
/create\s+me/i,
|
| 1342 |
+
/show\s+me\s+(an?\s+)?image/i,
|
| 1343 |
+
/\bimage\b.*\bfor\b/i
|
| 1344 |
+
];
|
| 1345 |
+
|
| 1346 |
+
const isImageRequest = imageKeywords.some(regex => regex.test(message));
|
| 1347 |
+
|
| 1348 |
+
// Check if image generation mode is active OR user asked for an image
|
| 1349 |
+
if (imageGenMode || isImageRequest) {
|
| 1350 |
+
// Extract the image prompt (remove common prefixes)
|
| 1351 |
+
let imagePrompt = message
|
| 1352 |
+
.replace(/^(generate|create|make|draw|show|give)\s+(me\s+)?(an?\s+)?(image|picture|photo)\s+(of\s+|for\s+)?/i, '')
|
| 1353 |
+
.trim() || message;
|
| 1354 |
+
|
| 1355 |
+
// Direct image generation
|
| 1356 |
+
await generateImage(imagePrompt);
|
| 1357 |
+
// Turn off image mode if it was on
|
| 1358 |
+
if (imageGenMode) toggleImageGen();
|
| 1359 |
+
// Save to history
|
| 1360 |
+
saveChatToHistory(message);
|
| 1361 |
+
// Reset state
|
| 1362 |
+
isGenerating = false;
|
| 1363 |
+
sendBtn.innerHTML = SEND_ICON;
|
| 1364 |
+
sendBtn.disabled = true;
|
| 1365 |
+
return;
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
// Show thinking
|
| 1369 |
+
showThinking();
|
| 1370 |
+
|
| 1371 |
+
try {
|
| 1372 |
+
const response = await fetch(`${serverUrl}/chat`, {
|
| 1373 |
+
method: 'POST',
|
| 1374 |
+
headers: {
|
| 1375 |
+
'Content-Type': 'application/json'
|
| 1376 |
+
},
|
| 1377 |
+
body: JSON.stringify({
|
| 1378 |
+
message: message,
|
| 1379 |
+
max_tokens: krceMode ? 420 : 1300,
|
| 1380 |
+
temperature: krceMode ? 0.1 : 0.35,
|
| 1381 |
+
krce_mode: krceMode,
|
| 1382 |
+
history: historyForBackend
|
| 1383 |
+
})
|
| 1384 |
+
});
|
| 1385 |
+
|
| 1386 |
+
const data = await response.json();
|
| 1387 |
+
hideThinking();
|
| 1388 |
+
|
| 1389 |
+
if (data.response) {
|
| 1390 |
+
await typeMessage('assistant', data.response);
|
| 1391 |
+
} else if (data.error) {
|
| 1392 |
+
addMessage('assistant', `Error: ${data.error}`);
|
| 1393 |
+
}
|
| 1394 |
+
|
| 1395 |
+
// Save to history
|
| 1396 |
+
saveChatToHistory(message);
|
| 1397 |
+
|
| 1398 |
+
} catch (err) {
|
| 1399 |
+
hideThinking();
|
| 1400 |
+
addMessage('assistant', 'Connection error. Is the server running?');
|
| 1401 |
+
console.error(err);
|
| 1402 |
+
} finally {
|
| 1403 |
+
// Reset generating state
|
| 1404 |
+
isGenerating = false;
|
| 1405 |
+
sendBtn.innerHTML = SEND_ICON;
|
| 1406 |
+
sendBtn.disabled = true;
|
| 1407 |
+
}
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
// Add message with action buttons
|
| 1411 |
+
function addMessage(role, content) {
|
| 1412 |
+
const container = document.getElementById('chatMessages');
|
| 1413 |
+
const msgIndex = messages.length;
|
| 1414 |
+
|
| 1415 |
+
// Process markdown
|
| 1416 |
+
let html = marked.parse(content);
|
| 1417 |
+
|
| 1418 |
+
// Add code block wrappers handling both with and without language
|
| 1419 |
+
html = html.replace(/<pre><code(?: class="language-(\w+)")?>/g, (match, lang) => {
|
| 1420 |
+
const displayLang = lang || 'code';
|
| 1421 |
+
const classStr = lang ? ` class="language-${lang}"` : '';
|
| 1422 |
+
return `<div class="code-block"><div class="code-header"><span class="code-lang">${displayLang}</span><button class="copy-btn" onclick="copyCode(this)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy</button></div><pre><code${classStr}>`;
|
| 1423 |
+
});
|
| 1424 |
+
html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>');
|
| 1425 |
+
|
| 1426 |
+
// Action buttons based on role
|
| 1427 |
+
const editIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>';
|
| 1428 |
+
|
| 1429 |
+
let actionsHtml = '';
|
| 1430 |
+
if (role === 'user') {
|
| 1431 |
+
actionsHtml = `
|
| 1432 |
+
<div class="message-actions">
|
| 1433 |
+
<button class="action-btn" onclick="copyMessage(${msgIndex}, this)" title="Copy">${COPY_ICON}</button>
|
| 1434 |
+
<button class="action-btn" onclick="editMessage(${msgIndex})" title="Edit">${editIcon}</button>
|
| 1435 |
+
</div>
|
| 1436 |
+
`;
|
| 1437 |
+
} else {
|
| 1438 |
+
actionsHtml = `
|
| 1439 |
+
<div class="message-actions">
|
| 1440 |
+
<button class="action-btn" onclick="copyMessage(${msgIndex}, this)" title="Copy">${COPY_ICON}</button>
|
| 1441 |
+
</div>
|
| 1442 |
+
`;
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
const div = document.createElement('div');
|
| 1446 |
+
div.className = `message ${role}`;
|
| 1447 |
+
div.setAttribute('data-index', msgIndex);
|
| 1448 |
+
div.innerHTML = `
|
| 1449 |
+
<div class="message-wrapper">
|
| 1450 |
+
<div class="message-content">${html}</div>
|
| 1451 |
+
${actionsHtml}
|
| 1452 |
+
</div>
|
| 1453 |
+
`;
|
| 1454 |
+
|
| 1455 |
+
container.appendChild(div);
|
| 1456 |
+
|
| 1457 |
+
// Scroll to bottom
|
| 1458 |
+
const chatContainer = document.getElementById('chatContainer');
|
| 1459 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1460 |
+
|
| 1461 |
+
// Highlight code
|
| 1462 |
+
div.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
| 1463 |
+
|
| 1464 |
+
messages.push({ role, content });
|
| 1465 |
+
persistConversationState();
|
| 1466 |
+
}
|
| 1467 |
+
|
| 1468 |
+
// Copy message content with tick feedback
|
| 1469 |
+
function copyMessage(index, btn) {
|
| 1470 |
+
const msg = messages[index];
|
| 1471 |
+
if (msg) {
|
| 1472 |
+
navigator.clipboard.writeText(msg.content);
|
| 1473 |
+
// Show tick feedback
|
| 1474 |
+
if (btn) {
|
| 1475 |
+
btn.classList.add('copied');
|
| 1476 |
+
setTimeout(() => btn.classList.remove('copied'), 2000);
|
| 1477 |
+
}
|
| 1478 |
+
}
|
| 1479 |
+
}
|
| 1480 |
+
|
| 1481 |
+
// Edit user message
|
| 1482 |
+
function editMessage(index) {
|
| 1483 |
+
const msg = messages[index];
|
| 1484 |
+
if (msg && msg.role === 'user') {
|
| 1485 |
+
document.getElementById('messageInput').value = msg.content;
|
| 1486 |
+
document.getElementById('messageInput').focus();
|
| 1487 |
+
document.getElementById('sendBtn').disabled = false;
|
| 1488 |
+
}
|
| 1489 |
+
}
|
| 1490 |
+
|
| 1491 |
+
// Toggle image generation mode
|
| 1492 |
+
function toggleImageGen() {
|
| 1493 |
+
imageGenMode = !imageGenMode;
|
| 1494 |
+
const btn = document.getElementById('imageGenBtn');
|
| 1495 |
+
const input = document.getElementById('messageInput');
|
| 1496 |
+
|
| 1497 |
+
if (imageGenMode) {
|
| 1498 |
+
btn.classList.add('active');
|
| 1499 |
+
input.placeholder = 'Describe the image you want to create...';
|
| 1500 |
+
} else {
|
| 1501 |
+
btn.classList.remove('active');
|
| 1502 |
+
input.placeholder = krceMode
|
| 1503 |
+
? 'KRCE Mode ON: ask KRCE-only questions...'
|
| 1504 |
+
: 'Message Krish Mind...';
|
| 1505 |
+
}
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
function toggleKrceMode() {
|
| 1509 |
+
krceMode = !krceMode;
|
| 1510 |
+
localStorage.setItem('krceMode', String(krceMode));
|
| 1511 |
+
updateKrceModeUI();
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
function updateKrceModeUI() {
|
| 1515 |
+
const btn = document.getElementById('krceModeBtn');
|
| 1516 |
+
const input = document.getElementById('messageInput');
|
| 1517 |
+
if (!btn || !input) return;
|
| 1518 |
+
|
| 1519 |
+
if (krceMode) {
|
| 1520 |
+
btn.classList.add('active');
|
| 1521 |
+
input.placeholder = 'KRCE Mode ON: ask KRCE-only questions...';
|
| 1522 |
+
} else {
|
| 1523 |
+
btn.classList.remove('active');
|
| 1524 |
+
input.placeholder = imageGenMode
|
| 1525 |
+
? 'Describe the image you want to create...'
|
| 1526 |
+
: 'Message Krish Mind...';
|
| 1527 |
+
}
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
// Generate image directly (frontend-triggered)
|
| 1531 |
+
async function generateImage(prompt) {
|
| 1532 |
+
const chatContainer = document.getElementById('chatContainer');
|
| 1533 |
+
const messagesContainer = document.getElementById('chatMessages');
|
| 1534 |
+
|
| 1535 |
+
// Create loading message
|
| 1536 |
+
const div = document.createElement('div');
|
| 1537 |
+
div.className = 'message assistant';
|
| 1538 |
+
div.innerHTML = `
|
| 1539 |
+
<div class="message-wrapper">
|
| 1540 |
+
<div class="message-content">
|
| 1541 |
+
<p>Generating image: "${prompt}"</p>
|
| 1542 |
+
<div class="image-container loading"></div>
|
| 1543 |
+
</div>
|
| 1544 |
+
</div>
|
| 1545 |
+
`;
|
| 1546 |
+
messagesContainer.appendChild(div);
|
| 1547 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1548 |
+
|
| 1549 |
+
// Create Pollinations URL
|
| 1550 |
+
const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=512&height=512&nologo=true`;
|
| 1551 |
+
|
| 1552 |
+
// Load image
|
| 1553 |
+
const container = div.querySelector('.image-container');
|
| 1554 |
+
const img = new Image();
|
| 1555 |
+
img.onload = () => {
|
| 1556 |
+
container.classList.remove('loading');
|
| 1557 |
+
container.innerHTML = `
|
| 1558 |
+
<img src="${imageUrl}" alt="${prompt}">
|
| 1559 |
+
<div class="image-actions">
|
| 1560 |
+
<button class="image-download-btn" onclick="downloadImage('${imageUrl}', '${prompt.replace(/'/g, "\\'")}')">
|
| 1561 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1562 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
| 1563 |
+
<polyline points="7 10 12 15 17 10"></polyline>
|
| 1564 |
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
| 1565 |
+
</svg>
|
| 1566 |
+
Download
|
| 1567 |
+
</button>
|
| 1568 |
+
</div>
|
| 1569 |
+
`;
|
| 1570 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1571 |
+
};
|
| 1572 |
+
img.onerror = () => {
|
| 1573 |
+
container.classList.remove('loading');
|
| 1574 |
+
container.innerHTML = `<p style="color:var(--text-muted);padding:12px;">Failed to generate image. Try again.</p>`;
|
| 1575 |
+
};
|
| 1576 |
+
img.src = imageUrl;
|
| 1577 |
+
|
| 1578 |
+
// Store in messages
|
| 1579 |
+
messages.push({ role: 'assistant', content: `` });
|
| 1580 |
+
persistConversationState();
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
// Type message with animation (renders markdown progressively)
|
| 1584 |
+
async function typeMessage(role, content) {
|
| 1585 |
+
const chatContainer = document.getElementById('chatContainer');
|
| 1586 |
+
const messagesContainer = document.getElementById('chatMessages');
|
| 1587 |
+
const msgIndex = messages.length;
|
| 1588 |
+
|
| 1589 |
+
const div = document.createElement('div');
|
| 1590 |
+
div.className = 'message assistant';
|
| 1591 |
+
div.setAttribute('data-index', msgIndex);
|
| 1592 |
+
div.innerHTML = `<div class="message-wrapper"><div class="message-content"></div></div>`;
|
| 1593 |
+
messagesContainer.appendChild(div);
|
| 1594 |
+
|
| 1595 |
+
const messageWrapper = div.querySelector('.message-wrapper');
|
| 1596 |
+
const messageContent = div.querySelector('.message-content');
|
| 1597 |
+
let displayedText = '';
|
| 1598 |
+
let i = 0;
|
| 1599 |
+
const speed = 8;
|
| 1600 |
+
|
| 1601 |
+
return new Promise(resolve => {
|
| 1602 |
+
function type() {
|
| 1603 |
+
if (i < content.length) {
|
| 1604 |
+
displayedText += content.charAt(i);
|
| 1605 |
+
i++;
|
| 1606 |
+
|
| 1607 |
+
// Render markdown progressively
|
| 1608 |
+
let html = marked.parse(displayedText + '\u258b');
|
| 1609 |
+
|
| 1610 |
+
// Handle code blocks safely
|
| 1611 |
+
html = html.replace(/<pre><code(?: class="language-(\w+)")?>/g, (match, lang) => {
|
| 1612 |
+
const displayLang = lang || 'code';
|
| 1613 |
+
const classStr = lang ? ` class="language-${lang}"` : '';
|
| 1614 |
+
return `<div class="code-block"><div class="code-header"><span class="code-lang">${displayLang}</span><button class="copy-btn" onclick="copyCode(this)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy</button></div><pre><code${classStr}>`;
|
| 1615 |
+
});
|
| 1616 |
+
html = html.replace(/<\/code><\/pre>/g, '</code></pre></div>');
|
| 1617 |
+
|
| 1618 |
+
// Handle images - show loading container first
|
| 1619 |
+
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
| 1620 |
+
return `<div class="image-container loading" data-src="${src}" data-alt="${alt}"></div>`;
|
| 1621 |
+
});
|
| 1622 |
+
|
| 1623 |
+
messageContent.innerHTML = html;
|
| 1624 |
+
|
| 1625 |
+
// Highlight any code that's complete
|
| 1626 |
+
div.querySelectorAll('pre code').forEach(block => {
|
| 1627 |
+
if (!block.classList.contains('hljs')) {
|
| 1628 |
+
hljs.highlightElement(block);
|
| 1629 |
+
}
|
| 1630 |
+
});
|
| 1631 |
+
|
| 1632 |
+
// Auto-scroll chat container
|
| 1633 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1634 |
+
|
| 1635 |
+
setTimeout(type, speed);
|
| 1636 |
+
} else {
|
| 1637 |
+
// Finished - render final version with safe code block regex
|
| 1638 |
+
let finalHtml = marked.parse(content);
|
| 1639 |
+
finalHtml = finalHtml.replace(/<pre><code(?: class="language-(\w+)")?>/g, (match, lang) => {
|
| 1640 |
+
const displayLang = lang || 'code';
|
| 1641 |
+
const classStr = lang ? ` class="language-${lang}"` : '';
|
| 1642 |
+
return `<div class="code-block"><div class="code-header"><span class="code-lang">${displayLang}</span><button class="copy-btn" onclick="copyCode(this)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy</button></div><pre><code${classStr}>`;
|
| 1643 |
+
});
|
| 1644 |
+
finalHtml = finalHtml.replace(/<\/code><\/pre>/g, '</code></pre></div>');
|
| 1645 |
+
|
| 1646 |
+
// Handle images - show loading container
|
| 1647 |
+
finalHtml = finalHtml.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
| 1648 |
+
return `<div class="image-container loading" data-src="${src}" data-alt="${alt}"></div>`;
|
| 1649 |
+
});
|
| 1650 |
+
|
| 1651 |
+
messageContent.innerHTML = finalHtml;
|
| 1652 |
+
|
| 1653 |
+
// Add copy button for AI response
|
| 1654 |
+
const copyIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
|
| 1655 |
+
const actionsDiv = document.createElement('div');
|
| 1656 |
+
actionsDiv.className = 'message-actions';
|
| 1657 |
+
actionsDiv.innerHTML = `<button class="action-btn" onclick="copyMessage(${msgIndex})" title="Copy">${copyIcon}</button>`;
|
| 1658 |
+
messageWrapper.appendChild(actionsDiv);
|
| 1659 |
+
|
| 1660 |
+
// Load images properly
|
| 1661 |
+
div.querySelectorAll('.image-container.loading').forEach(container => {
|
| 1662 |
+
const src = container.dataset.src;
|
| 1663 |
+
const alt = container.dataset.alt;
|
| 1664 |
+
const img = new Image();
|
| 1665 |
+
img.onload = () => {
|
| 1666 |
+
container.classList.remove('loading');
|
| 1667 |
+
container.innerHTML = `
|
| 1668 |
+
<img src="${src}" alt="${alt}">
|
| 1669 |
+
<div class="image-actions">
|
| 1670 |
+
<button class="image-download-btn" onclick="downloadImage('${src}', '${alt || 'image'}')">
|
| 1671 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 1672 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
| 1673 |
+
<polyline points="7 10 12 15 17 10"></polyline>
|
| 1674 |
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
| 1675 |
+
</svg>
|
| 1676 |
+
Download
|
| 1677 |
+
</button>
|
| 1678 |
+
</div>
|
| 1679 |
+
`;
|
| 1680 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1681 |
+
};
|
| 1682 |
+
img.onerror = () => {
|
| 1683 |
+
container.classList.remove('loading');
|
| 1684 |
+
container.innerHTML = `<p style="color:var(--text-muted);padding:12px;">Failed to load image</p>`;
|
| 1685 |
+
};
|
| 1686 |
+
img.src = src;
|
| 1687 |
+
});
|
| 1688 |
+
|
| 1689 |
+
div.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
|
| 1690 |
+
messages.push({ role, content });
|
| 1691 |
+
persistConversationState();
|
| 1692 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1693 |
+
resolve();
|
| 1694 |
+
}
|
| 1695 |
+
}
|
| 1696 |
+
type();
|
| 1697 |
+
});
|
| 1698 |
+
}
|
| 1699 |
+
|
| 1700 |
+
// Download image
|
| 1701 |
+
function downloadImage(url, filename) {
|
| 1702 |
+
fetch(url)
|
| 1703 |
+
.then(response => response.blob())
|
| 1704 |
+
.then(blob => {
|
| 1705 |
+
const link = document.createElement('a');
|
| 1706 |
+
link.href = URL.createObjectURL(blob);
|
| 1707 |
+
link.download = filename.replace(/[^a-zA-Z0-9]/g, '_') + '.png';
|
| 1708 |
+
link.click();
|
| 1709 |
+
URL.revokeObjectURL(link.href);
|
| 1710 |
+
})
|
| 1711 |
+
.catch(err => {
|
| 1712 |
+
console.error('Download failed:', err);
|
| 1713 |
+
// Fallback: open in new tab
|
| 1714 |
+
window.open(url, '_blank');
|
| 1715 |
+
});
|
| 1716 |
+
}
|
| 1717 |
+
|
| 1718 |
+
// Thinking animation
|
| 1719 |
+
function showThinking() {
|
| 1720 |
+
const container = document.getElementById('chatMessages');
|
| 1721 |
+
const div = document.createElement('div');
|
| 1722 |
+
div.className = 'message assistant';
|
| 1723 |
+
div.id = 'thinking';
|
| 1724 |
+
div.innerHTML = `
|
| 1725 |
+
<div class="message-content">
|
| 1726 |
+
<div class="thinking">
|
| 1727 |
+
<div class="thinking-dots"><span></span><span></span><span></span></div>
|
| 1728 |
+
<span>Thinking...</span>
|
| 1729 |
+
</div>
|
| 1730 |
+
</div>
|
| 1731 |
+
`;
|
| 1732 |
+
container.appendChild(div);
|
| 1733 |
+
// Scroll main container to bottom
|
| 1734 |
+
const chatContainer = document.getElementById('chatContainer');
|
| 1735 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 1736 |
+
}
|
| 1737 |
+
|
| 1738 |
+
function hideThinking() {
|
| 1739 |
+
const thinking = document.getElementById('thinking');
|
| 1740 |
+
if (thinking) thinking.remove();
|
| 1741 |
+
}
|
| 1742 |
+
|
| 1743 |
+
// Copy code
|
| 1744 |
+
function copyCode(btn) {
|
| 1745 |
+
const code = btn.closest('.code-block').querySelector('code').textContent;
|
| 1746 |
+
navigator.clipboard.writeText(code);
|
| 1747 |
+
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> Copied!';
|
| 1748 |
+
setTimeout(() => {
|
| 1749 |
+
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy';
|
| 1750 |
+
}, 2000);
|
| 1751 |
+
}
|
| 1752 |
+
|
| 1753 |
+
// Send suggestion
|
| 1754 |
+
function sendSuggestion(text) {
|
| 1755 |
+
document.getElementById('messageInput').value = text;
|
| 1756 |
+
document.getElementById('sendBtn').disabled = false;
|
| 1757 |
+
sendMessage();
|
| 1758 |
+
}
|
| 1759 |
+
|
| 1760 |
+
// Handle keyboard
|
| 1761 |
+
function handleKeyDown(e) {
|
| 1762 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 1763 |
+
e.preventDefault();
|
| 1764 |
+
sendMessage();
|
| 1765 |
+
}
|
| 1766 |
+
}
|
| 1767 |
+
|
| 1768 |
+
// Auto resize textarea
|
| 1769 |
+
function autoResize(el) {
|
| 1770 |
+
el.style.height = 'auto';
|
| 1771 |
+
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
|
| 1772 |
+
}
|
| 1773 |
+
|
| 1774 |
+
// Save chat to history
|
| 1775 |
+
function saveChatToHistory(firstMessage) {
|
| 1776 |
+
if (!currentChatId) {
|
| 1777 |
+
currentChatId = Date.now();
|
| 1778 |
+
chatHistory.unshift({
|
| 1779 |
+
id: currentChatId,
|
| 1780 |
+
title: firstMessage.slice(0, 30) + (firstMessage.length > 30 ? '...' : ''),
|
| 1781 |
+
timestamp: new Date().toISOString()
|
| 1782 |
+
});
|
| 1783 |
+
} else {
|
| 1784 |
+
const index = chatHistory.findIndex(chat => chat.id === currentChatId);
|
| 1785 |
+
if (index === -1) {
|
| 1786 |
+
chatHistory.unshift({
|
| 1787 |
+
id: currentChatId,
|
| 1788 |
+
title: firstMessage.slice(0, 30) + (firstMessage.length > 30 ? '...' : ''),
|
| 1789 |
+
timestamp: new Date().toISOString()
|
| 1790 |
+
});
|
| 1791 |
+
}
|
| 1792 |
+
}
|
| 1793 |
+
persistConversationState();
|
| 1794 |
+
renderChatHistory();
|
| 1795 |
+
}
|
| 1796 |
+
|
| 1797 |
+
// Close modal on outside click
|
| 1798 |
+
document.getElementById('connectModal').addEventListener('click', (e) => {
|
| 1799 |
+
if (e.target.id === 'connectModal') hideConnectModal();
|
| 1800 |
+
});
|
| 1801 |
+
</script>
|
| 1802 |
+
</body>
|
| 1803 |
+
|
| 1804 |
+
</html>
|