GitHub Copilot commited on
Commit
a654c7f
·
1 Parent(s): 96b8200

Deploy Docker Space package (no binary assets)

Browse files
.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: green
5
- colorTo: purple
6
  sdk: docker
 
7
  pinned: false
8
- license: apache-2.0
9
- short_description: Explore Krish Mind , Your Lightning fast Model
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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![{prompt}]({url})"}
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: `![${prompt}](${imageUrl})` });
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>