Shirpi commited on
Commit
a531dee
·
verified ·
1 Parent(s): 9f6a49b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +434 -150
app.py CHANGED
@@ -2,7 +2,6 @@ import os
2
  import uuid
3
  import time
4
  import json
5
- import markdown
6
  from flask import Flask, request, jsonify, render_template_string
7
  import google.generativeai as genai
8
 
@@ -12,11 +11,6 @@ import google.generativeai as genai
12
  keys_string = os.environ.get("API_KEYS", "")
13
  API_KEYS = [k.strip() for k in keys_string.replace(',', ' ').replace('\n', ' ').split() if k.strip()]
14
 
15
- print(f"✅ Loaded {len(API_KEYS)} API Keys.")
16
-
17
- current_key_index = 0
18
- app = Flask(__name__)
19
-
20
  # --- 💾 DATABASE ---
21
  DB_FILE = "chat_db.json"
22
  def load_db():
@@ -31,160 +25,360 @@ def save_db(db):
31
  except: pass
32
  user_db = load_db()
33
 
34
- # --- 🧠 SYSTEM INSTRUCTION (UPDATED FOR ENGLISH DEFAULT) ---
 
 
 
35
  SYSTEM_INSTRUCTION = """
36
- ROLE: You are "Student's AI", a coding tutor.
37
 
38
  RULES:
39
- 1. **DEFAULT LANGUAGE:** Always explain in **ENGLISH** unless the user explicitly asks for Tamil or Tanglish.
40
- 2. **CODE FORMAT:** Always format code inside Markdown blocks (```python ... ```).
41
- 3. **CLARITY:** Keep explanations simple, short, and beginner-friendly.
42
- 4. **NO HINDI:** Do not use Hindi. Use only English or Tamil/Tanglish when requested.
 
43
  """
44
 
45
- # --- 🧬 DYNAMIC MODEL FINDER (The Magic Fix) ---
46
  def get_working_model(key):
47
- """
48
- Asks Google: 'What models are available for this key?'
49
- Returns the best available model name to avoid 404 errors.
50
- """
51
  genai.configure(api_key=key)
52
  try:
53
  models = list(genai.list_models())
54
-
55
- # Filter models that support chat generation
56
  chat_models = [m for m in models if 'generateContent' in m.supported_generation_methods]
57
-
58
- # Priority Logic: Try to find Flash -> Pro 1.5 -> Pro 1.0
59
  for m in chat_models:
60
  if "flash" in m.name.lower() and "1.5" in m.name: return m.name
61
  for m in chat_models:
62
  if "pro" in m.name.lower() and "1.5" in m.name: return m.name
63
- for m in chat_models:
64
- if "pro" in m.name.lower() and "1.0" in m.name: return m.name
65
-
66
- # Fallback: Take the first available chat model
67
- if chat_models:
68
- return chat_models[0].name
69
-
70
- except Exception as e:
71
- print(f"⚠️ Error scanning models: {e}")
72
- return None
73
  return None
74
 
75
  def generate_with_retry(prompt, history_messages=[]):
76
  global current_key_index
77
-
78
- if not API_KEYS: return "🚨 API Keys Missing. Please check Secrets."
79
-
80
- # History Format
81
- formatted_history = []
82
- for msg in history_messages[-6:]:
83
- role = "user" if msg["role"] == "user" else "model"
84
- formatted_history.append({"role": role, "parts": [msg["content"]]})
85
 
86
- log_messages = []
87
-
88
- # RETRY LOGIC (Keys + Dynamic Model)
89
  for i in range(len(API_KEYS)):
90
  key = API_KEYS[current_key_index]
 
91
 
92
- # STEP 1: Find a valid model name for this key
93
- valid_model_name = get_working_model(key)
94
-
95
- if not valid_model_name:
96
- log_messages.append(f"Key #{current_key_index+1} -> No chat models found.")
97
  current_key_index = (current_key_index + 1) % len(API_KEYS)
98
  continue
99
 
100
- # STEP 2: Try to generate
101
  try:
102
  genai.configure(api_key=key)
103
- model = genai.GenerativeModel(model_name=valid_model_name, system_instruction=SYSTEM_INSTRUCTION)
104
-
105
  chat = model.start_chat(history=formatted_history)
106
  response = chat.send_message(prompt)
107
- return response.text # SUCCESS!
108
-
109
- except Exception as e:
110
- err = str(e)
111
- log_messages.append(f"Key #{current_key_index+1} ({valid_model_name}) -> {err}")
112
  current_key_index = (current_key_index + 1) % len(API_KEYS)
113
  time.sleep(1)
114
 
115
- return f"⚠️ **ALL FAILED:**<br><small>{'<br>'.join(log_messages)}</small>"
116
 
