YousifCreates commited on
Commit
81726c9
Β·
1 Parent(s): c920a02

First commit

Browse files
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .vscode
2
+ .env
3
+ venv
4
+ __pycache__
5
+ .venv
6
+ .env.example
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ .Python
11
+ data
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+
3
+ FROM python:3.11-slim
4
+
5
+ # System dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ gcc \
8
+ g++ \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Set working directory
13
+ WORKDIR /app
14
+
15
+ # Copy requirements first (layer caching)
16
+ COPY requirements.txt .
17
+
18
+ # Install Python dependencies (no GPU on HF Spaces)
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Pre-download embedding model during build
22
+ RUN python3 -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('intfloat/multilingual-e5-large')"
23
+
24
+ # Copy project files
25
+ COPY . .
26
+
27
+ # HuggingFace Spaces runs on port 7860
28
+ EXPOSE 7860
29
+
30
+ # Run Flask app
31
+ CMD ["python3", "app.py"]
app.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+
3
+ import os
4
+ from flask import Flask, request, jsonify, render_template
5
+ from dotenv import load_dotenv
6
+ from chain.qa_chain import run_chain
7
+
8
+ load_dotenv()
9
+
10
+ app = Flask(__name__)
11
+
12
+ # ── Routes ────────────────────────────────────────────────────────────────────
13
+
14
+ @app.route("/")
15
+ def index():
16
+ return render_template("index.html")
17
+
18
+
19
+ @app.route("/chat", methods=["POST"])
20
+ def chat():
21
+ data = request.get_json()
22
+ query = data.get("query", "").strip()
23
+ topic = data.get("topic", None) or None # empty string β†’ None
24
+
25
+ if not query:
26
+ return jsonify({"error": "Query is empty."}), 400
27
+
28
+ try:
29
+ response = run_chain(query, topic=topic)
30
+ return jsonify({"response": response})
31
+ except Exception as e:
32
+ return jsonify({"error": str(e)}), 500
33
+
34
+
35
+ # ── Run ───────────────────────────────────────────────────────────────────────
36
+
37
+ if __name__ == "__main__":
38
+ app.run(debug=True, host="0.0.0.0", port=7860)
chain/__init__.py ADDED
File without changes
chain/qa_chain.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # chain/qa_chain.py
2
+
3
+ import os
4
+ from dotenv import load_dotenv
5
+ from openai import OpenAI
6
+ from rag.retriever import retrieve, format_context
7
+
8
+ load_dotenv()
9
+
10
+ # ── Config ───────────────────────────────────────────────────────────────────
11
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
12
+ MODEL = "openai/gpt-oss-120b"
13
+ MAX_TOKENS = 2048
14
+
15
+ # ── OpenRouter Client ─────────────────────────────────────────────────────────
16
+ client = OpenAI(
17
+ api_key = OPENROUTER_API_KEY,
18
+ base_url = "https://openrouter.ai/api/v1"
19
+ )
20
+
21
+ # ── System Prompt ─────────────────────────────────────────────────────────────
22
+ SYSTEM_PROMPT = """You are Study Saathi β€” a friendly and smart study assistant.
23
+ You help students understand their Operating Systems notes.
24
+
25
+ Rules you must follow:
26
+ - Answer ONLY from the provided context. Never use outside knowledge.
27
+ - If the answer is not in the context, say: "Yeh topic notes mein nahi mila."
28
+ - Explain in simple Roman Urdu, Urdu, or English β€” based on what the user uses.
29
+ - If user says that you have to give explaination, try not to use bullet points, use simple language, examples in plain text.
30
+ - Use markdown formatting: headings, bullet points, bold, tables where helpful.
31
+ - Keep explanations clear and student-friendly.
32
+ - For MCQs: generate exactly the number asked, only from the provided context.
33
+ - For MCQ answer keys: always return them in a markdown table.
34
+ """
35
+
36
+ # ── Build Prompt ──────────────────────────────────────────────────────────────
37
+ def build_prompt(query: str, context: str) -> str:
38
+ return f"""Use the following context from the student's notes to answer the question.
39
+
40
+ --- CONTEXT START ---
41
+ {context}
42
+ --- CONTEXT END ---
43
+
44
+ Student's Question: {query}
45
+ """
46
+
47
+ # ── Detect MCQ Request ────────────────────────────────────────────────────────
48
+ def extract_mcq_count(query: str):
49
+ """Returns number of MCQs requested, or None if not an MCQ request."""
50
+ import re
51
+ match = re.search(r'(\d+)\s*(mcq|question|mcqs|questions)', query.lower())
52
+ return int(match.group(1)) if match else None
53
+
54
+ # ── Main Chain ────────────────────────────────────────────────────────────────
55
+ def run_chain(query: str, topic: str = None) -> str:
56
+ """
57
+ Full RAG chain:
58
+ 1. Retrieve chunks from Pinecone
59
+ 2. Format context
60
+ 3. Send to gpt-oss-120b via OpenRouter
61
+ 4. Return response
62
+ """
63
+ mcq_count = extract_mcq_count(query)
64
+
65
+ # fetch more chunks for MCQ generation
66
+ top_k = 10 if mcq_count else 5
67
+ chunks = retrieve(query, topic=topic, top_k=top_k)
68
+
69
+ if not chunks:
70
+ return "Koi relevant content nahi mila notes mein. Please topic check karein."
71
+
72
+ context = format_context(chunks)
73
+ prompt = build_prompt(query, context)
74
+
75
+ # inject MCQ instruction into prompt if needed
76
+ if mcq_count:
77
+ prompt += f"\n\nIMPORTANT: Generate exactly {mcq_count} MCQs from the context above. \
78
+ Format each MCQ as:\n**Q1.** Question\n- A) option\n- B) option\n- C) option\n- D) option\
79
+ \n\nAfter all MCQs, provide the answer key in a markdown table with columns: | Q# | Answer | Explanation |"
80
+
81
+ messages = [
82
+ {"role": "system", "content": SYSTEM_PROMPT},
83
+ {"role": "user", "content": prompt}
84
+ ]
85
+
86
+ response = client.chat.completions.create(
87
+ model = MODEL,
88
+ messages = messages,
89
+ max_tokens = MAX_TOKENS,
90
+ )
91
+
92
+ return response.choices[0].message.content
93
+
94
+
95
+ # ── Quick Test ────────────────────────────────────────────────────────────────
96
+ if __name__ == "__main__":
97
+ query = "Explain Process Registers in simple words"
98
+ topic = "ch-01-updated"
99
+
100
+ print("[INFO] Running chain...\n")
101
+ result = run_chain(query, topic=topic)
102
+ print(result)
rag/__init__.py ADDED
File without changes
rag/ingest.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag/ingest.py
2
+
3
+ import os
4
+ import torch
5
+ from dotenv import load_dotenv
6
+ from pinecone import Pinecone, ServerlessSpec
7
+ from langchain_community.document_loaders import PyPDFLoader, TextLoader
8
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
9
+ from sentence_transformers import SentenceTransformer
10
+ from tqdm import tqdm
11
+
12
+ load_dotenv()
13
+
14
+ # ── Config ──────────────────────────────────────────────────────────────────
15
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
16
+ PINECONE_INDEX = os.getenv("PINECONE_INDEX", "study-saathi")
17
+ EMBEDDING_MODEL = "intfloat/multilingual-e5-large"
18
+ DATA_DIR = "data/os_notes"
19
+ CHUNK_SIZE = 512
20
+ CHUNK_OVERLAP = 64
21
+ BATCH_SIZE = 32
22
+ DIMENSION = 1024 # multilingual-e5-large output dim
23
+
24
+ # ── Device ──────────────────────────────────────────────────────────────────
25
+ device = "cuda" if torch.cuda.is_available() else "cpu"
26
+ print(f"[INFO] Using device: {device}")
27
+
28
+ # ── Load Embedding Model ─────────────────────────────────────────────────────
29
+ print("[INFO] Loading embedding model...")
30
+ embedder = SentenceTransformer(EMBEDDING_MODEL, device=device)
31
+
32
+ # ── Pinecone Setup ───────────────────────────────────────────────────────────
33
+ pc = Pinecone(api_key=PINECONE_API_KEY)
34
+
35
+ if PINECONE_INDEX not in [i.name for i in pc.list_indexes()]:
36
+ print(f"[INFO] Creating Pinecone index: {PINECONE_INDEX}")
37
+ pc.create_index(
38
+ name=PINECONE_INDEX,
39
+ dimension=DIMENSION,
40
+ metric="cosine",
41
+ spec=ServerlessSpec(cloud="aws", region="us-east-1")
42
+ )
43
+
44
+ index = pc.Index(PINECONE_INDEX)
45
+
46
+ # ── Load Documents ───────────────────────────────────────────────────────────
47
+ def load_documents(data_dir: str) -> list:
48
+ docs = []
49
+ for filename in os.listdir(data_dir):
50
+ filepath = os.path.join(data_dir, filename)
51
+ if filename.endswith(".pdf"):
52
+ loader = PyPDFLoader(filepath)
53
+ elif filename.endswith(".txt"):
54
+ loader = TextLoader(filepath, encoding="utf-8")
55
+ else:
56
+ print(f"[SKIP] Unsupported file: {filename}")
57
+ continue
58
+ loaded = loader.load()
59
+ # attach filename as topic metadata
60
+ topic = os.path.splitext(filename)[0]
61
+ for doc in loaded:
62
+ doc.metadata["topic"] = topic
63
+ doc.metadata["source"] = filename
64
+ docs.extend(loaded)
65
+ print(f"[LOADED] {filename} β€” {len(loaded)} page(s)")
66
+ return docs
67
+
68
+ # ── Chunk Documents ──────────────────────────────────────────────────────────
69
+ def chunk_documents(docs: list) -> list:
70
+ splitter = RecursiveCharacterTextSplitter(
71
+ chunk_size=CHUNK_SIZE,
72
+ chunk_overlap=CHUNK_OVERLAP
73
+ )
74
+ chunks = splitter.split_documents(docs)
75
+ print(f"[INFO] Total chunks: {len(chunks)}")
76
+ return chunks
77
+
78
+ # ── Embed & Upsert ───────────────────────────────────────────────────────────
79
+ def embed_and_upsert(chunks: list):
80
+ texts = [
81
+ f"passage: {chunk.page_content}" for chunk in chunks # e5 prefix
82
+ ]
83
+ print("[INFO] Generating embeddings...")
84
+ all_vectors = []
85
+
86
+ for i in tqdm(range(0, len(texts), BATCH_SIZE)):
87
+ batch_texts = texts[i: i + BATCH_SIZE]
88
+ batch_chunks = chunks[i: i + BATCH_SIZE]
89
+ embeddings = embedder.encode(
90
+ batch_texts,
91
+ normalize_embeddings=True,
92
+ show_progress_bar=False
93
+ )
94
+ vectors = []
95
+ for j, (emb, chunk) in enumerate(zip(embeddings, batch_chunks)):
96
+ vectors.append({
97
+ "id": f"chunk-{i + j}",
98
+ "values": emb.tolist(),
99
+ "metadata": {
100
+ "text": chunk.page_content,
101
+ "topic": chunk.metadata.get("topic", "unknown"),
102
+ "source": chunk.metadata.get("source", "unknown"),
103
+ }
104
+ })
105
+ all_vectors.extend(vectors)
106
+
107
+ # upsert in batches of 100 (Pinecone limit)
108
+ print("[INFO] Upserting to Pinecone...")
109
+ for i in tqdm(range(0, len(all_vectors), 100)):
110
+ index.upsert(vectors=all_vectors[i: i + 100])
111
+
112
+ print(f"[DONE] Upserted {len(all_vectors)} chunks to Pinecone.")
113
+
114
+ # ── Main ─────────────────────────────────────────────────────────────────────
115
+ if __name__ == "__main__":
116
+ docs = load_documents(DATA_DIR)
117
+ if not docs:
118
+ print("[ERROR] No documents found in data/os_notes/")
119
+ exit(1)
120
+ chunks = chunk_documents(docs)
121
+ embed_and_upsert(chunks)
rag/retriever.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag/retriever.py
2
+
3
+ import os
4
+ import torch
5
+ from dotenv import load_dotenv
6
+ from pinecone import Pinecone
7
+ from sentence_transformers import SentenceTransformer
8
+
9
+ load_dotenv()
10
+
11
+ # ── Config ───────────────────────────────────────────────────────────────────
12
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
13
+ PINECONE_INDEX = os.getenv("PINECONE_INDEX", "study-saathi")
14
+ EMBEDDING_MODEL = "intfloat/multilingual-e5-large"
15
+ TOP_K = 5
16
+
17
+ # ── Device ───────────────────────────────────────────────────────────────────
18
+ device = "cuda" if torch.cuda.is_available() else "cpu"
19
+
20
+ # ── Load Embedding Model ──────────────────────────────────────────────────────
21
+ print("[INFO] Loading embedding model...")
22
+ embedder = SentenceTransformer(EMBEDDING_MODEL, device=device)
23
+
24
+ # ── Pinecone Setup ────────────────────────────────────────────────────────────
25
+ pc = Pinecone(api_key=PINECONE_API_KEY)
26
+ index = pc.Index(PINECONE_INDEX)
27
+
28
+ # ── Retrieve ──────────────────────────────────────────────────────────────────
29
+ def retrieve(query: str, topic: str = None, top_k: int = TOP_K) -> list:
30
+ """
31
+ Retrieve relevant chunks from Pinecone.
32
+ - query : user's question or topic
33
+ - topic : filename-based topic filter (e.g. "deadlocks")
34
+ - top_k : number of chunks to return
35
+ """
36
+ # e5 requires "query: " prefix for questions
37
+ query_embedding = embedder.encode(
38
+ f"query: {query}",
39
+ normalize_embeddings=True
40
+ ).tolist()
41
+
42
+ # build filter only if topic is provided
43
+ filter_dict = {"topic": {"$eq": topic}} if topic else None
44
+
45
+ results = index.query(
46
+ vector=query_embedding,
47
+ top_k=top_k,
48
+ include_metadata=True,
49
+ filter=filter_dict
50
+ )
51
+
52
+ chunks = []
53
+ for match in results["matches"]:
54
+ chunks.append({
55
+ "text": match["metadata"]["text"],
56
+ "topic": match["metadata"].get("topic", "unknown"),
57
+ "score": round(match["score"], 4)
58
+ })
59
+
60
+ return chunks
61
+
62
+
63
+ # ── Format for LLM ────────────────────────────────────────────────────────────
64
+ def format_context(chunks: list) -> str:
65
+ """Joins retrieved chunks into a single context string for the LLM."""
66
+ return "\n\n".join(
67
+ f"[Chunk {i+1} | Topic: {c['topic']} | Score: {c['score']}]\n{c['text']}"
68
+ for i, c in enumerate(chunks)
69
+ )
70
+
71
+
72
+ # ── Quick Test ────────────────────────────────────────────────────────────────
73
+ if __name__ == "__main__":
74
+ query = "Explain Process Registers?"
75
+ topic = "ch-01-updated" # change to match your filename
76
+
77
+ chunks = retrieve(query, topic=topic)
78
+ context = format_context(chunks)
79
+
80
+ print(context)
requirements.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM & LangChain
2
+ langchain==0.3.25
3
+ langchain-community==0.3.24
4
+ langchain-openai==0.3.16
5
+ openai==1.84.0
6
+
7
+ # Vector Database
8
+ pinecone-client==5.0.1
9
+
10
+ # Embeddings (local GPU)
11
+ sentence-transformers==3.4.1
12
+ torch==2.5.1
13
+ transformers==4.51.3
14
+
15
+ # Document Loading
16
+ pypdf==5.4.0
17
+ unstructured==0.17.2
18
+
19
+ # Backend
20
+ flask==3.1.1
21
+ flask-cors==5.0.1
22
+ python-dotenv==1.1.0
23
+
24
+ # Utilities
25
+ tqdm==4.67.1
26
+ numpy==1.26.4
static/css/style.css ADDED
@@ -0,0 +1,606 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── Variables ──────────────────────────────────────────────────────────────── */
2
+ :root {
3
+ --bg: #0a0f1a;
4
+ --bg-2: #0f1624;
5
+ --bg-3: #151e30;
6
+ --surface: #1a2540;
7
+ --surface-2: #1f2d4a;
8
+ --border: #243354;
9
+ --border-light: #2e3f63;
10
+
11
+ --accent: #38bdf8;
12
+ --accent-dim: #0ea5e915;
13
+ --accent-glow: #38bdf830;
14
+ --accent-dark: #0369a1;
15
+
16
+ --text: #e2eaf5;
17
+ --text-2: #94a3b8;
18
+ --text-3: #64748b;
19
+
20
+ --user-bubble: #0c2d4a;
21
+ --ai-bubble: #111827;
22
+
23
+ --radius: 14px;
24
+ --radius-sm: 8px;
25
+ --sidebar-w: 270px;
26
+
27
+ --font-display: 'Syne', sans-serif;
28
+ --font-mono: 'DM Mono', monospace;
29
+
30
+ --shadow: 0 4px 24px #00000050;
31
+ --shadow-accent:0 0 24px #38bdf820;
32
+ }
33
+
34
+ /* ── Reset ──────────────────────────────────────────────────────────────────── */
35
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
36
+
37
+ html, body {
38
+ height: 100%;
39
+ background: var(--bg);
40
+ color: var(--text);
41
+ font-family: var(--font-display);
42
+ font-size: 15px;
43
+ overflow: hidden;
44
+ }
45
+
46
+ /* ── Background Effects ─────────────────────────────────────────────────────── */
47
+ .bg-grid {
48
+ position: fixed;
49
+ inset: 0;
50
+ background-image:
51
+ linear-gradient(var(--border) 1px, transparent 1px),
52
+ linear-gradient(90deg, var(--border) 1px, transparent 1px);
53
+ background-size: 40px 40px;
54
+ opacity: 0.18;
55
+ pointer-events: none;
56
+ z-index: 0;
57
+ }
58
+
59
+ .bg-glow {
60
+ position: fixed;
61
+ top: -200px;
62
+ left: 50%;
63
+ transform: translateX(-50%);
64
+ width: 700px;
65
+ height: 500px;
66
+ background: radial-gradient(ellipse, #38bdf812 0%, transparent 70%);
67
+ pointer-events: none;
68
+ z-index: 0;
69
+ }
70
+
71
+ /* ── Layout ─────────────────────────────────────────────────────────────────── */
72
+ body { display: flex; position: relative; z-index: 1; }
73
+
74
+ /* ── Sidebar ─────────────────────────────────────────────────────────────────── */
75
+ .sidebar {
76
+ width: var(--sidebar-w);
77
+ min-width: var(--sidebar-w);
78
+ height: 100vh;
79
+ background: var(--bg-2);
80
+ border-right: 1px solid var(--border);
81
+ display: flex;
82
+ flex-direction: column;
83
+ gap: 0;
84
+ z-index: 100;
85
+ transition: transform 0.3s cubic-bezier(.4,0,.2,1);
86
+ overflow-y: auto;
87
+ }
88
+
89
+ .sidebar-header {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: space-between;
93
+ padding: 20px 18px 16px;
94
+ border-bottom: 1px solid var(--border);
95
+ }
96
+
97
+ .logo {
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 8px;
101
+ font-size: 1.2rem;
102
+ font-weight: 800;
103
+ letter-spacing: -0.02em;
104
+ }
105
+
106
+ .logo-icon { font-size: 1.4rem; }
107
+
108
+ .sidebar-close {
109
+ display: none;
110
+ background: none;
111
+ border: none;
112
+ color: var(--text-2);
113
+ font-size: 1.1rem;
114
+ cursor: pointer;
115
+ padding: 4px 8px;
116
+ border-radius: var(--radius-sm);
117
+ transition: color 0.2s, background 0.2s;
118
+ }
119
+
120
+ .sidebar-close:hover { color: var(--text); background: var(--surface); }
121
+
122
+ .sidebar-section {
123
+ padding: 18px;
124
+ border-bottom: 1px solid var(--border);
125
+ }
126
+
127
+ .sidebar-label {
128
+ display: block;
129
+ font-size: 0.7rem;
130
+ font-weight: 700;
131
+ letter-spacing: 0.12em;
132
+ text-transform: uppercase;
133
+ color: var(--text-3);
134
+ margin-bottom: 10px;
135
+ }
136
+
137
+ .topic-input-wrap {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 8px;
141
+ background: var(--surface);
142
+ border: 1px solid var(--border);
143
+ border-radius: var(--radius-sm);
144
+ padding: 8px 12px;
145
+ transition: border-color 0.2s, box-shadow 0.2s;
146
+ }
147
+
148
+ .topic-input-wrap:focus-within {
149
+ border-color: var(--accent);
150
+ box-shadow: 0 0 0 3px var(--accent-glow);
151
+ }
152
+
153
+ .topic-icon { font-size: 0.9rem; }
154
+
155
+ .topic-input {
156
+ background: none;
157
+ border: none;
158
+ outline: none;
159
+ color: var(--text);
160
+ font-family: var(--font-mono);
161
+ font-size: 0.82rem;
162
+ width: 100%;
163
+ }
164
+
165
+ .topic-input::placeholder { color: var(--text-3); }
166
+
167
+ .sidebar-hint {
168
+ font-size: 0.72rem;
169
+ color: var(--text-3);
170
+ margin-top: 7px;
171
+ }
172
+
173
+ .quick-prompts {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 7px;
177
+ }
178
+
179
+ .quick-btn {
180
+ background: var(--surface);
181
+ border: 1px solid var(--border);
182
+ color: var(--text-2);
183
+ font-family: var(--font-display);
184
+ font-size: 0.8rem;
185
+ padding: 9px 12px;
186
+ border-radius: var(--radius-sm);
187
+ cursor: pointer;
188
+ text-align: left;
189
+ transition: all 0.2s;
190
+ }
191
+
192
+ .quick-btn:hover {
193
+ background: var(--accent-dim);
194
+ border-color: var(--accent);
195
+ color: var(--accent);
196
+ transform: translateX(3px);
197
+ }
198
+
199
+ .sidebar-footer {
200
+ margin-top: auto;
201
+ padding: 16px 18px;
202
+ font-size: 0.72rem;
203
+ color: var(--text-3);
204
+ line-height: 1.7;
205
+ font-family: var(--font-mono);
206
+ border-top: 1px solid var(--border);
207
+ }
208
+
209
+ /* ── Main ────────────────────────────────────────────────────────────────────── */
210
+ .main {
211
+ flex: 1;
212
+ display: flex;
213
+ flex-direction: column;
214
+ height: 100vh;
215
+ overflow: hidden;
216
+ min-width: 0;
217
+ }
218
+
219
+ /* ── Topbar ──────────────────────────────────────────────────────────────────── */
220
+ .topbar {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 12px;
224
+ padding: 14px 20px;
225
+ border-bottom: 1px solid var(--border);
226
+ background: var(--bg-2);
227
+ z-index: 10;
228
+ flex-shrink: 0;
229
+ }
230
+
231
+ .menu-btn {
232
+ display: none;
233
+ flex-direction: column;
234
+ gap: 5px;
235
+ background: none;
236
+ border: none;
237
+ cursor: pointer;
238
+ padding: 6px;
239
+ border-radius: var(--radius-sm);
240
+ transition: background 0.2s;
241
+ }
242
+
243
+ .menu-btn:hover { background: var(--surface); }
244
+
245
+ .menu-btn span {
246
+ display: block;
247
+ width: 20px;
248
+ height: 2px;
249
+ background: var(--text-2);
250
+ border-radius: 2px;
251
+ }
252
+
253
+ .topbar-title {
254
+ flex: 1;
255
+ font-size: 1.05rem;
256
+ font-weight: 800;
257
+ letter-spacing: -0.01em;
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 7px;
261
+ }
262
+
263
+ .clear-btn {
264
+ background: none;
265
+ border: 1px solid var(--border);
266
+ color: var(--text-3);
267
+ font-size: 1rem;
268
+ padding: 6px 10px;
269
+ border-radius: var(--radius-sm);
270
+ cursor: pointer;
271
+ transition: all 0.2s;
272
+ }
273
+
274
+ .clear-btn:hover {
275
+ border-color: #ef444455;
276
+ color: #ef4444;
277
+ background: #ef444410;
278
+ }
279
+
280
+ /* ── Chat Window ─────────────────────────────────────────────────────────────── */
281
+ .chat-window {
282
+ flex: 1;
283
+ overflow-y: auto;
284
+ padding: 24px 20px;
285
+ display: flex;
286
+ flex-direction: column;
287
+ gap: 20px;
288
+ scroll-behavior: smooth;
289
+ }
290
+
291
+ .chat-window::-webkit-scrollbar { width: 5px; }
292
+ .chat-window::-webkit-scrollbar-track { background: transparent; }
293
+ .chat-window::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
294
+ .chat-window::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
295
+
296
+ /* ── Messages ────────────────────────────────────────────────────────────────── */
297
+ .message {
298
+ display: flex;
299
+ gap: 12px;
300
+ max-width: 820px;
301
+ width: 100%;
302
+ animation: fadeUp 0.35s ease forwards;
303
+ }
304
+
305
+ @keyframes fadeUp {
306
+ from { opacity: 0; transform: translateY(12px); }
307
+ to { opacity: 1; transform: translateY(0); }
308
+ }
309
+
310
+ .message.user { flex-direction: row-reverse; align-self: flex-end; }
311
+ .message.assistant { align-self: flex-start; }
312
+
313
+ .avatar {
314
+ width: 36px;
315
+ height: 36px;
316
+ border-radius: 10px;
317
+ background: linear-gradient(135deg, var(--accent-dark), var(--accent));
318
+ color: #fff;
319
+ font-size: 0.65rem;
320
+ font-weight: 700;
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ flex-shrink: 0;
325
+ letter-spacing: 0.05em;
326
+ box-shadow: var(--shadow-accent);
327
+ }
328
+
329
+ .message.user .avatar {
330
+ background: linear-gradient(135deg, #1e3a5f, #2563eb);
331
+ }
332
+
333
+ .bubble {
334
+ background: var(--ai-bubble);
335
+ border: 1px solid var(--border);
336
+ border-radius: var(--radius);
337
+ padding: 14px 18px;
338
+ line-height: 1.7;
339
+ font-size: 0.92rem;
340
+ max-width: calc(100% - 50px);
341
+ word-break: break-word;
342
+ }
343
+
344
+ .message.user .bubble {
345
+ background: var(--user-bubble);
346
+ border-color: var(--accent-dark);
347
+ color: var(--text);
348
+ }
349
+
350
+ /* ── Markdown Styles inside bubble ──────────────────────────────────────────── */
351
+ .bubble h1, .bubble h2, .bubble h3, .bubble h4 {
352
+ font-family: var(--font-display);
353
+ font-weight: 700;
354
+ color: var(--accent);
355
+ margin: 16px 0 8px;
356
+ line-height: 1.3;
357
+ }
358
+
359
+ .bubble h1 { font-size: 1.2rem; }
360
+ .bubble h2 { font-size: 1.05rem; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
361
+ .bubble h3 { font-size: 0.95rem; }
362
+ .bubble h4 { font-size: 0.88rem; color: var(--text-2); }
363
+
364
+ .bubble p { margin: 8px 0; }
365
+
366
+ .bubble ul, .bubble ol {
367
+ padding-left: 20px;
368
+ margin: 8px 0;
369
+ }
370
+
371
+ .bubble li { margin: 4px 0; }
372
+
373
+ .bubble strong { color: var(--accent); font-weight: 600; }
374
+ .bubble em { color: var(--text-2); font-style: italic; }
375
+
376
+ .bubble code {
377
+ font-family: var(--font-mono);
378
+ font-size: 0.82rem;
379
+ background: var(--surface);
380
+ border: 1px solid var(--border);
381
+ padding: 2px 6px;
382
+ border-radius: 4px;
383
+ color: #7dd3fc;
384
+ }
385
+
386
+ .bubble pre {
387
+ background: #0d1117;
388
+ border: 1px solid var(--border);
389
+ border-radius: var(--radius-sm);
390
+ padding: 14px;
391
+ overflow-x: auto;
392
+ margin: 12px 0;
393
+ }
394
+
395
+ .bubble pre code {
396
+ background: none;
397
+ border: none;
398
+ padding: 0;
399
+ font-size: 0.82rem;
400
+ color: var(--text);
401
+ }
402
+
403
+ /* ── Tables ──────────────────────────────────────────────────────────────────── */
404
+ .bubble table {
405
+ width: 100%;
406
+ border-collapse: collapse;
407
+ margin: 14px 0;
408
+ font-size: 0.85rem;
409
+ overflow-x: auto;
410
+ display: block;
411
+ }
412
+
413
+ .bubble thead tr {
414
+ background: var(--surface-2);
415
+ border-bottom: 2px solid var(--accent);
416
+ }
417
+
418
+ .bubble th {
419
+ padding: 10px 14px;
420
+ text-align: left;
421
+ font-weight: 700;
422
+ color: var(--accent);
423
+ font-size: 0.8rem;
424
+ letter-spacing: 0.04em;
425
+ white-space: nowrap;
426
+ }
427
+
428
+ .bubble td {
429
+ padding: 9px 14px;
430
+ border-bottom: 1px solid var(--border);
431
+ color: var(--text-2);
432
+ vertical-align: top;
433
+ }
434
+
435
+ .bubble tbody tr:hover { background: var(--accent-dim); }
436
+
437
+ .bubble blockquote {
438
+ border-left: 3px solid var(--accent);
439
+ padding: 8px 16px;
440
+ margin: 10px 0;
441
+ background: var(--accent-dim);
442
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
443
+ color: var(--text-2);
444
+ font-style: italic;
445
+ }
446
+
447
+ .bubble hr {
448
+ border: none;
449
+ border-top: 1px solid var(--border);
450
+ margin: 14px 0;
451
+ }
452
+
453
+ /* ── Typing Indicator ────────────────────────────────────────────────────────── */
454
+ .typing-indicator {
455
+ display: flex;
456
+ gap: 5px;
457
+ align-items: center;
458
+ padding: 10px 14px;
459
+ }
460
+
461
+ .typing-indicator span {
462
+ width: 7px;
463
+ height: 7px;
464
+ background: var(--accent);
465
+ border-radius: 50%;
466
+ animation: bounce 1.2s infinite;
467
+ }
468
+
469
+ .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
470
+ .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
471
+
472
+ @keyframes bounce {
473
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
474
+ 40% { transform: translateY(-6px); opacity: 1; }
475
+ }
476
+
477
+ /* ── Input Area ──────────────────────────────────────────────────────────────── */
478
+ .input-area {
479
+ padding: 14px 20px 16px;
480
+ border-top: 1px solid var(--border);
481
+ background: var(--bg-2);
482
+ flex-shrink: 0;
483
+ }
484
+
485
+ .input-wrap {
486
+ display: flex;
487
+ gap: 10px;
488
+ align-items: flex-end;
489
+ background: var(--surface);
490
+ border: 1px solid var(--border);
491
+ border-radius: var(--radius);
492
+ padding: 10px 10px 10px 16px;
493
+ transition: border-color 0.2s, box-shadow 0.2s;
494
+ }
495
+
496
+ .input-wrap:focus-within {
497
+ border-color: var(--accent);
498
+ box-shadow: 0 0 0 3px var(--accent-glow);
499
+ }
500
+
501
+ .query-input {
502
+ flex: 1;
503
+ background: none;
504
+ border: none;
505
+ outline: none;
506
+ color: var(--text);
507
+ font-family: var(--font-display);
508
+ font-size: 0.92rem;
509
+ line-height: 1.6;
510
+ resize: none;
511
+ max-height: 140px;
512
+ overflow-y: auto;
513
+ }
514
+
515
+ .query-input::placeholder { color: var(--text-3); }
516
+
517
+ .send-btn {
518
+ background: var(--accent);
519
+ border: none;
520
+ color: var(--bg);
521
+ width: 38px;
522
+ height: 38px;
523
+ border-radius: 10px;
524
+ cursor: pointer;
525
+ display: flex;
526
+ align-items: center;
527
+ justify-content: center;
528
+ flex-shrink: 0;
529
+ transition: all 0.2s;
530
+ }
531
+
532
+ .send-btn svg { width: 17px; height: 17px; }
533
+
534
+ .send-btn:hover {
535
+ background: #7dd3fc;
536
+ transform: scale(1.05);
537
+ box-shadow: 0 0 14px var(--accent-glow);
538
+ }
539
+
540
+ .send-btn:disabled {
541
+ background: var(--surface-2);
542
+ color: var(--text-3);
543
+ cursor: not-allowed;
544
+ transform: none;
545
+ box-shadow: none;
546
+ }
547
+
548
+ .input-hint {
549
+ font-size: 0.7rem;
550
+ color: var(--text-3);
551
+ margin-top: 7px;
552
+ font-family: var(--font-mono);
553
+ }
554
+
555
+ /* ── Accent ──────────────────────────────────────────────────────────────────── */
556
+ .accent { color: var(--accent); }
557
+
558
+ /* ── Overlay ─────────────────────────────────────────────────────────────────── */
559
+ .overlay {
560
+ display: none;
561
+ position: fixed;
562
+ inset: 0;
563
+ background: #00000070;
564
+ z-index: 90;
565
+ backdrop-filter: blur(2px);
566
+ }
567
+
568
+ /* ── Mobile Responsive ───────────────────────────────────────────────────────── */
569
+ @media (max-width: 768px) {
570
+ .sidebar {
571
+ position: fixed;
572
+ top: 0;
573
+ left: 0;
574
+ height: 100vh;
575
+ transform: translateX(-100%);
576
+ box-shadow: var(--shadow);
577
+ }
578
+
579
+ .sidebar.open { transform: translateX(0); }
580
+
581
+ .sidebar-close { display: flex; }
582
+
583
+ .overlay.show { display: block; }
584
+
585
+ .menu-btn { display: flex; }
586
+
587
+ .topbar-title { font-size: 0.95rem; }
588
+
589
+ .chat-window { padding: 16px 12px; gap: 16px; }
590
+
591
+ .bubble { font-size: 0.88rem; padding: 12px 14px; }
592
+
593
+ .input-area { padding: 10px 12px 12px; }
594
+
595
+ .input-hint { display: none; }
596
+
597
+ .bubble table { font-size: 0.78rem; }
598
+
599
+ .bubble th, .bubble td { padding: 7px 10px; }
600
+ }
601
+
602
+ @media (max-width: 420px) {
603
+ .message { gap: 8px; }
604
+ .avatar { width: 30px; height: 30px; font-size: 0.6rem; border-radius: 8px; }
605
+ .bubble { padding: 10px 12px; font-size: 0.85rem; }
606
+ }
static/js/design.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/js/design.js
2
+
3
+ // ── Marked.js config ──────────────────────────────────────────────────────────
4
+ marked.setOptions({
5
+ breaks: true,
6
+ gfm: true,
7
+ highlight: (code, lang) => {
8
+ if (lang && hljs.getLanguage(lang)) {
9
+ return hljs.highlight(code, { language: lang }).value;
10
+ }
11
+ return hljs.highlightAuto(code).value;
12
+ }
13
+ });
14
+
15
+ // ── DOM refs ──────────────────────────────────────────────────────────────────
16
+ const chatWindow = document.getElementById("chatWindow");
17
+ const queryInput = document.getElementById("queryInput");
18
+ const sendBtn = document.getElementById("sendBtn");
19
+ const topicInput = document.getElementById("topicInput");
20
+ const clearBtn = document.getElementById("clearBtn");
21
+ const menuBtn = document.getElementById("menuBtn");
22
+ const sidebar = document.getElementById("sidebar");
23
+ const overlay = document.getElementById("overlay");
24
+ const sidebarClose= document.getElementById("sidebarClose");
25
+ const quickBtns = document.querySelectorAll(".quick-btn");
26
+
27
+ // ── Sidebar toggle ────────────────────────────────────────────────────────────
28
+ function openSidebar() {
29
+ sidebar.classList.add("open");
30
+ overlay.classList.add("show");
31
+ }
32
+
33
+ function closeSidebar() {
34
+ sidebar.classList.remove("open");
35
+ overlay.classList.remove("show");
36
+ }
37
+
38
+ menuBtn.addEventListener("click", openSidebar);
39
+ sidebarClose.addEventListener("click", closeSidebar);
40
+ overlay.addEventListener("click", closeSidebar);
41
+
42
+ // ── Auto-resize textarea ──────────────────────────────────────────────────────
43
+ queryInput.addEventListener("input", () => {
44
+ queryInput.style.height = "auto";
45
+ queryInput.style.height = Math.min(queryInput.scrollHeight, 140) + "px";
46
+ });
47
+
48
+ // ── Quick prompt buttons ──────────────────────────────────────────────────────
49
+ quickBtns.forEach(btn => {
50
+ btn.addEventListener("click", () => {
51
+ queryInput.value = btn.dataset.prompt;
52
+ queryInput.dispatchEvent(new Event("input"));
53
+ queryInput.focus();
54
+ closeSidebar();
55
+ });
56
+ });
57
+
58
+ // ── Clear chat ────────────────────────────────────────────────────────────────
59
+ clearBtn.addEventListener("click", () => {
60
+ // keep only the welcome message
61
+ const welcome = document.getElementById("welcomeMsg");
62
+ chatWindow.innerHTML = "";
63
+ if (welcome) chatWindow.appendChild(welcome);
64
+ });
65
+
66
+ // ── Scroll to bottom ──────────────────────────────────────────────────────────
67
+ function scrollToBottom() {
68
+ chatWindow.scrollTo({ top: chatWindow.scrollHeight, behavior: "smooth" });
69
+ }
70
+
71
+ // ── Add message bubble ────────────────────────────────────────────────────────
72
+ function addMessage(role, content) {
73
+ const msg = document.createElement("div");
74
+ msg.className = `message ${role}`;
75
+
76
+ const avatar = document.createElement("div");
77
+ avatar.className = "avatar";
78
+ avatar.textContent = role === "user" ? "YOU" : "SS";
79
+
80
+ const bubble = document.createElement("div");
81
+ bubble.className = "bubble";
82
+
83
+ if (role === "assistant") {
84
+ bubble.innerHTML = marked.parse(content);
85
+ // syntax highlight all code blocks
86
+ bubble.querySelectorAll("pre code").forEach(el => hljs.highlightElement(el));
87
+ } else {
88
+ // user message: plain text, escape HTML
89
+ bubble.textContent = content;
90
+ }
91
+
92
+ msg.appendChild(avatar);
93
+ msg.appendChild(bubble);
94
+ chatWindow.appendChild(msg);
95
+ scrollToBottom();
96
+ return msg;
97
+ }
98
+
99
+ // ── Typing indicator ──────────────────────────────────────────────────────────
100
+ function showTyping() {
101
+ const msg = document.createElement("div");
102
+ msg.className = "message assistant";
103
+ msg.id = "typingIndicator";
104
+
105
+ const avatar = document.createElement("div");
106
+ avatar.className = "avatar";
107
+ avatar.textContent = "SS";
108
+
109
+ const bubble = document.createElement("div");
110
+ bubble.className = "bubble";
111
+ bubble.innerHTML = `
112
+ <div class="typing-indicator">
113
+ <span></span><span></span><span></span>
114
+ </div>`;
115
+
116
+ msg.appendChild(avatar);
117
+ msg.appendChild(bubble);
118
+ chatWindow.appendChild(msg);
119
+ scrollToBottom();
120
+ }
121
+
122
+ function hideTyping() {
123
+ const indicator = document.getElementById("typingIndicator");
124
+ if (indicator) indicator.remove();
125
+ }
126
+
127
+ // ── Send message ──────────────────────────────────────────────────────────────
128
+ async function sendMessage() {
129
+ const query = queryInput.value.trim();
130
+ if (!query) return;
131
+
132
+ const topic = topicInput.value.trim() || null;
133
+
134
+ // show user message
135
+ addMessage("user", query);
136
+
137
+ // reset input
138
+ queryInput.value = "";
139
+ queryInput.style.height = "auto";
140
+ sendBtn.disabled = true;
141
+
142
+ // show typing
143
+ showTyping();
144
+
145
+ try {
146
+ const res = await fetch("/chat", {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({ query, topic })
150
+ });
151
+
152
+ const data = await res.json();
153
+ hideTyping();
154
+
155
+ if (data.error) {
156
+ addMessage("assistant", `⚠️ **Error:** ${data.error}`);
157
+ } else {
158
+ addMessage("assistant", data.response);
159
+ }
160
+ } catch (err) {
161
+ hideTyping();
162
+ addMessage("assistant", "⚠️ **Network error.** Flask server se connect nahi ho saka. Please check karein.");
163
+ } finally {
164
+ sendBtn.disabled = false;
165
+ queryInput.focus();
166
+ }
167
+ }
168
+
169
+ // ── Send on button click ──────────────────────────────────────────────────────
170
+ sendBtn.addEventListener("click", sendMessage);
171
+
172
+ // ── Send on Enter (Shift+Enter = new line) ────────────────────────────────────
173
+ queryInput.addEventListener("keydown", (e) => {
174
+ if (e.key === "Enter" && !e.shiftKey) {
175
+ e.preventDefault();
176
+ sendMessage();
177
+ }
178
+ });
templates/index.html ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Study Saathi</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:wght@300;400;500&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/static/css/style.css" />
11
+ <!-- Marked.js for markdown rendering -->
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
13
+ <!-- highlight.js for code blocks -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
16
+ </head>
17
+ <body>
18
+
19
+ <!-- Background grid -->
20
+ <div class="bg-grid"></div>
21
+ <div class="bg-glow"></div>
22
+
23
+ <!-- ── Sidebar ── -->
24
+ <aside class="sidebar" id="sidebar">
25
+ <div class="sidebar-header">
26
+ <div class="logo">
27
+ <span class="logo-icon">πŸ“š</span>
28
+ <span class="logo-text">Study<span class="accent">Saathi</span></span>
29
+ </div>
30
+ <button class="sidebar-close" id="sidebarClose" aria-label="Close sidebar">βœ•</button>
31
+ </div>
32
+
33
+ <div class="sidebar-section">
34
+ <label class="sidebar-label">Filter by Topic</label>
35
+ <div class="topic-input-wrap">
36
+ <span class="topic-icon">πŸ—‚</span>
37
+ <input
38
+ type="text"
39
+ id="topicInput"
40
+ class="topic-input"
41
+ placeholder="e.g. ch-01-updated"
42
+ />
43
+ </div>
44
+ <p class="sidebar-hint">Leave empty to search all notes</p>
45
+ </div>
46
+
47
+ <div class="sidebar-section">
48
+ <label class="sidebar-label">Quick Prompts</label>
49
+ <div class="quick-prompts">
50
+ <button class="quick-btn" data-prompt="Explain this topic in simple Roman Urdu">πŸ“– Explain Topic</button>
51
+ <button class="quick-btn" data-prompt="Generate 5 MCQs from this topic">πŸ“ 5 MCQs</button>
52
+ <button class="quick-btn" data-prompt="Generate 10 MCQs from this topic">πŸ“ 10 MCQs</button>
53
+ <button class="quick-btn" data-prompt="Summarize the key points of this topic">πŸ”‘ Key Points</button>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="sidebar-footer">
58
+ <p>Powered by <span class="accent">gpt-oss-120b</span></p>
59
+ <p>RAG Β· Pinecone Β· LangChain</p>
60
+ </div>
61
+ </aside>
62
+
63
+ <!-- Overlay for mobile sidebar -->
64
+ <div class="overlay" id="overlay"></div>
65
+
66
+ <!-- ── Main ── -->
67
+ <main class="main">
68
+
69
+ <!-- Top bar -->
70
+ <header class="topbar">
71
+ <button class="menu-btn" id="menuBtn" aria-label="Open sidebar">
72
+ <span></span><span></span><span></span>
73
+ </button>
74
+ <div class="topbar-title">
75
+ <span class="logo-icon">πŸ“š</span>
76
+ Study<span class="accent">Saathi</span>
77
+ </div>
78
+ <button class="clear-btn" id="clearBtn" title="Clear chat">πŸ—‘</button>
79
+ </header>
80
+
81
+ <!-- Chat window -->
82
+ <section class="chat-window" id="chatWindow">
83
+ <!-- Welcome message -->
84
+ <div class="message assistant" id="welcomeMsg">
85
+ <div class="avatar">SS</div>
86
+ <div class="bubble">
87
+ <p>Assalam-o-Alaikum! πŸ‘‹ Main hoon <strong>Study Saathi</strong> β€” tumhara OS notes ka study partner.</p>
88
+ <ul>
89
+ <li>Koi bhi topic <strong>explain</strong> kara sakte ho</li>
90
+ <li><strong>MCQs generate</strong> karo kisi bhi topic se</li>
91
+ <li>Left sidebar se <strong>topic filter</strong> lagao</li>
92
+ </ul>
93
+ <p>Shuru karo! πŸš€</p>
94
+ </div>
95
+ </div>
96
+ </section>
97
+
98
+ <!-- Input area -->
99
+ <footer class="input-area">
100
+ <div class="input-wrap">
101
+ <textarea
102
+ id="queryInput"
103
+ class="query-input"
104
+ placeholder="Koi sawaal poochho... (e.g. Explain deadlocks, Generate 5 MCQs)"
105
+ rows="1"
106
+ ></textarea>
107
+ <button class="send-btn" id="sendBtn" aria-label="Send">
108
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
109
+ <line x1="22" y1="2" x2="11" y2="13"></line>
110
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
111
+ </svg>
112
+ </button>
113
+ </div>
114
+ <p class="input-hint">Enter to send Β· Shift+Enter for new line</p>
115
+ </footer>
116
+
117
+ </main>
118
+
119
+ <script src="/static/js/design.js"></script>
120
+ </body>
121
+ </html>