Shirpi commited on
Commit
144825c
·
verified ·
1 Parent(s): affd9fd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +516 -215
app.py CHANGED
@@ -13,12 +13,12 @@ import google.generativeai as genai
13
  warnings.filterwarnings("ignore")
14
 
15
  # ==========================================
16
- # 👇 API KEYS SETUP 👇
17
  # ==========================================
18
  keys_string = os.environ.get("API_KEYS", "")
19
  API_KEYS = [k.strip() for k in keys_string.replace(',', ' ').replace('\n', ' ').split() if k.strip()]
20
 
21
- # --- 💾 DATABASE ---
22
  DB_FILE = "chat_db.json"
23
  def load_db():
24
  try:
@@ -35,33 +35,42 @@ user_db = load_db()
35
  current_key_index = 0
36
  app = Flask(__name__)
37
 
38
- # --- 🧠 SYSTEM INSTRUCTION ---
39
- BASE_INSTRUCTION = """
40
  ROLE: You are "Student's AI", a professional academic tutor.
41
  RULES:
42
  1. **MATH:** Use LaTeX for formulas ($$ ... $$).
43
  2. **DIAGRAMS:** Use Mermaid.js (```mermaid ... ```).
44
  3. **LANGUAGE:** English by default. Use Tamil/Tanglish ONLY if requested.
45
  4. **FORMAT:** Markdown. Bold key terms.
 
46
  """
47
 
48
- # --- 🧬 MODEL FUNCTIONS ---
49
  def get_working_model(key):
50
  try:
51
  genai.configure(api_key=key)
52
- return 'gemini-1.5-flash'
 
 
 
 
 
 
53
  except: return None
 
54
 
55
  def process_image(image_data):
56
  try:
57
- if "base64," in image_data: image_data = image_data.split("base64,")[1]
 
58
  image_bytes = base64.b64decode(image_data)
59
  return Image.open(io.BytesIO(image_bytes))
60
  except: return None
61
 
62
- def generate_with_retry(prompt, image_data=None, file_text=None, history_messages=[], user_context=""):
63
  global current_key_index
64
- if not API_KEYS: return "🚨 API Keys Missing."
65
 
66
  formatted_history = []
67
  for m in history_messages[-6:]:
@@ -69,141 +78,254 @@ def generate_with_retry(prompt, image_data=None, file_text=None, history_message
69
  formatted_history.append({"role": role, "parts": [m["content"]]})
70
 
71
  current_parts = []
72
- full_prompt = prompt
73
- if user_context:
74
- full_prompt = f"[Context: Student is studying {user_context}]\nQuestion: {prompt}"
75
-
76
  if file_text: current_parts.append(f"analyzing file:\n{file_text}\n\n")
77
- current_parts.append(full_prompt)
78
  if image_data:
79
  img = process_image(image_data)
80
  if img: current_parts.append(img)
81
 
82
  for i in range(len(API_KEYS)):
83
  key = API_KEYS[current_key_index]
 
 
 
 
 
 
84
  try:
85
  genai.configure(api_key=key)
86
- model = genai.GenerativeModel(model_name='gemini-1.5-flash', system_instruction=BASE_INSTRUCTION)
87
- if image_data or file_text: response = model.generate_content(current_parts)
 
 
88
  else:
89
  chat = model.start_chat(history=formatted_history)
90
- response = chat.send_message(full_prompt)
91
  return response.text
92
  except Exception as e:
93
  current_key_index = (current_key_index + 1) % len(API_KEYS)
94
  time.sleep(1)
95
- return "⚠️ System Busy. Please try again."
96
 
97
- # --- UI TEMPLATE ---
 
 
98
  HTML_TEMPLATE = """
99
  <!DOCTYPE html>
100
  <html lang="en">
101
  <head>
102
  <meta charset="UTF-8">
103
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
 
 
 
 
104
  <title>Student's AI</title>
 
105
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
106
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 
107
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
108
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
109
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
110
  <script>window.MathJax = { tex: { inlineMath: [['$', '$']] }, svg: { fontCache: 'global' } };</script>
111
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
 
 
 
 
 
112
 
113
  <style>
114
- :root { --bg: #09090b; --card: #18181b; --user-msg: #27272a; --text: #e4e4e7; --border: #27272a; --dim: #71717a; }
115
- body { margin: 0; background: var(--bg); color: var(--text); font-family: 'Outfit', sans-serif; height: 100dvh; overflow: hidden; }
 
 
 
116
 
117
- /* HEADER */
118
- header { height: 70px; padding: 0 20px; background: rgba(9,9,11, 0.98); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; position: absolute; top: 0; width: 100%; z-index: 50; }
119
- .app-title { font-size: 24px; font-weight: 800; color: #fff; }
120
- .menu-btn { width: 40px; height: 40px; border-radius: 50%; border: 1px solid #333; display: flex; align-items: center; justify-content: center; cursor: pointer; color:#fff; }
121
- .settings-icon { font-size: 18px; color: #aaa; cursor: pointer; }
122
-
123
- /* MAIN CONTAINER */
124
- #app-container { display: flex; flex-direction: column; height: 100dvh; padding-top: 70px; }
125
- #chat-box { flex: 1; overflow-y: auto; padding: 20px 5%; padding-bottom: 80px; display: flex; flex-direction: column; gap: 20px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- /* OVERLAYS (LOGIN & SELECTION) - FORCE VISIBILITY HANDLED BY JS */
128
- .overlay { position: fixed; inset: 0; background: #000; z-index: 2000; display: flex; align-items: flex-start; justify-content: center; padding-top: 150px; transition: opacity 0.3s; }
129
- .overlay.hidden { display: none !important; opacity: 0; pointer-events: none; }
130
 
131
- .login-box { width: 90%; max-width: 350px; text-align: center; padding: 30px; border: 1px solid var(--border); border-radius: 20px; background: #0a0a0a; }
 
 
 
 
 
 
 
 
 
132
 
133
- /* FORM ELEMENTS */
134
- .form-label { display: block; text-align: left; font-size: 12px; color: #aaa; margin-bottom: 5px; margin-top: 15px; }
135
- input, select { width: 100%; padding: 12px; margin-bottom: 5px; background: #18181b; border: 1px solid #333; color: #fff; border-radius: 8px; outline: none; font-size: 16px; font-family: 'Outfit', sans-serif; }
136
- .start-btn { width: 100%; padding: 15px; border-radius: 12px; border: none; background: #fff; font-weight: 800; cursor: pointer; font-size: 16px; margin-top: 25px; }
 
 
 
 
 
137
 
138
- /* MESSAGES */
139
- .msg { display: flex; flex-direction: column; opacity: 0; animation: fadeInstant 0.3s forwards; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  @keyframes fadeInstant { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
 
141
  .user-msg { align-items: flex-end; }
142
- .user-content { background: var(--user-msg); padding: 10px 16px; border-radius: 18px 18px 4px 18px; max-width: 85%; color: #fff; }
 
143
  .ai-msg { align-items: flex-start; }
144
- .ai-content { width: 100%; color: #d4d4d8; }
145
- .ai-content strong { color: #fff; }
146
-
147
- /* INPUT AREA */
148
- .input-wrapper { background: var(--bg); padding: 15px; border-top: 1px solid var(--border); }
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  .input-container { max-width: 900px; margin: 0 auto; background: var(--card); border: 1px solid var(--border); border-radius: 24px; padding: 8px 12px; display: flex; align-items: flex-end; gap: 12px; }
150
  textarea { flex: 1; background: transparent; border: none; color: #fff; font-size: 17px; max-height: 120px; padding: 10px 5px; resize: none; outline: none; font-family: 'Outfit', sans-serif; }
151
- .icon-btn, .send-btn { width: 38px; height: 38px; display: flex; align-items: center; justify-content: center; border-radius: 50%; border: none; cursor: pointer; font-size: 18px; }
152
- .icon-btn { background: transparent; color: #a1a1aa; }
153
- .send-btn { background: #fff; color: #000; }
154
-
155
- /* SIDEBAR */
156
- #sidebar { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--bg); z-index: 100; padding: 25px; padding-top: 80px; transform: translateY(-100%); transition: transform 0.4s; }
157
- #sidebar.open { transform: translateY(0); }
158
-
159
- /* INTRO */
160
- #intro-container { position: absolute; top: 140px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 600px; text-align: center; pointer-events: none; z-index: 10; }
 
 
 
 
 
 
 
 
 
161
 
162
- #preview-area { display:none; position:absolute; bottom:85px; left:20px; }
163
- .preview-box { width:60px; height:60px; background:#222; border:2px solid #fff; border-radius:12px; overflow:hidden; }
164
- .preview-img { width:100%; height:100%; object-fit:cover; }
 
 
 
 
 
 
165
  </style>
166
  </head>
167
  <body>
168
 
169
- <div id="login-overlay" class="overlay">
170
  <div class="login-box">
171
  <h1 class="app-title" style="margin-bottom:10px;">Student's AI</h1>
172
- <input type="text" id="username-input" placeholder="Your Name">
173
- <button class="start-btn" onclick="handleLogin()">Next</button>
174
  </div>
175
  </div>
176
 
177
- <div id="selection-overlay" class="overlay hidden">
178
- <div class="login-box">
179
- <h2 style="color:#fff; margin-top:0;">Customize Profile</h2>
180
-
181
- <span class="form-label">Education Level</span>
182
- <select id="edu-level" onchange="updateEduOptions()">
183
- <option value="school">School (6th - 12th)</option>
184
- <option value="college">College (Arts/Engg)</option>
185
- </select>
186
-
187
- <span class="form-label">Class / Year</span>
188
- <select id="edu-year"></select>
189
-
190
- <span class="form-label">Subject (Optional)</span>
191
- <input type="text" id="edu-subject" placeholder="Ex: Maths, Physics...">
192
-
193
- <button class="start-btn" onclick="handleSelection()">Start Learning</button>
194
- </div>
195
  </div>
196
 
197
  <div id="sidebar">
198
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
199
- <div style="font-size:20px; font-weight:700; color:#fff;">Hi <span id="display-name">User</span></div>
200
  <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-times"></i></div>
201
  </div>
202
- <button class="start-btn" style="margin:0 0 20px 0; padding:12px;" onclick="newChat()">New Chat</button>
203
- <div style="color:#71717a; font-size:13px; font-weight:600; text-transform:uppercase;">Chat History</div>
204
- <div id="history-list" style="margin-top:10px;"></div>
205
- <div style="position:absolute; bottom:30px; width:100%; left:0; text-align:center;">
206
- <div style="color:#ef4444; cursor:pointer; font-weight:600;" onclick="handleLogout()">Log Out</div>
 
207
  </div>
208
  </div>
209
 
@@ -211,20 +333,26 @@ HTML_TEMPLATE = """
211
  <header>
212
  <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-bars"></i></div>
213
  <span class="app-title">Student's AI</span>
214
- <div class="settings-icon" onclick="openSettings()"><i class="fas fa-cog"></i></div>
215
  </header>
216
 
217
  <div id="chat-box"></div>
218
 
219
  <div class="input-wrapper">
220
- <div id="preview-area">
221
- <div class="preview-box"><img id="preview-img" class="preview-img"></div>
222
- <button onclick="clearAttachment()" style="background:red; color:white; border:none; border-radius:50%; width:20px; height:20px; position:absolute; top:-5px; right:-5px;">×</button>
223
  </div>
 
224
  <div class="input-container">
225
  <button class="icon-btn" onclick="document.getElementById('file-input').click()"><i class="fas fa-paperclip"></i></button>
226
- <input type="file" id="file-input" hidden onchange="handleFile(this)">
227
- <textarea id="input" placeholder="Type a message..." rows="1" oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"></textarea>
 
 
 
 
 
228
  <button class="send-btn" onclick="send()"><i class="fas fa-arrow-up"></i></button>
229
  </div>
230
  </div>
@@ -233,178 +361,306 @@ HTML_TEMPLATE = """
233
  <script>
234
  let currentUser = null;
235
  let currentChatId = null;
236
- let userContext = "";
237
- let currentAttachment = null;
238
-
 
239
  function getIntroHtml(name) {
240
- return `<div id="intro-container"><div class="msg ai-msg"><div class="ai-content"><h1>Hi ${name},</h1><p>Ready to master ${userContext ? userContext.split(',')[0] : "studies"}?</p></div></div></div>`;
241
  }
242
 
243
- // --- AUTH LOGIC (ROBUST FIX) ---
244
  function checkLogin() {
245
- const u = localStorage.getItem("student_ai_user");
246
- const c = localStorage.getItem("student_ai_context");
247
- if(u) {
248
- currentUser = u;
249
- document.getElementById('login-overlay').style.display = 'none';
250
- if(c) {
251
- userContext = c;
252
- document.getElementById('selection-overlay').style.display = 'none';
253
- showApp();
254
- } else {
255
- document.getElementById('selection-overlay').classList.remove('hidden');
256
- updateEduOptions();
257
  }
258
- }
259
  }
260
-
261
  function handleLogin() {
262
- const name = document.getElementById("username-input").value.trim();
263
- if(!name) { alert("Please enter a name"); return; }
264
-
265
- localStorage.setItem("student_ai_user", name);
266
- currentUser = name;
267
-
268
- // FORCE DISPLAY CHANGE
269
- document.getElementById("login-overlay").style.display = 'none';
270
- const sel = document.getElementById("selection-overlay");
271
- sel.style.display = 'flex';
272
- sel.classList.remove('hidden');
273
- sel.style.opacity = '1';
274
-
275
- updateEduOptions();
276
- }
277
-
278
- function updateEduOptions() {
279
- const level = document.getElementById('edu-level').value;
280
- const yearSelect = document.getElementById('edu-year');
281
- yearSelect.innerHTML = "";
282
- let opts = level === 'school' ? ["6th", "7th", "8th", "9th", "10th", "11th", "12th"] : ["1st Year", "2nd Year", "3rd Year", "4th Year"];
283
- opts.forEach(o => {
284
- let op = document.createElement('option');
285
- op.value = o; op.innerText = o;
286
- yearSelect.appendChild(op);
287
- });
288
  }
289
-
290
- function handleSelection() {
291
- const l = document.getElementById('edu-level').value;
292
- const y = document.getElementById('edu-year').value;
293
- const s = document.getElementById('edu-subject').value;
294
- userContext = `${l}, ${y}, ${s}`;
295
- localStorage.setItem("student_ai_context", userContext);
296
- document.getElementById('selection-overlay').style.display = 'none';
297
- showApp();
 
 
298
  }
299
-
300
  function showApp() {
301
- document.getElementById('display-name').innerText = currentUser;
302
  loadHistory();
303
- if(!currentChatId && !document.getElementById('intro-container')) {
304
- document.getElementById('chat-box').innerHTML = getIntroHtml(currentUser);
 
 
 
305
  }
306
  }
307
 
308
- function handleLogout() {
309
- localStorage.clear();
310
- location.reload();
311
- }
312
-
313
- function openSettings() {
314
- const sel = document.getElementById("selection-overlay");
315
- sel.style.display = 'flex';
316
- sel.classList.remove('hidden');
317
- sel.style.opacity = '1';
318
  }
319
 
320
- function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); }
321
-
322
- function handleFile(input) {
323
- if(input.files[0]) {
324
  const reader = new FileReader();
325
- reader.onload = (e) => {
326
- currentAttachment = e.target.result;
327
- document.getElementById('preview-area').style.display = 'block';
328
- document.getElementById('preview-img').src = currentAttachment;
329
- };
330
- reader.readAsDataURL(input.files[0]);
 
 
 
 
331
  }
 
332
  }
 
333
  function clearAttachment() {
334
- currentAttachment = null;
335
  document.getElementById('preview-area').style.display = 'none';
336
- document.getElementById('file-input').value = "";
 
 
 
 
 
 
337
  }
338
 
339
  async function send() {
340
- const txt = document.getElementById('input').value.trim();
341
- if(!txt && !currentAttachment) return;
 
342
 
 
343
  const intro = document.getElementById('intro-container');
344
- if(intro) intro.remove();
345
-
346
- const box = document.getElementById('chat-box');
347
- let imgHtml = currentAttachment ? `<br><img src="${currentAttachment}" style="max-height:100px;border-radius:8px;">` : "";
348
- box.insertAdjacentHTML('beforeend', `<div class="msg user-msg"><div class="user-content">${txt}${imgHtml}</div></div>`);
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- document.getElementById('input').value = "";
351
- let imgData = currentAttachment;
 
 
 
352
  clearAttachment();
353
- box.scrollTo(0, box.scrollHeight);
354
 
355
  const msgId = "ai-" + Date.now();
356
- box.insertAdjacentHTML('beforeend', `<div id="${msgId}" class="msg ai-msg">...</div>`);
357
-
 
358
  try {
359
- if(!currentChatId) {
360
  const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
361
  const d = await r.json(); currentChatId = d.chat_id;
 
362
  }
363
  const res = await fetch('/chat', {
364
- method:'POST', headers:{'Content-Type':'application/json'},
365
- body:JSON.stringify({message:txt, image:imgData, username:currentUser, chat_id:currentChatId, user_context:userContext})
366
  });
367
  const data = await res.json();
368
- document.getElementById(msgId).innerHTML = `<div class="ai-content">${marked.parse(data.response)}</div>`;
369
- hljs.highlightAll();
370
- box.scrollTo(0, box.scrollHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  if(data.new_title) loadHistory();
372
- } catch(e) { document.getElementById(msgId).innerText = "Error."; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  }
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  async function loadHistory() {
376
- try {
377
  const res = await fetch('/get_history', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
378
  const data = await res.json();
379
  const list = document.getElementById('history-list'); list.innerHTML = "";
380
- Object.keys(data.chats).reverse().forEach(cid => {
381
- list.innerHTML += `<div style="padding:10px; margin-bottom:5px; background:#18181b; border-radius:8px; cursor:pointer;" onclick="loadChat('${cid}')">${data.chats[cid].title || "Chat"}</div>`;
382
- });
383
- } catch(e){}
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  async function loadChat(cid) {
387
- currentChatId = cid;
388
- const res = await fetch('/get_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})});
389
- const data = await res.json();
390
- const box = document.getElementById('chat-box'); box.innerHTML = "";
391
- data.messages.forEach(m => {
392
- let cls = m.role === 'user' ? 'user' : 'ai';
393
- let content = m.role === 'user' ? m.content : marked.parse(m.content);
394
- box.insertAdjacentHTML('beforeend', `<div class="msg ${cls}-msg"><div class="${cls}-content">${content}</div></div>`);
 
 
 
 
 
 
395
  });
396
  document.getElementById('sidebar').classList.remove('open');
397
- hljs.highlightAll();
398
  }
399
 
400
- // Init
401
  checkLogin();
402
  </script>
403
  </body>
404
  </html>
405
  """
406
-
407
- # --- ROUTES ---
408
  @app.route("/", methods=["GET"])
409
  def home(): return render_template_string(HTML_TEMPLATE)
410
 
@@ -417,6 +673,24 @@ def new_chat():
417
  save_db(user_db)
418
  return jsonify({"chat_id": nid})
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  @app.route("/get_history", methods=["POST"])
421
  def get_history():
422
  u = request.json.get("username")
@@ -430,22 +704,49 @@ def get_chat():
430
  @app.route("/chat", methods=["POST"])
431
  def chat():
432
  d = request.json
433
- u, cid, msg, ctx = d.get("username"), d.get("chat_id"), d.get("message"), d.get("user_context", "")
434
- img = d.get("image")
435
-
 
436
  if u not in user_db: user_db[u] = {}
437
  if cid not in user_db[u]: user_db[u][cid] = {"messages": []}
438
-
439
  user_db[u][cid]["messages"].append({"role": "user", "content": msg})
440
- reply = generate_with_retry(msg, img, None, user_db[u][cid]["messages"][:-1], ctx)
441
  user_db[u][cid]["messages"].append({"role": "model", "content": reply})
442
 
443
  new_title = False
444
  if len(user_db[u][cid]["messages"]) <= 2:
445
  user_db[u][cid]["title"] = " ".join(msg.split()[:4])
446
  new_title = True
 
447
  save_db(user_db)
448
  return jsonify({"response": reply, "new_title": new_title})
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  if __name__ == '__main__':
451
  app.run(host='0.0.0.0', port=7860)
 
13
  warnings.filterwarnings("ignore")
14
 
15
  # ==========================================
16
+ # 👇 API KEYS SETUP 👇
17
  # ==========================================
18
  keys_string = os.environ.get("API_KEYS", "")
19
  API_KEYS = [k.strip() for k in keys_string.replace(',', ' ').replace('\n', ' ').split() if k.strip()]
20
 
21
+ # --- 💾 DATABASE ---
22
  DB_FILE = "chat_db.json"
23
  def load_db():
24
  try:
 
35
  current_key_index = 0
36
  app = Flask(__name__)
37
 
38
+ # --- 🧠SYSTEM INSTRUCTION ---
39
+ SYSTEM_INSTRUCTION = """
40
  ROLE: You are "Student's AI", a professional academic tutor.
41
  RULES:
42
  1. **MATH:** Use LaTeX for formulas ($$ ... $$).
43
  2. **DIAGRAMS:** Use Mermaid.js (```mermaid ... ```).
44
  3. **LANGUAGE:** English by default. Use Tamil/Tanglish ONLY if requested.
45
  4. **FORMAT:** Markdown. Bold key terms.
46
+ 5. **CODE:** Use Python/Java/C++ blocks. Explain logic briefly.
47
  """
48
 
49
+ # --- 🧬 MODEL & FILE HANDLING ---
50
  def get_working_model(key):
51
  try:
52
  genai.configure(api_key=key)
53
+ models = list(genai.list_models())
54
+ chat_models = [m for m in models if 'generateContent' in m.supported_generation_methods]
55
+ for m in chat_models:
56
+ if "flash" in m.name.lower() and "1.5" in m.name: return m.name
57
+ for m in chat_models:
58
+ if "pro" in m.name.lower() and "1.5" in m.name: return m.name
59
+ if chat_models: return chat_models[0].name
60
  except: return None
61
+ return None
62
 
63
  def process_image(image_data):
64
  try:
65
+ if "base64," in image_data:
66
+ image_data = image_data.split("base64,")[1]
67
  image_bytes = base64.b64decode(image_data)
68
  return Image.open(io.BytesIO(image_bytes))
69
  except: return None
70
 
71
+ def generate_with_retry(prompt, image_data=None, file_text=None, history_messages=[]):
72
  global current_key_index
73
+ if not API_KEYS: return "🚨 API Keys Missing."
74
 
75
  formatted_history = []
76
  for m in history_messages[-6:]:
 
78
  formatted_history.append({"role": role, "parts": [m["content"]]})
79
 
80
  current_parts = []
 
 
 
 
81
  if file_text: current_parts.append(f"analyzing file:\n{file_text}\n\n")
82
+ current_parts.append(prompt)
83
  if image_data:
84
  img = process_image(image_data)
85
  if img: current_parts.append(img)
86
 
87
  for i in range(len(API_KEYS)):
88
  key = API_KEYS[current_key_index]
89
+ model_name = get_working_model(key)
90
+
91
+ if not model_name:
92
+ current_key_index = (current_key_index + 1) % len(API_KEYS)
93
+ continue
94
+
95
  try:
96
  genai.configure(api_key=key)
97
+ model = genai.GenerativeModel(model_name=model_name, system_instruction=SYSTEM_INSTRUCTION)
98
+
99
+ if image_data or file_text:
100
+ response = model.generate_content(current_parts)
101
  else:
102
  chat = model.start_chat(history=formatted_history)
103
+ response = chat.send_message(prompt)
104
  return response.text
105
  except Exception as e:
106
  current_key_index = (current_key_index + 1) % len(API_KEYS)
107
  time.sleep(1)
 
108
 
109
+ return "⚠️ System Busy. Please try again."
110
+
111
+ # --- UI TEMPLATE (Fixed Quotes & Logic) ---
112
  HTML_TEMPLATE = """
113
  <!DOCTYPE html>
114
  <html lang="en">
115
  <head>
116
  <meta charset="UTF-8">
117
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content">
118
+ <meta name="mobile-web-app-capable" content="yes">
119
+ <meta name="apple-mobile-web-app-capable" content="yes">
120
+ <meta name="theme-color" content="#09090b">
121
+ <link rel="manifest" href="/manifest.json">
122
  <title>Student's AI</title>
123
+
124
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
125
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
126
+
127
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
128
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
129
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
130
  <script>window.MathJax = { tex: { inlineMath: [['$', '$']] }, svg: { fontCache: 'global' } };</script>
131
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
132
+ <script type="module">
133
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
134
+ mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' });
135
+ window.mermaid = mermaid;
136
+ </script>
137
 
138
  <style>
139
+ :root {
140
+ --bg: #09090b; --card: #18181b; --user-msg: #27272a; --text: #e4e4e7;
141
+ --accent: #fff; --border: #27272a; --dim: #71717a;
142
+ }
143
+ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
144
 
145
+ body, html {
146
+ margin: 0; padding: 0; height: 100dvh; width: 100%; max-width: 100%;
147
+ background: var(--bg); color: var(--text); font-family: 'Outfit', sans-serif;
148
+ overflow: hidden; font-size: 17px;
149
+ -webkit-user-select: none; user-select: none;
150
+ }
151
+
152
+ textarea, input { -webkit-user-select: text !important; user-select: text !important; }
153
+ .user-content, .ai-content, code, pre { -webkit-user-select: none !important; user-select: none !important; }
154
+
155
+ /* --- APP CONTAINER (Pushed down for Header) --- */
156
+ #app-container {
157
+ display: flex; flex-direction: column;
158
+ height: 100dvh; width: 100%;
159
+ position: relative; overflow-x: hidden;
160
+ padding-top: 70px; /* Space for fixed header */
161
+ }
162
+
163
+ /* --- HEADER LOCKED --- */
164
+ header {
165
+ height: 70px; padding: 0 20px; background: rgba(9,9,11, 0.98);
166
+ border-bottom: 1px solid var(--border-color);
167
+ display: flex; align-items: center; justify-content: space-between;
168
+ z-index: 50;
169
+ padding-top: env(safe-area-inset-top);
170
+ position: absolute; top: 0; left: 0; right: 0;
171
+ }
172
+ .menu-btn {
173
+ width: 40px; height: 40px; border-radius: 50%; border: 1px solid #333;
174
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
175
+ transition: 0.2s; color: #fff;
176
+ }
177
+ .menu-btn:active { transform: scale(0.95); background: #222; }
178
+ .app-title { font-size: 24px; font-weight: 800; letter-spacing: -0.5px; color: #fff; }
179
+
180
+ /* --- SIDEBAR ANIMATION --- */
181
+ #sidebar {
182
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
183
+ background: var(--bg); z-index: 100;
184
+ display: flex; flex-direction: column; padding: 25px;
185
+ padding-top: calc(70px + env(safe-area-inset-top));
186
+ transform: translateY(-100%);
187
+ transition: transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
188
+ overflow-y: auto;
189
+ }
190
+ #sidebar.open { transform: translateY(0); }
191
+
192
+ @media (min-width: 768px) {
193
+ #sidebar { width: 350px; border-right: 1px solid var(--border); }
194
+ .input-container { max-width: 800px; }
195
+ #chat-box { padding: 20px 15%; }
196
+ }
197
+
198
+ .user-info { margin-bottom: 30px; font-size: 20px; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 15px; flex-shrink: 0; }
199
+ .new-chat-btn {
200
+ width: 100%; padding: 15px; background: #fff; color: #000; border: none;
201
+ border-radius: 12px; font-weight: 700; font-size: 16px; cursor: pointer; margin-bottom: 25px; flex-shrink: 0;
202
+ }
203
 
204
+ .history-label { color: var(--dim); font-size: 13px; font-weight: 600; margin-bottom: 10px; letter-spacing: 1px; text-transform: uppercase; flex-shrink: 0; }
205
+ #history-list { flex: 1; overflow-y: auto; padding: 10px 0; min-height: 100px; }
 
206
 
207
+ .history-item {
208
+ display: flex; justify-content: space-between; align-items: center;
209
+ padding: 15px; margin-bottom: 12px; background: var(--card); border: 1px solid var(--border); border-radius: 12px;
210
+ cursor: pointer; color: #a1a1aa; font-size: 15px; transition: 0.2s;
211
+ }
212
+ .history-item:active { background: #222; color: #fff; border-color: #444; }
213
+ .h-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; flex: 1; margin-right: 10px; }
214
+ .h-actions { display: none; gap: 15px; }
215
+ .history-item.active-mode .h-actions { display: flex; }
216
+ .h-icon { font-size: 16px; color: #fff; padding: 5px; }
217
 
218
+ .rename-input {
219
+ background: transparent; border: none; border-bottom: 1px solid #fff;
220
+ color: #fff; font-family: 'Outfit', sans-serif; font-size: 15px;
221
+ width: 100%; outline: none; padding: 0;
222
+ }
223
+
224
+ .brand-section { text-align: center; margin-top: 20px; padding-bottom: env(safe-area-inset-bottom); flex-shrink: 0; }
225
+ .brand-name { font-family: 'Outfit', sans-serif; font-weight: 600; font-size: 12px; color: var(--dim); letter-spacing: 2px; margin-bottom: 10px; opacity: 0.6; }
226
+ .logout-btn { color: #ef4444; cursor: pointer; font-size: 15px; font-weight: 600; padding: 10px; }
227
 
228
+ #chat-box {
229
+ flex: 1; overflow-y: auto; padding: 20px 5%; padding-bottom: 80px;
230
+ display: flex; flex-direction: column; gap: 25px;
231
+ -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain; min-height: 0;
232
+ }
233
+
234
+ /* --- LOCKED INTRO TEXT --- */
235
+ #intro-container {
236
+ position: absolute;
237
+ top: 140px; /* Fixed from top so it won't move up with keyboard */
238
+ left: 50%;
239
+ transform: translateX(-50%);
240
+ width: 90%; max-width: 600px;
241
+ text-align: center;
242
+ z-index: 10;
243
+ pointer-events: none;
244
+ }
245
+
246
+ .msg { width: 100%; line-height: 1.7; font-size: 17px; opacity: 0; animation: fadeInstant 0.3s forwards; display: flex; flex-direction: column; }
247
  @keyframes fadeInstant { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
248
+
249
  .user-msg { align-items: flex-end; }
250
+ .user-content { display: inline-block; width: fit-content; max-width: 85%; background: var(--user-msg); padding: 10px 16px; border-radius: 18px 18px 4px 18px; text-align: left; color: #fff; word-wrap: break-word; }
251
+
252
  .ai-msg { align-items: flex-start; }
253
+ .ai-content { width: 100%; color: #d4d4d8; word-wrap: break-word; }
254
+ .ai-content strong { color: #fff; font-weight: 700; }
255
+ .ai-content h1, .ai-content h2 { margin-top: 20px; color: #fff; font-weight: 700; }
256
+ .ai-content img, .user-content img { cursor: pointer; transition: 0.2s; }
257
+ .ai-content img:active, .user-content img:active { transform: scale(0.98); }
258
+
259
+ pre { background: #1e1e1e !important; border-radius: 12px; padding: 15px; overflow-x: auto; margin: 15px 0; border: 1px solid #333; max-width: 100%; }
260
+ code { font-family: 'JetBrains Mono', monospace; font-size: 14px; }
261
+ .mjx-chtml { background: #18181b; padding: 10px; border-radius: 8px; border: 1px solid #333; overflow-x: auto; margin: 10px 0; text-align: center; max-width: 100%; }
262
+ .mermaid { background: #111; padding: 15px; border-radius: 10px; text-align: center; margin: 15px 0; overflow-x: auto; }
263
+
264
+ .msg-actions { margin-top: 10px; opacity: 0; transition: opacity 0.2s; display: flex; gap: 20px; align-items: center; }
265
+ .user-msg .msg-actions { justify-content: flex-end; }
266
+ .msg:hover .msg-actions { opacity: 1; }
267
+ .action-icon { cursor: pointer; color: var(--dim); font-size: 18px; transition: 0.2s; }
268
+ .action-icon:hover { color: #fff; transform: scale(1.1); }
269
+
270
+ .input-wrapper { background: var(--bg); padding: 15px; border-top: 1px solid var(--border); flex-shrink: 0; z-index: 60; padding-bottom: max(15px, env(safe-area-inset-bottom)); }
271
  .input-container { max-width: 900px; margin: 0 auto; background: var(--card); border: 1px solid var(--border); border-radius: 24px; padding: 8px 12px; display: flex; align-items: flex-end; gap: 12px; }
272
  textarea { flex: 1; background: transparent; border: none; color: #fff; font-size: 17px; max-height: 120px; padding: 10px 5px; resize: none; outline: none; font-family: 'Outfit', sans-serif; }
273
+ .icon-btn { width: 38px; height: 38px; display: flex; align-items: center; justify-content: center; border-radius: 50%; border: none; background: transparent; color: #a1a1aa; cursor: pointer; font-size: 18px; }
274
+ .send-btn { background: #fff; color: #000; width: 38px; height: 38px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 18px; }
275
+
276
+ #preview-area { position: absolute; bottom: 85px; left: 20px; display: none; z-index: 70; }
277
+ .preview-box { width: 60px; height: 60px; border-radius: 12px; border: 2px solid #fff; background: #222; overflow: hidden; position: relative; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }
278
+ .preview-img { width: 100%; height: 100%; object-fit: cover; }
279
+ .remove-preview { position: absolute; top: -8px; right: -8px; background: red; color: white; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; cursor: pointer; border: none; display: flex; align-items: center; justify-content: center; }
280
+
281
+ /* --- LOGIN PAGE LOCKED (FIXED) --- */
282
+ #login-overlay {
283
+ position: fixed; inset: 0; background: #000; z-index: 2000;
284
+ display: flex;
285
+ /* Align to START (Top) to prevent centering jump */
286
+ align-items: flex-start; justify-content: center;
287
+ /* Force padding from top so it never moves */
288
+ padding-top: 180px;
289
+ transition: opacity 0.8s ease; opacity: 1; pointer-events: auto;
290
+ }
291
+ #login-overlay.hidden { opacity: 0; pointer-events: none; }
292
 
293
+ .login-box {
294
+ width: 90%; max-width: 350px; text-align: center;
295
+ padding: 40px; border: 1px solid var(--border); border-radius: 20px; background: #0a0a0a;
296
+ /* No auto margins */
297
+ }
298
+
299
+ #image-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 3000; display: none; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s; }
300
+ #image-modal img { max-width: 95%; max-height: 90%; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.8); }
301
+ #image-modal.active { opacity: 1; }
302
  </style>
303
  </head>
304
  <body>
305
 
306
+ <div id="login-overlay">
307
  <div class="login-box">
308
  <h1 class="app-title" style="margin-bottom:10px;">Student's AI</h1>
309
+ <input type="text" id="username-input" placeholder="Your Name" style="width:100%; padding:15px; border-radius:12px; border:1px solid #333; background:#111; color:#fff; text-align:center; outline:none; margin-bottom:20px; font-size: 16px;" onkeydown="if(event.key==='Enter') handleLogin()">
310
+ <button onclick="handleLogin()" style="width:100%; padding:15px; border-radius:12px; border:none; background:#fff; font-weight:800; cursor:pointer; font-size: 16px;">Start Learning</button>
311
  </div>
312
  </div>
313
 
314
+ <div id="image-modal" onclick="closeImagePreview()">
315
+ <img id="modal-img" src="" alt="Preview">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  </div>
317
 
318
  <div id="sidebar">
319
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; flex-shrink:0;">
320
+ <div class="user-info"><span id="display-name">User</span></div>
321
  <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-times"></i></div>
322
  </div>
323
+ <button class="new-chat-btn" onclick="newChat()">New Chat</button>
324
+ <div class="history-label">Chat History</div>
325
+ <div id="history-list"></div>
326
+ <div class="brand-section">
327
+ <div class="brand-name">Designed by Shirpi</div>
328
+ <div class="logout-btn" onclick="handleLogout()">Log Out</div>
329
  </div>
330
  </div>
331
 
 
333
  <header>
334
  <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-bars"></i></div>
335
  <span class="app-title">Student's AI</span>
336
+ <div style="width:40px;"></div>
337
  </header>
338
 
339
  <div id="chat-box"></div>
340
 
341
  <div class="input-wrapper">
342
+ <div id="preview-area">
343
+ <div class="preview-box"><div id="preview-visual"></div></div>
344
+ <button class="remove-preview" onclick="clearAttachment()">×</button>
345
  </div>
346
+
347
  <div class="input-container">
348
  <button class="icon-btn" onclick="document.getElementById('file-input').click()"><i class="fas fa-paperclip"></i></button>
349
+ <input type="file" id="file-input" accept="image/*,.txt,.py,.js,.html,.css,.md,.csv,.json" hidden onchange="handleFileSelect(this)">
350
+
351
+ <button class="icon-btn" onclick="document.getElementById('camera-input').click()"><i class="fas fa-camera"></i></button>
352
+ <input type="file" id="camera-input" accept="image/*" capture="environment" hidden onchange="handleFileSelect(this)">
353
+
354
+ <textarea id="input" placeholder="Type a message..." rows="1" oninput="resizeInput(this)"></textarea>
355
+
356
  <button class="send-btn" onclick="send()"><i class="fas fa-arrow-up"></i></button>
357
  </div>
358
  </div>
 
361
  <script>
362
  let currentUser = null;
363
  let currentChatId = null;
364
+ let currentAttachment = { type: null, data: null, name: null };
365
+ let longPressTimer;
366
+
367
+ // --- FIXED INTRO: LOCKED CONTAINER ---
368
  function getIntroHtml(name) {
369
+ return `<div id="intro-container"><div class="msg ai-msg"><div class="ai-content"><h1>Hi ${name},</h1><p>Ready to master your studies today?</p></div></div></div>`;
370
  }
371
 
372
+ // --- AUTH LOGIC (SMOOTH TRANSITION) ---
373
  function checkLogin() {
374
+ try {
375
+ const stored = localStorage.getItem("student_ai_user");
376
+ if (stored) {
377
+ currentUser = stored;
378
+ document.getElementById("login-overlay").classList.add('hidden');
379
+ showApp();
 
 
 
 
 
 
380
  }
381
+ } catch(e) { console.log("Storage access denied"); }
382
  }
383
+
384
  function handleLogin() {
385
+ const input = document.getElementById("username-input");
386
+ const name = input.value.trim();
387
+ if(name) {
388
+ try { localStorage.setItem("student_ai_user", name); } catch(e){}
389
+ currentUser = name;
390
+ // FORCE HIDE DIRECTLY
391
+ const overlay = document.getElementById("login-overlay");
392
+ overlay.classList.add('hidden');
393
+ setTimeout(() => overlay.style.display = 'none', 500);
394
+ showApp();
395
+ } else {
396
+ input.style.border = "1px solid red";
397
+ setTimeout(() => input.style.border = "1px solid #333", 2000);
398
+ }
 
 
 
 
 
 
 
 
 
 
 
 
399
  }
400
+ function handleLogout() {
401
+ try { localStorage.removeItem("student_ai_user"); } catch(e){}
402
+ const overlay = document.getElementById("login-overlay");
403
+ overlay.style.display = 'flex';
404
+ setTimeout(() => overlay.classList.remove('hidden'), 10);
405
+ document.getElementById('sidebar').classList.remove('open');
406
+ setTimeout(() => {
407
+ document.getElementById('chat-box').innerHTML = "";
408
+ currentChatId = null;
409
+ document.getElementById("username-input").value = "";
410
+ }, 500);
411
  }
412
+
413
  function showApp() {
414
+ document.getElementById("display-name").innerText = "Hi " + currentUser;
415
  loadHistory();
416
+ if(!currentChatId) {
417
+ const box = document.getElementById("chat-box");
418
+ if(box.innerHTML === "") {
419
+ box.innerHTML = getIntroHtml(currentUser);
420
+ }
421
  }
422
  }
423
 
424
+ function resizeInput(el) {
425
+ el.style.height = 'auto';
426
+ el.style.height = Math.min(el.scrollHeight, 150) + 'px';
 
 
 
 
 
 
 
427
  }
428
 
429
+ function handleFileSelect(input) {
430
+ if (input.files && input.files[0]) {
431
+ const file = input.files[0];
 
432
  const reader = new FileReader();
433
+ reader.onload = function(e) {
434
+ const result = e.target.result;
435
+ const isImage = file.type.startsWith('image/');
436
+ currentAttachment = { type: isImage ? 'image' : 'file', data: isImage ? result : atob(result.split(',')[1]), name: file.name };
437
+ const previewArea = document.getElementById('preview-area');
438
+ const visual = document.getElementById('preview-visual');
439
+ previewArea.style.display = 'block';
440
+ visual.innerHTML = isImage ? `<img src="${result}" class="preview-img">` : `<div class="preview-file-icon"><i class="fas fa-file-alt"></i></div>`;
441
+ }
442
+ reader.readAsDataURL(file);
443
  }
444
+ input.value = "";
445
  }
446
+
447
  function clearAttachment() {
448
+ currentAttachment = { type: null, data: null, name: null };
449
  document.getElementById('preview-area').style.display = 'none';
450
+ }
451
+
452
+ function scrollToBottom() {
453
+ const box = document.getElementById('chat-box');
454
+ setTimeout(() => {
455
+ box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
456
+ }, 100);
457
  }
458
 
459
  async function send() {
460
+ const input = document.getElementById('input');
461
+ const text = input.value.trim();
462
+ if (!text && !currentAttachment.data) return;
463
 
464
+ // --- REMOVE INTRO TEXT AUTOMATICALLY ---
465
  const intro = document.getElementById('intro-container');
466
+ if(intro) { intro.style.display = 'none'; intro.remove(); }
467
+
468
+ const chatBox = document.getElementById('chat-box');
469
+ let attachHtml = '';
470
+ if (currentAttachment.type === 'image') attachHtml = `<br><img src="${currentAttachment.data}" style="max-height:100px; margin-top:10px; border-radius:8px;">`;
471
+ if (currentAttachment.type === 'file') attachHtml = `<br><small>📄 ${currentAttachment.name}</small>`;
472
+
473
+ const userHtml = `
474
+ <div class="msg user-msg">
475
+ <div class="user-content">${text.replace(/</g, "&lt;")}${attachHtml}</div>
476
+ <div class="msg-actions">
477
+ <i class="fas fa-copy action-icon" onclick="copyText('${text}')"></i>
478
+ <i class="fas fa-pen action-icon" onclick="editMessage('${text}')"></i>
479
+ </div>
480
+ </div>`;
481
+ chatBox.insertAdjacentHTML('beforeend', userHtml);
482
 
483
+ const promptText = text;
484
+ const imgData = currentAttachment.type === 'image' ? currentAttachment.data : null;
485
+ const fileText = currentAttachment.type === 'file' ? currentAttachment.data : null;
486
+
487
+ input.value = ''; input.style.height = 'auto';
488
  clearAttachment();
489
+ scrollToBottom();
490
 
491
  const msgId = "ai-" + Date.now();
492
+ chatBox.insertAdjacentHTML('beforeend', `<div id="${msgId}" class="msg ai-msg"><i class="fas fa-circle-notch fa-spin"></i></div>`);
493
+ scrollToBottom();
494
+
495
  try {
496
+ if (!currentChatId) {
497
  const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
498
  const d = await r.json(); currentChatId = d.chat_id;
499
+ loadHistory();
500
  }
501
  const res = await fetch('/chat', {
502
+ method: 'POST', headers: {'Content-Type': 'application/json'},
503
+ body: JSON.stringify({ message: promptText, image: imgData, file_text: fileText, username: currentUser, chat_id: currentChatId })
504
  });
505
  const data = await res.json();
506
+
507
+ const aiDiv = document.getElementById(msgId);
508
+ aiDiv.innerHTML = "";
509
+ const contentDiv = document.createElement('div');
510
+ contentDiv.className = 'ai-content';
511
+ aiDiv.appendChild(contentDiv);
512
+
513
+ await typeWriter(contentDiv, data.response);
514
+
515
+ aiDiv.insertAdjacentHTML('beforeend', `
516
+ <div class="msg-actions">
517
+ <i class="fas fa-copy action-icon" onclick="copyAiResponse(this)"></i>
518
+ <i class="fas fa-share-alt action-icon" onclick="shareResponse(this)"></i>
519
+ <i class="fas fa-redo action-icon" onclick="regenerate('${promptText}')"></i>
520
+ </div>`);
521
+
522
+ scrollToBottom();
523
  if(data.new_title) loadHistory();
524
+ } catch (e) { document.getElementById(msgId).innerHTML = "⚠️ Error: " + e.message; }
525
+ }
526
+
527
+ function copyText(text) { navigator.clipboard.writeText(text); }
528
+ function copyAiResponse(btn) {
529
+ const text = btn.closest('.ai-msg').querySelector('.ai-content').innerText;
530
+ navigator.clipboard.writeText(text);
531
+ }
532
+ function shareResponse(btn) {
533
+ const text = btn.closest('.ai-msg').querySelector('.ai-content').innerText;
534
+ if (navigator.share) navigator.share({ title: 'Student AI', text: text });
535
+ else navigator.clipboard.writeText(text);
536
+ }
537
+ function editMessage(oldText) { document.getElementById('input').value = oldText; document.getElementById('input').focus(); }
538
+ function regenerate(text) { document.getElementById('input').value = text; send(); }
539
+
540
+ document.getElementById('chat-box').addEventListener('click', function(e) {
541
+ if(e.target.tagName === 'IMG') {
542
+ const modal = document.getElementById('image-modal');
543
+ const modalImg = document.getElementById('modal-img');
544
+ modalImg.src = e.target.src;
545
+ modal.style.display = 'flex';
546
+ setTimeout(() => modal.classList.add('active'), 10);
547
+ }
548
+ });
549
+ function closeImagePreview() {
550
+ const modal = document.getElementById('image-modal');
551
+ modal.classList.remove('active');
552
+ setTimeout(() => modal.style.display = 'none', 300);
553
  }
554
 
555
+ function handleHistoryTouchStart(e, cid) {
556
+ longPressTimer = setTimeout(() => {
557
+ e.target.closest('.history-item').classList.add('active-mode');
558
+ }, 600);
559
+ }
560
+ function handleHistoryTouchEnd(e) { clearTimeout(longPressTimer); }
561
+
562
+ function startRename(cid) {
563
+ const item = document.getElementById('chat-' + cid);
564
+ const titleSpan = item.querySelector('.h-title');
565
+ const currentTitle = titleSpan.innerText;
566
+ const input = document.createElement('input');
567
+ input.type = 'text'; input.value = currentTitle; input.className = 'rename-input';
568
+
569
+ async function save() {
570
+ const newTitle = input.value.trim();
571
+ if(newTitle && newTitle !== currentTitle) {
572
+ await fetch('/rename_chat', {
573
+ method:'POST', headers:{'Content-Type':'application/json'},
574
+ body:JSON.stringify({username:currentUser, chat_id:cid, title:newTitle})
575
+ });
576
+ loadHistory();
577
+ } else { loadHistory(); }
578
+ }
579
+ input.addEventListener('blur', save);
580
+ input.addEventListener('keydown', (e) => { if(e.key === 'Enter') { input.blur(); } });
581
+ titleSpan.replaceWith(input); input.focus();
582
+ }
583
+
584
+ async function deleteChat(cid) {
585
+ const el = document.getElementById('chat-' + cid);
586
+ if(el) el.remove();
587
+ await fetch('/delete_chat', {
588
+ method:'POST', headers:{'Content-Type':'application/json'},
589
+ body:JSON.stringify({username:currentUser, chat_id:cid})
590
+ });
591
+ if(currentChatId === cid) newChat();
592
+ loadHistory();
593
+ }
594
+
595
  async function loadHistory() {
596
+ try {
597
  const res = await fetch('/get_history', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
598
  const data = await res.json();
599
  const list = document.getElementById('history-list'); list.innerHTML = "";
600
+ if (data.chats) {
601
+ Object.keys(data.chats).reverse().forEach(cid => {
602
+ const title = data.chats[cid].title || "New Chat";
603
+ list.innerHTML += `
604
+ <div class="history-item" id="chat-${cid}" onclick="loadChat('${cid}')" oncontextmenu="return false;"
605
+ ontouchstart="handleHistoryTouchStart(event, '${cid}')" ontouchend="handleHistoryTouchEnd(event)">
606
+ <span class="h-title">${title}</span>
607
+ <div class="h-actions">
608
+ <i class="fas fa-pen h-icon" onclick="event.stopPropagation(); startRename('${cid}')"></i>
609
+ <i class="fas fa-trash h-icon" onclick="event.stopPropagation(); deleteChat('${cid}')"></i>
610
+ </div>
611
+ </div>`;
612
+ });
613
+ }
614
+ } catch(e) {}
615
  }
616
 
617
+ async function typeWriter(element, markdownText) {
618
+ element.innerHTML = marked.parse(markdownText);
619
+ hljs.highlightAll();
620
+ if (window.MathJax) await MathJax.typesetPromise([element]);
621
+ if (window.mermaid) {
622
+ const m = element.querySelectorAll('code.language-mermaid');
623
+ m.forEach(c => { const d = document.createElement('div'); d.className='mermaid'; d.innerHTML=c.innerText; c.parentElement.replaceWith(d); });
624
+ window.mermaid.init(undefined, element.querySelectorAll('.mermaid'));
625
+ }
626
+ element.style.opacity = 0; element.style.transition = 'opacity 0.4s';
627
+ setTimeout(() => { element.style.opacity = 1; scrollToBottom(); }, 50);
628
+ }
629
+
630
+ function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); }
631
+ async function newChat() {
632
+ currentChatId = null;
633
+ document.getElementById('chat-box').innerHTML = getIntroHtml(currentUser);
634
+ const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
635
+ const d = await r.json(); currentChatId = d.chat_id; loadHistory();
636
+ document.getElementById('sidebar').classList.remove('open');
637
+ }
638
  async function loadChat(cid) {
639
+ currentChatId = cid; const res = await fetch('/get_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})});
640
+ const data = await res.json(); const box = document.getElementById('chat-box'); box.innerHTML = "";
641
+ data.messages.forEach(msg => {
642
+ const isUser = msg.role === 'user';
643
+ if(isUser) {
644
+ box.insertAdjacentHTML('beforeend', `<div class="msg user-msg"><div class="user-content">${msg.content.replace(/</g, "&lt;")}</div></div>`);
645
+ } else {
646
+ const div = document.createElement('div'); div.className = 'msg ai-msg';
647
+ div.innerHTML = `<div class="ai-content">${marked.parse(msg.content)}</div>`;
648
+ box.appendChild(div);
649
+ hljs.highlightAll();
650
+ if(window.MathJax) MathJax.typesetPromise([div]);
651
+ div.insertAdjacentHTML('beforeend', `<div class="msg-actions"><i class="fas fa-copy action-icon" onclick="copyAiResponse(this)"></i></div>`);
652
+ }
653
  });
654
  document.getElementById('sidebar').classList.remove('open');
655
+ box.scrollTop = box.scrollHeight;
656
  }
657
 
 
658
  checkLogin();
659
  </script>
660
  </body>
661
  </html>
662
  """
663
+ # --- BACKEND ROUTES ---
 
664
  @app.route("/", methods=["GET"])
665
  def home(): return render_template_string(HTML_TEMPLATE)
666
 
 
673
  save_db(user_db)
674
  return jsonify({"chat_id": nid})
675
 
676
+ @app.route("/rename_chat", methods=["POST"])
677
+ def rename_chat():
678
+ d = request.json
679
+ u, cid, t = d.get("username"), d.get("chat_id"), d.get("title")
680
+ if u in user_db and cid in user_db[u]:
681
+ user_db[u][cid]["title"] = t
682
+ save_db(user_db)
683
+ return jsonify({"status":"ok"})
684
+
685
+ @app.route("/delete_chat", methods=["POST"])
686
+ def delete_chat():
687
+ d = request.json
688
+ u, cid = d.get("username"), d.get("chat_id")
689
+ if u in user_db and cid in user_db[u]:
690
+ del user_db[u][cid]
691
+ save_db(user_db)
692
+ return jsonify({"status":"ok"})
693
+
694
  @app.route("/get_history", methods=["POST"])
695
  def get_history():
696
  u = request.json.get("username")
 
704
  @app.route("/chat", methods=["POST"])
705
  def chat():
706
  d = request.json
707
+ u, cid, msg = d.get("username"), d.get("chat_id"), d.get("message")
708
+ img_data = d.get("image")
709
+ file_text = d.get("file_text")
710
+
711
  if u not in user_db: user_db[u] = {}
712
  if cid not in user_db[u]: user_db[u][cid] = {"messages": []}
713
+
714
  user_db[u][cid]["messages"].append({"role": "user", "content": msg})
715
+ reply = generate_with_retry(msg, img_data, file_text, user_db[u][cid]["messages"][:-1])
716
  user_db[u][cid]["messages"].append({"role": "model", "content": reply})
717
 
718
  new_title = False
719
  if len(user_db[u][cid]["messages"]) <= 2:
720
  user_db[u][cid]["title"] = " ".join(msg.split()[:4])
721
  new_title = True
722
+
723
  save_db(user_db)
724
  return jsonify({"response": reply, "new_title": new_title})
725
 
726
+ @app.route('/manifest.json')
727
+ def manifest():
728
+ data = {
729
+ "name": "Student's AI",
730
+ "short_name": "Student's AI",
731
+ "start_url": "/",
732
+ "display": "standalone",
733
+ "orientation": "portrait",
734
+ "background_color": "#09090b",
735
+ "theme_color": "#09090b",
736
+ "icons": [
737
+ {
738
+ "src": "https://huggingface.co/spaces/Shirpi/Student-s_AI/resolve/main/1000177401.png",
739
+ "sizes": "192x192",
740
+ "type": "image/png"
741
+ },
742
+ {
743
+ "src": "https://huggingface.co/spaces/Shirpi/Student-s_AI/resolve/main/1000177401.png",
744
+ "sizes": "512x512",
745
+ "type": "image/png"
746
+ }
747
+ ]
748
+ }
749
+ return Response(json.dumps(data), mimetype='application/json')
750
+
751
  if __name__ == '__main__':
752
  app.run(host='0.0.0.0', port=7860)