117
- # --- UI TEMPLATE (STABLE) ---
118
  HTML_TEMPLATE = """
119
  <!DOCTYPE html>
120
  <html lang="en">
121
  <head>
122
  <meta charset="UTF-8">
123
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
124
  <title>Student's AI</title>
125
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
 
 
126
  <style>
127
- :root { --bg: #000; --sidebar: #0a0a0a; --header: rgba(0,0,0,0.9); --user-bg: #222; --text: #ececec; }
128
- body, html { margin: 0; padding: 0; height: 100%; width: 100%; background: var(--bg); color: var(--text); font-family: sans-serif; overflow: hidden; }
129
- pre { background: #161616; border: 1px solid #333; padding: 15px; overflow-x: auto; color: #e6edf3; border-radius: 8px; }
130
- #app-container { display: flex; flex-direction: column; height: 100%; }
131
- header { height: 60px; padding: 0 20px; background: var(--header); border-bottom: 1px solid #222; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; z-index: 50; }
132
- #sidebar { position: fixed; inset: 0; width: 280px; background: var(--sidebar); border-right: 1px solid #222; transform: translateX(-100%); transition: 0.3s; z-index: 100; padding: 20px; display: flex; flex-direction: column; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  #sidebar.open { transform: translateX(0); }
134
- .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 90; display: none; }
135
- .overlay.active { display: block; }
136
- #chat-box { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 20px; }
137
- .msg { padding: 12px 18px; border-radius: 10px; max-width: 85%; line-height: 1.6; }
138
- .user-msg { align-self: flex-end; background: #222; color: #fff; }
139
- .ai-msg { align-self: flex-start; color: #ececec; }
140
- .input-wrap { padding: 15px; background: #000; border-top: 1px solid #333; display: flex; gap: 10px; z-index: 60; }
141
- input { flex: 1; padding: 12px; border-radius: 25px; border: 1px solid #333; background: #111; color: #fff; outline: none; }
142
- button { padding: 10px 20px; border-radius: 25px; border: none; background: #fff; font-weight: bold; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- /* Login Styles */
145
- #login-overlay { position: fixed; inset: 0; z-index: 2000; background: #000; display: flex; flex-direction: column; align-items: center; justify-content: center; }
146
- .login-box { width: 90%; max-width: 350px; padding: 30px; background: #111; border: 1px solid #333; border-radius: 15px; text-align: center; }
147
- .login-input { width: 100%; padding: 12px; background: #000; border: 1px solid #333; color: #fff; border-radius: 8px; margin-bottom: 15px; text-align: center; }
148
- .login-btn { width: 100%; padding: 12px; background: #fff; color: #000; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </style>
150
  </head>
151
  <body>
 
152
  <div id="login-overlay">
153
  <div class="login-box">
154
- <h2 style="color:#fff;">Student's AI</h2>
155
- <input type="text" id="username-input" class="login-input" placeholder="Enter Name" autocomplete="off">
156
- <button onclick="handleLogin()" class="login-btn">Start</button>
 
157
  </div>
158
  </div>
 
159
  <div id="app-container">
160
  <div class="overlay" id="overlay" onclick="toggleSidebar()"></div>
161
  <div id="sidebar">
162
- <div style="padding-bottom:20px; border-bottom:1px solid #222; margin-bottom:20px; font-weight:bold; display:flex; gap:10px; align-items:center;">
163
- <div id="avatar" style="width:35px; height:35px; background:#333; border-radius:50%; display:flex; align-items:center; justify-content:center;">U</div>
164
- <span id="display-name">User</span>
165
  </div>
166
- <div style="background:#fff; color:#000; padding:12px; border-radius:8px; text-align:center; font-weight:bold; cursor:pointer;" onclick="newChat()">+ New Chat</div>
167
- <div style="flex:1; overflow-y:auto; margin-top:20px;" id="history-list"></div>
168
- <div style="margin-top:20px; color:#ff5555; cursor:pointer; text-align:center;" onclick="handleLogout()">Log Out</div>
 
 
 
 
 
169
  </div>
 
170
  <header>
171
- <i class="fas fa-bars" style="color:#fff; font-size:20px; cursor:pointer;" onclick="toggleSidebar()"></i>
172
- <div style="font-weight:bold; font-size:18px; color:#fff;">Student's AI</div>
 
 
173
  <div style="width:20px;"></div>
174
  </header>
 
175
  <div id="chat-box"></div>
176
- <div class="input-wrap">
177
- <input id="input" placeholder="Ask a coding question..." onkeydown="if(event.key==='Enter') send()">
178
- <button onclick="send()">Send</button>
 
 
 
179
  </div>
180
  </div>
 
181
  <script>
182
- let currentChatId = null;
183
  let currentUser = null;
184
-
 
 
185
  function checkLogin() {
186
  const stored = localStorage.getItem("student_ai_user");
187
- if (stored) { currentUser = stored; showApp(); } else { document.getElementById("login-overlay").style.display = "flex"; }
188
  }
189
  function handleLogin() {
190
  const name = document.getElementById("username-input").value.trim();
@@ -194,124 +388,214 @@ HTML_TEMPLATE = """
194
 
195
  function showApp() {
196
  document.getElementById("login-overlay").style.display = "none";
197
- document.getElementById("app-container").style.display = "flex";
198
  document.getElementById("display-name").innerText = currentUser;
199
  document.getElementById("avatar").innerText = currentUser[0].toUpperCase();
200
  loadHistory();
201
  if(!currentChatId) {
202
- const box = document.getElementById("chat-box");
203
- if(box.innerHTML.trim() === "") { box.innerHTML = `<div class="msg ai-msg"><strong>Welcome ${currentUser}!</strong><br>I am ready. Ask me any code.</div>`; }
 
 
204
  }
205
  }
206
-
 
 
 
 
 
 
207
  async function send() {
208
  const input = document.getElementById('input');
209
  const text = input.value.trim();
210
- if(!text) return;
211
-
 
212
  const chatBox = document.getElementById('chat-box');
213
- chatBox.innerHTML += `<div class="msg user-msg">${text}</div>`;
214
- input.value = '';
215
 
216
- // Loading
217
- const loadId = "load-"+Date.now();
218
- chatBox.innerHTML += `<div id="${loadId}" class="msg ai-msg">Thinking...</div>`;
 
 
 
 
219
  chatBox.scrollTop = chatBox.scrollHeight;
220
 
221
  try {
222
- if(!currentChatId) {
223
- const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
 
224
  const d = await r.json(); currentChatId = d.chat_id;
 
225
  }
 
 
226
  const res = await fetch('/chat', {
227
  method: 'POST', headers: {'Content-Type': 'application/json'},
228
  body: JSON.stringify({message: text, username: currentUser, chat_id: currentChatId})
229
  });
230
  const data = await res.json();
231
- document.getElementById(loadId).innerHTML = data.response;
232
- } catch(e) {
233
- document.getElementById(loadId).innerHTML = "Error: " + e.message;
 
 
 
 
 
 
 
 
 
 
234
  }
235
- chatBox.scrollTop = chatBox.scrollHeight;
236
  }
237
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  async function loadHistory() {
239
  try {
240
- const res = await fetch('/get_history', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: currentUser }) });
241
  const data = await res.json();
242
- const list = document.getElementById("history-list"); list.innerHTML = "";
243
- if(data.chats) Object.keys(data.chats).reverse().forEach(cid => { list.innerHTML += `<div style="padding:10px; color:#888; cursor:pointer;" onclick="loadChat('${cid}')"><i class="fas fa-comment"></i> ${data.chats[cid].title}</div>`; });
 
 
 
 
 
 
244
  } catch(e) {}
245
  }
 
 
 
 
 
 
 
 
 
 
246
  async function newChat() {
247
- try {
248
- const res = await fetch('/new_chat', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: currentUser }) });
249
- const data = await res.json(); currentChatId = data.chat_id;
250
- document.getElementById('chat-box').innerHTML = '';
251
- loadHistory();
252
- document.getElementById('sidebar').classList.remove('open');
253
- document.getElementById('overlay').classList.remove('active');
254
- } catch(e) {}
255
  }
 
256
  async function loadChat(cid) {
 
257
  currentChatId = cid;
258
- const res = await fetch('/get_chat', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: currentUser, chat_id: cid }) });
259
- const data = await res.json();
260
- const box = document.getElementById('chat-box'); box.innerHTML = '';
 
 
 
261
  data.messages.forEach(msg => {
262
  const cls = msg.role === 'user' ? 'user-msg' : 'ai-msg';
263
- box.innerHTML += `<div class="msg ${cls}">${msg.content}</div>`;
 
 
264
  });
265
- document.getElementById('sidebar').classList.remove('open');
266
- document.getElementById('overlay').classList.remove('active');
267
  box.scrollTop = box.scrollHeight;
268
  }
269
- function toggleSidebar() {
270
- document.getElementById('sidebar').classList.toggle('open');
271
- document.getElementById('overlay').classList.toggle('active');
272
- }
273
-
274
  checkLogin();
275
  </script>
276
  </body>
277
  </html>
278
  """
279
 
280
- # --- ROUTES ---
281
  @app.route("/", methods=["GET"])
282
  def home(): return render_template_string(HTML_TEMPLATE)
283
 
284
  @app.route("/new_chat", methods=["POST"])
285
  def new_chat():
286
- user = request.json.get("username")
287
- if user not in user_db: user_db[user] = {}
288
  nid = str(uuid.uuid4())
289
- user_db[user][nid] = {"title": "New Chat", "messages": []}
290
  save_db(user_db)
291
  return jsonify({"chat_id": nid})
292
 
 
 
 
 
 
 
 
 
 
 
293
  @app.route("/chat", methods=["POST"])
294
  def chat():
295
  d = request.json
296
  u, cid, msg = d.get("username"), d.get("chat_id"), d.get("message")
297
-
298
  if u not in user_db: user_db[u] = {}
299
  if cid not in user_db[u]: user_db[u][cid] = {"messages": []}
300
 
301
  user_db[u][cid]["messages"].append({"role": "user", "content": msg})
302
 
303
- # CALL DYNAMIC GENERATOR
304
  reply = generate_with_retry(msg, user_db[u][cid]["messages"][:-1])
305
 
306
- # Error Handling for Display
307
- if "FAILED" in reply or "Missing" in reply:
308
- formatted_html = reply
309
- else:
310
- formatted_html = markdown.markdown(reply, extensions=['fenced_code'])
311
-
312
- user_db[u][cid]["messages"].append({"role": "model", "content": formatted_html})
 
 
313
  save_db(user_db)
314
- return jsonify({"response": formatted_html})
315
 
316
  if __name__ == '__main__':
317
- app.run(host='0.0.0.0', port=7860)
 
 
2
  import uuid
3
  import time
4
  import json
 
5
  from flask import Flask, request, jsonify, render_template_string
6
  import google.generativeai as genai
7
 
 
11
  keys_string = os.environ.get("API_KEYS", "")
12
  API_KEYS = [k.strip() for k in keys_string.replace(',', ' ').replace('\n', ' ').split() if k.strip()]
13
 
 
 
 
 
 
14
  # --- 💾 DATABASE ---
15
  DB_FILE = "chat_db.json"
16
  def load_db():
 
25
  except: pass
26
  user_db = load_db()
27
 
28
+ current_key_index = 0
29
+ app = Flask(__name__)
30
+
31
+ # --- 🧠 SYSTEM INSTRUCTION (UPDATED) ---
32
  SYSTEM_INSTRUCTION = """
33
+ ROLE: You are a "Universal Tutor" for students. You help with Coding, Science, Math, and General concepts.
34
 
35
  RULES:
36
+ 1. **TITLES:** When explaining a concept (e.g., Velocity, Loops), always start with the topic name in **Bold** (e.g., **Velocity**).
37
+ 2. **FORMAT:** Use Markdown.
38
+ 3. **CODE:** If the answer involves coding, strictly use Markdown Code Blocks (```python ... ```).
39
+ 4. **STYLE:** Explain clearly. If asked for code, explain the logic briefly then show the code.
40
+ 5. **LANGUAGE:** English by default. Use Tamil/Tanglish ONLY if requested.
41
  """
42
 
43
+ # --- 🧬 DYNAMIC MODEL FINDER ---
44
  def get_working_model(key):
 
 
 
 
45
  genai.configure(api_key=key)
46
  try:
47
  models = list(genai.list_models())
 
 
48
  chat_models = [m for m in models if 'generateContent' in m.supported_generation_methods]
 
 
49
  for m in chat_models:
50
  if "flash" in m.name.lower() and "1.5" in m.name: return m.name
51
  for m in chat_models:
52
  if "pro" in m.name.lower() and "1.5" in m.name: return m.name
53
+ if chat_models: return chat_models[0].name
54
+ except: return None
 
 
 
 
 
 
 
 
55
  return None
56
 
57
  def generate_with_retry(prompt, history_messages=[]):
58
  global current_key_index
59
+ if not API_KEYS: return "🚨 API Keys Missing."
 
 
 
 
 
 
 
60
 
61
+ formatted_history = [{"role": ("user" if m["role"]=="user" else "model"), "parts": [m["content"]]} for m in history_messages[-6:]]
62
+
 
63
  for i in range(len(API_KEYS)):
64
  key = API_KEYS[current_key_index]
65
+ model_name = get_working_model(key)
66
 
67
+ if not model_name:
 
 
 
 
68
  current_key_index = (current_key_index + 1) % len(API_KEYS)
69
  continue
70
 
 
71
  try:
72
  genai.configure(api_key=key)
73
+ model = genai.GenerativeModel(model_name=model_name, system_instruction=SYSTEM_INSTRUCTION)
 
74
  chat = model.start_chat(history=formatted_history)
75
  response = chat.send_message(prompt)
76
+ return response.text
77
+ except:
 
 
 
78
  current_key_index = (current_key_index + 1) % len(API_KEYS)
79
  time.sleep(1)
80
 
81
+ return "⚠️ System Busy. Please try again."
82
 
83
+ # --- UI TEMPLATE (UNIQUE DESIGN + ANIMATIONS) ---
84
  HTML_TEMPLATE = """
85
  <!DOCTYPE html>
86
  <html lang="en">
87
  <head>
88
  <meta charset="UTF-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content">
90
  <title>Student's AI</title>
91
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
92
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
93
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
94
  <style>
95
+ :root {
96
+ --bg-color: #09090b;
97
+ --sidebar-bg: #000000;
98
+ --card-bg: #18181b;
99
+ --user-msg-bg: #27272a;
100
+ --accent-color: #ffffff;
101
+ --text-color: #e4e4e7;
102
+ --code-bg: #111113;
103
+ --border-color: #27272a;
104
+ }
105
+
106
+ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
107
+
108
+ body, html {
109
+ margin: 0; padding: 0;
110
+ height: 100%; width: 100%;
111
+ background-color: var(--bg-color);
112
+ color: var(--text-color);
113
+ font-family: 'Outfit', sans-serif;
114
+ overflow: hidden; /* Prevents body scroll */
115
+ }
116
+
117
+ /* --- LAYOUT --- */
118
+ #app-container {
119
+ display: flex;
120
+ flex-direction: column;
121
+ height: 100dvh; /* Dynamic Height for Mobile */
122
+ position: relative;
123
+ }
124
+
125
+ /* --- HEADER --- */
126
+ header {
127
+ height: 60px;
128
+ padding: 0 20px;
129
+ background: rgba(9, 9, 11, 0.9);
130
+ backdrop-filter: blur(10px);
131
+ border-bottom: 1px solid var(--border-color);
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ flex-shrink: 0;
136
+ z-index: 50;
137
+ }
138
+
139
+ /* --- SIDEBAR --- */
140
+ #sidebar {
141
+ position: fixed; top: 0; left: 0; bottom: 0;
142
+ width: 280px;
143
+ background: var(--sidebar-bg);
144
+ border-right: 1px solid var(--border-color);
145
+ transform: translateX(-100%);
146
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
147
+ z-index: 100;
148
+ display: flex;
149
+ flex-direction: column;
150
+ padding: 20px;
151
+ }
152
  #sidebar.open { transform: translateX(0); }
153
+ .overlay {
154
+ position: fixed; inset: 0;
155
+ background: rgba(0,0,0,0.6);
156
+ backdrop-filter: blur(2px);
157
+ z-index: 90;
158
+ opacity: 0; pointer-events: none;
159
+ transition: opacity 0.3s;
160
+ }
161
+ .overlay.active { opacity: 1; pointer-events: auto; }
162
+
163
+ /* Search & History */
164
+ .search-box {
165
+ background: var(--card-bg);
166
+ border: 1px solid var(--border-color);
167
+ padding: 10px;
168
+ border-radius: 8px;
169
+ color: #fff;
170
+ width: 100%;
171
+ margin-bottom: 15px;
172
+ font-family: 'Outfit', sans-serif;
173
+ }
174
+ #history-list { flex: 1; overflow-y: auto; }
175
+ .history-item {
176
+ padding: 12px;
177
+ border-radius: 8px;
178
+ cursor: pointer;
179
+ color: #a1a1aa;
180
+ font-size: 14px;
181
+ transition: 0.2s;
182
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
183
+ }
184
+ .history-item:hover { background: var(--user-msg-bg); color: #fff; }
185
+
186
+ /* --- CHAT AREA --- */
187
+ #chat-box {
188
+ flex: 1;
189
+ overflow-y: auto;
190
+ padding: 20px 20px 40px 20px;
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 25px;
194
+ scroll-behavior: smooth;
195
+ }
196
+
197
+ /* Message Styles */
198
+ .msg {
199
+ max-width: 90%;
200
+ width: fit-content;
201
+ line-height: 1.6;
202
+ font-size: 15px;
203
+ animation: fadeIn 0.3s ease;
204
+ }
205
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
206
+
207
+ .user-msg {
208
+ align-self: flex-end;
209
+ background: var(--user-msg-bg);
210
+ color: #fff;
211
+ padding: 12px 18px;
212
+ border-radius: 18px 18px 4px 18px;
213
+ }
214
+
215
+ .ai-msg {
216
+ align-self: flex-start;
217
+ color: var(--text-color);
218
+ width: 100%;
219
+ }
220
+
221
+ /* Markdown Styling */
222
+ .ai-msg strong { color: #fff; font-weight: 600; }
223
+ .ai-msg h1, .ai-msg h2, .ai-msg h3 { margin-top: 10px; color: #fff; }
224
+ .ai-msg p { margin-bottom: 10px; }
225
+
226
+ /* Code Box */
227
+ .ai-msg pre {
228
+ background: var(--code-bg);
229
+ border: 1px solid var(--border-color);
230
+ border-radius: 8px;
231
+ padding: 15px;
232
+ overflow-x: auto;
233
+ margin: 10px 0;
234
+ font-family: 'JetBrains Mono', monospace;
235
+ }
236
+ .ai-msg code {
237
+ font-family: 'JetBrains Mono', monospace;
238
+ font-size: 13px;
239
+ color: #a5d6ff;
240
+ background: rgba(255,255,255,0.05);
241
+ padding: 2px 4px;
242
+ border-radius: 4px;
243
+ }
244
+ .ai-msg pre code { background: transparent; padding: 0; color: #e6edf3; }
245
+
246
+ /* --- INPUT AREA (FIXED) --- */
247
+ .input-wrapper {
248
+ background: var(--bg-color);
249
+ padding: 15px 20px;
250
+ border-top: 1px solid var(--border-color);
251
+ flex-shrink: 0;
252
+ z-index: 60;
253
+ }
254
+ .input-bar {
255
+ max-width: 800px;
256
+ margin: 0 auto;
257
+ background: var(--card-bg);
258
+ border: 1px solid var(--border-color);
259
+ border-radius: 24px;
260
+ padding: 8px 10px 8px 20px;
261
+ display: flex;
262
+ align-items: center;
263
+ transition: border 0.2s;
264
+ }
265
+ .input-bar:focus-within { border-color: #555; }
266
+
267
+ textarea {
268
+ flex: 1;
269
+ background: transparent;
270
+ border: none;
271
+ color: #fff;
272
+ font-size: 16px;
273
+ max-height: 120px;
274
+ padding: 5px 0;
275
+ resize: none;
276
+ outline: none;
277
+ font-family: 'Outfit', sans-serif;
278
+ }
279
 
280
+ .send-btn {
281
+ width: 38px; height: 38px;
282
+ background: #fff;
283
+ color: #000;
284
+ border-radius: 50%;
285
+ border: none;
286
+ cursor: pointer;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ transition: transform 0.2s;
291
+ }
292
+ .send-btn:hover { transform: scale(1.05); }
293
+
294
+ /* --- LOGIN --- */
295
+ #login-overlay {
296
+ position: fixed; inset: 0;
297
+ background: #000;
298
+ z-index: 2000;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ }
303
+ .login-box {
304
+ width: 90%; max-width: 350px;
305
+ text-align: center;
306
+ padding: 30px;
307
+ border: 1px solid var(--border-color);
308
+ border-radius: 16px;
309
+ background: var(--card-bg);
310
+ }
311
+ .login-inp {
312
+ width: 100%; padding: 12px;
313
+ margin: 20px 0;
314
+ background: #000; border: 1px solid #333;
315
+ color: #fff; border-radius: 8px;
316
+ text-align: center;
317
+ outline: none;
318
+ }
319
+ .login-btn-start {
320
+ width: 100%; padding: 12px;
321
+ background: #fff; color: #000;
322
+ border: none; border-radius: 8px;
323
+ font-weight: 600; cursor: pointer;
324
+ }
325
+
326
  </style>
327
  </head>
328
  <body>
329
+
330
  <div id="login-overlay">
331
  <div class="login-box">
332
+ <h2 style="margin-bottom:5px;">Student's AI</h2>
333
+ <p style="color:#666; font-size:13px;">Your Personal Tutor</p>
334
+ <input type="text" id="username-input" class="login-inp" placeholder="Enter your name" autocomplete="off">
335
+ <button onclick="handleLogin()" class="login-btn-start">Start Learning</button>
336
  </div>
337
  </div>
338
+
339
  <div id="app-container">
340
  <div class="overlay" id="overlay" onclick="toggleSidebar()"></div>
341
  <div id="sidebar">
342
+ <div style="padding-bottom:15px; border-bottom:1px solid #333; margin-bottom:15px; display:flex; align-items:center; gap:10px;">
343
+ <div style="width:32px; height:32px; background:#fff; color:#000; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:bold;" id="avatar">U</div>
344
+ <span id="display-name" style="font-weight:600;">User</span>
345
  </div>
346
+
347
+ <button onclick="newChat()" style="width:100%; padding:10px; background:#fff; border:none; border-radius:6px; cursor:pointer; font-weight:600; margin-bottom:15px;">+ New Chat</button>
348
+
349
+ <input type="text" class="search-box" placeholder="Search history..." onkeyup="filterHistory(this.value)">
350
+
351
+ <div id="history-list"></div>
352
+
353
+ <div style="margin-top:auto; padding-top:15px; border-top:1px solid #333; text-align:center; color:#ef4444; cursor:pointer; font-size:14px;" onclick="handleLogout()">Log Out</div>
354
  </div>
355
+
356
  <header>
357
+ <div style="display:flex; align-items:center; gap:15px;">
358
+ <i class="fas fa-bars" style="cursor:pointer; font-size:18px;" onclick="toggleSidebar()"></i>
359
+ <span style="font-weight:600; font-size:18px;">Student's AI</span>
360
+ </div>
361
  <div style="width:20px;"></div>
362
  </header>
363
+
364
  <div id="chat-box"></div>
365
+
366
+ <div class="input-wrapper">
367
+ <div class="input-bar">
368
+ <textarea id="input" placeholder="Ask about Velocity, Coding, etc..." rows="1" oninput="resizeInput(this)"></textarea>
369
+ <button class="send-btn" onclick="send()"><i class="fas fa-arrow-up"></i></button>
370
+ </div>
371
  </div>
372
  </div>
373
+
374
  <script>
 
375
  let currentUser = null;
376
+ let currentChatId = null;
377
+
378
+ // --- LOGIN LOGIC ---
379
  function checkLogin() {
380
  const stored = localStorage.getItem("student_ai_user");
381
+ if (stored) { currentUser = stored; showApp(); }
382
  }
383
  function handleLogin() {
384
  const name = document.getElementById("username-input").value.trim();
 
388
 
389
  function showApp() {
390
  document.getElementById("login-overlay").style.display = "none";
 
391
  document.getElementById("display-name").innerText = currentUser;
392
  document.getElementById("avatar").innerText = currentUser[0].toUpperCase();
393
  loadHistory();
394
  if(!currentChatId) {
395
+ const box = document.getElementById('chat-box');
396
+ if(box.innerHTML === '') {
397
+ box.innerHTML = `<div class="msg ai-msg">Hello <strong>${currentUser}</strong>! I am ready to teach. <br>Ask me about any concept or code.</div>`;
398
+ }
399
  }
400
  }
401
+
402
+ // --- CHAT LOGIC ---
403
+ function resizeInput(el) {
404
+ el.style.height = 'auto';
405
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
406
+ }
407
+
408
  async function send() {
409
  const input = document.getElementById('input');
410
  const text = input.value.trim();
411
+ if (!text) return;
412
+
413
+ // 1. Show User Message
414
  const chatBox = document.getElementById('chat-box');
415
+ chatBox.innerHTML += `<div class="msg user-msg">${text.replace(/</g, "&lt;")}</div>`;
 
416
 
417
+ input.value = '';
418
+ input.style.height = 'auto';
419
+ chatBox.scrollTop = chatBox.scrollHeight;
420
+
421
+ // 2. Prepare AI Container (Hidden initially for typing effect)
422
+ const msgId = "ai-" + Date.now();
423
+ chatBox.innerHTML += `<div id="${msgId}" class="msg ai-msg"><i class="fas fa-circle-notch fa-spin"></i></div>`;
424
  chatBox.scrollTop = chatBox.scrollHeight;
425
 
426
  try {
427
+ // Ensure Chat ID
428
+ if (!currentChatId) {
429
+ const r = await fetch('/new_chat', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: currentUser})});
430
  const d = await r.json(); currentChatId = d.chat_id;
431
+ loadHistory();
432
  }
433
+
434
+ // API Call
435
  const res = await fetch('/chat', {
436
  method: 'POST', headers: {'Content-Type': 'application/json'},
437
  body: JSON.stringify({message: text, username: currentUser, chat_id: currentChatId})
438
  });
439
  const data = await res.json();
440
+
441
+ // 3. REMOVE SPINNER & START TYPEWRITER EFFECT
442
+ const aiDiv = document.getElementById(msgId);
443
+ aiDiv.innerHTML = ""; // Clear spinner
444
+
445
+ // TYPEWRITER EFFECT LOGIC
446
+ // We receive Markdown. We verify format, then type it out.
447
+ typeWriter(aiDiv, data.response);
448
+
449
+ if(data.new_title) loadHistory();
450
+
451
+ } catch (e) {
452
+ document.getElementById(msgId).innerHTML = "⚠️ Error: " + e.message;
453
  }
 
454
  }
455
+
456
+ // --- TYPEWRITER FUNCTION (Like ChatGPT) ---
457
+ function typeWriter(element, markdownText) {
458
+ // Parse Markdown to HTML first using Marked.js
459
+ const htmlContent = marked.parse(markdownText);
460
+
461
+ // To animate nicely, we can't type HTML tags one by one (it breaks).
462
+ // So we simply Fade it in or use a "Stream" simulation.
463
+ // Since the user asked for "Line by line", we will simulate it safely.
464
+
465
+ element.innerHTML = htmlContent;
466
+
467
+ // Apply a fade-in animation to children to simulate typing flow
468
+ const children = element.children;
469
+ for(let i=0; i<children.length; i++) {
470
+ children[i].style.opacity = '0';
471
+ children[i].style.transform = 'translateY(10px)';
472
+ children[i].style.transition = 'opacity 0.3s ease, transform 0.3s ease';
473
+
474
+ setTimeout(() => {
475
+ children[i].style.opacity = '1';
476
+ children[i].style.transform = 'translateY(0)';
477
+ }, i * 150); // 150ms delay between each line/block
478
+ }
479
+
480
+ // Scroll to bottom as it reveals
481
+ const box = document.getElementById('chat-box');
482
+ let interval = setInterval(() => {
483
+ box.scrollTop = box.scrollHeight;
484
+ if(children[children.length-1].style.opacity === '1') clearInterval(interval);
485
+ }, 100);
486
+ }
487
+
488
+ // --- SIDEBAR & HISTORY ---
489
+ function toggleSidebar() {
490
+ document.getElementById('sidebar').classList.toggle('open');
491
+ document.getElementById('overlay').classList.toggle('active');
492
+ }
493
+
494
  async function loadHistory() {
495
  try {
496
+ const res = await fetch('/get_history', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: currentUser})});
497
  const data = await res.json();
498
+ const list = document.getElementById('history-list');
499
+ list.innerHTML = "";
500
+ if (data.chats) {
501
+ Object.keys(data.chats).reverse().forEach(cid => {
502
+ const title = data.chats[cid].title || "New Chat";
503
+ list.innerHTML += `<div class="history-item" onclick="loadChat('${cid}')" data-title="${title.toLowerCase()}">${title}</div>`;
504
+ });
505
+ }
506
  } catch(e) {}
507
  }
508
+
509
+ function filterHistory(query) {
510
+ const items = document.getElementsByClassName('history-item');
511
+ const q = query.toLowerCase();
512
+ for(let item of items) {
513
+ const title = item.getAttribute('data-title');
514
+ item.style.display = title.includes(q) ? 'block' : 'none';
515
+ }
516
+ }
517
+
518
  async function newChat() {
519
+ toggleSidebar();
520
+ currentChatId = null;
521
+ document.getElementById('chat-box').innerHTML = `<div class="msg ai-msg">New chat started. What's on your mind?</div>`;
522
+ const r = await fetch('/new_chat', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: currentUser})});
523
+ const d = await r.json(); currentChatId = d.chat_id;
524
+ loadHistory();
 
 
525
  }
526
+
527
  async function loadChat(cid) {
528
+ toggleSidebar();
529
  currentChatId = cid;
530
+ const res = await fetch('/get_chat', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: currentUser, chat_id: cid})});
531
+ const data = await res.json();
532
+ const box = document.getElementById('chat-box');
533
+ box.innerHTML = "";
534
+
535
+ // No typing effect for history, just load it
536
  data.messages.forEach(msg => {
537
  const cls = msg.role === 'user' ? 'user-msg' : 'ai-msg';
538
+ let content = msg.content;
539
+ if(msg.role === 'model') content = marked.parse(content);
540
+ box.innerHTML += `<div class="msg ${cls}">${content}</div>`;
541
  });
 
 
542
  box.scrollTop = box.scrollHeight;
543
  }
544
+
 
 
 
 
545
  checkLogin();
546
  </script>
547
  </body>
548
  </html>
549
  """
550
 
551
+ # --- BACKEND ROUTES ---
552
  @app.route("/", methods=["GET"])
553
  def home(): return render_template_string(HTML_TEMPLATE)
554
 
555
  @app.route("/new_chat", methods=["POST"])
556
  def new_chat():
557
+ u = request.json.get("username")
558
+ if u not in user_db: user_db[u] = {}
559
  nid = str(uuid.uuid4())
560
+ user_db[u][nid] = {"title": "New Chat", "messages": []}
561
  save_db(user_db)
562
  return jsonify({"chat_id": nid})
563
 
564
+ @app.route("/get_history", methods=["POST"])
565
+ def get_history():
566
+ u = request.json.get("username")
567
+ return jsonify({"chats": user_db.get(u, {})})
568
+
569
+ @app.route("/get_chat", methods=["POST"])
570
+ def get_chat():
571
+ d = request.json
572
+ return jsonify({"messages": user_db.get(d["username"], {}).get(d["chat_id"], {}).get("messages", [])})
573
+
574
  @app.route("/chat", methods=["POST"])
575
  def chat():
576
  d = request.json
577
  u, cid, msg = d.get("username"), d.get("chat_id"), d.get("message")
578
+
579
  if u not in user_db: user_db[u] = {}
580
  if cid not in user_db[u]: user_db[u][cid] = {"messages": []}
581
 
582
  user_db[u][cid]["messages"].append({"role": "user", "content": msg})
583
 
584
+ # Generate RAW Markdown from AI (Frontend will convert to HTML)
585
  reply = generate_with_retry(msg, user_db[u][cid]["messages"][:-1])
586
 
587
+ # Save Raw Markdown
588
+ user_db[u][cid]["messages"].append({"role": "model", "content": reply})
589
+
590
+ # Update Title if new
591
+ new_title = False
592
+ if len(user_db[u][cid]["messages"]) <= 2:
593
+ user_db[u][cid]["title"] = " ".join(msg.split()[:4])
594
+ new_title = True
595
+
596
  save_db(user_db)
597
+ return jsonify({"response": reply, "new_title": new_title})
598
 
599
  if __name__ == '__main__':
600
+ app.run(host='0.0.0.0', port=7860)
601
+