Shirpi commited on
Commit
922f0ea
·
verified ·
1 Parent(s): 1298015

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -314
app.py CHANGED
@@ -31,19 +31,14 @@ user_db = load_db()
31
  current_key_index = 0
32
  app = Flask(__name__)
33
 
34
- # --- 🧠 SYSTEM INSTRUCTION (UPDATED) ---
35
  SYSTEM_INSTRUCTION = """
36
- ROLE: You are "Student's AI", an advanced tutor.
37
-
38
  RULES:
39
- 1. **MATH:** If the topic is Math, output formulas inside LaTeX block (e.g., $$ E = mc^2 $$). NEVER use plain text for formulas.
40
- 2. **DIAGRAMS:** If a diagram/flowchart is needed, use Mermaid.js syntax inside a code block (```mermaid ... ```).
41
- 3. **LANGUAGE:**
42
- - If user asks in **Tanglish** (Tamil in English letters), reply in **Tanglish** (e.g., "Idhu romba easy").
43
- - If user asks in **Tamil script**, reply in **Tamil script** (e.g., "இது மிகவும் எளிதானது").
44
- - Default is **English**.
45
- 4. **FORMAT:** Use Markdown. Bold key terms.
46
- 5. **CODE:** Put code in strictly formatted blocks.
47
  """
48
 
49
  # --- 🧬 MODEL & IMAGE HANDLING ---
@@ -52,7 +47,6 @@ def get_working_model(key):
52
  try:
53
  models = list(genai.list_models())
54
  chat_models = [m for m in models if 'generateContent' in m.supported_generation_methods]
55
- # Priority: Flash -> Pro
56
  for m in chat_models:
57
  if "flash" in m.name.lower() and "1.5" in m.name: return m.name
58
  for m in chat_models:
@@ -62,7 +56,6 @@ def get_working_model(key):
62
  return None
63
 
64
  def process_image(image_data):
65
- """Decodes Base64 image to PIL Image"""
66
  try:
67
  if "base64," in image_data:
68
  image_data = image_data.split("base64,")[1]
@@ -74,15 +67,11 @@ def generate_with_retry(prompt, image_data=None, history_messages=[]):
74
  global current_key_index
75
  if not API_KEYS: return "🚨 API Keys Missing."
76
 
77
- # Prepare Context
78
  formatted_history = []
79
  for m in history_messages[-6:]:
80
- # Images cannot be easily passed in history in this simple setup,
81
- # so we mostly rely on text history + current image.
82
  role = "user" if m["role"] == "user" else "model"
83
  formatted_history.append({"role": role, "parts": [m["content"]]})
84
 
85
- # Prepare Current Input
86
  current_parts = [prompt]
87
  if image_data:
88
  img = process_image(image_data)
@@ -100,8 +89,6 @@ def generate_with_retry(prompt, image_data=None, history_messages=[]):
100
  genai.configure(api_key=key)
101
  model = genai.GenerativeModel(model_name=model_name, system_instruction=SYSTEM_INSTRUCTION)
102
 
103
- # Note: start_chat history + images can be tricky in some versions.
104
- # We will use generate_content for image requests (stateless) or chat for text.
105
  if image_data:
106
  response = model.generate_content(current_parts)
107
  else:
@@ -110,13 +97,12 @@ def generate_with_retry(prompt, image_data=None, history_messages=[]):
110
 
111
  return response.text
112
  except Exception as e:
113
- print(f"Error: {e}")
114
  current_key_index = (current_key_index + 1) % len(API_KEYS)
115
  time.sleep(1)
116
 
117
  return "⚠️ System Busy. Please try again."
118
 
119
- # --- UI TEMPLATE (CORRECTED & ENHANCED) ---
120
  HTML_TEMPLATE = """
121
  <!DOCTYPE html>
122
  <html lang="en">
@@ -130,10 +116,7 @@ HTML_TEMPLATE = """
130
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
131
 
132
  <script>
133
- window.MathJax = {
134
- tex: { inlineMath: [['$', '$'], ['\\\\(', '\\\\)']], displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']] },
135
- svg: { fontCache: 'global' }
136
- };
137
  </script>
138
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
139
 
@@ -145,245 +128,99 @@ HTML_TEMPLATE = """
145
 
146
  <style>
147
  :root {
148
- --bg-color: #09090b;
149
- --sidebar-bg: #000000;
150
- --card-bg: #18181b;
151
- --user-msg-bg: #27272a;
152
- --ai-msg-bg: transparent;
153
- --text-color: #e4e4e7;
154
- --accent-color: #fff;
155
- --code-bg: #0f0f11;
156
  --border-color: #27272a;
157
  }
158
-
159
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
160
-
161
- body, html {
162
- margin: 0; padding: 0;
163
- height: 100%; width: 100%;
164
- background-color: var(--bg-color);
165
- color: var(--text-color);
166
- font-family: 'Outfit', sans-serif;
167
- overflow: hidden;
168
- }
169
 
170
- /* --- LAYOUT --- */
171
  #app-container { display: flex; flex-direction: column; height: 100dvh; position: relative; }
172
 
173
- /* --- HEADER (IMPROVED) --- */
174
  header {
175
- height: 70px;
176
- padding: 0 25px;
177
- background: rgba(9, 9, 11, 0.95);
178
- backdrop-filter: blur(10px);
179
  border-bottom: 1px solid var(--border-color);
180
- display: flex; align-items: center; justify-content: space-between;
181
- flex-shrink: 0; z-index: 50;
182
- }
183
- .app-title {
184
- font-size: 24px;
185
- font-weight: 700;
186
- background: linear-gradient(90deg, #fff, #a1a1aa);
187
- -webkit-background-clip: text;
188
- -webkit-text-fill-color: transparent;
189
- letter-spacing: -0.5px;
190
  }
 
191
 
192
- /* --- SIDEBAR --- */
193
  #sidebar {
194
- position: fixed; top: 0; left: 0; bottom: 0;
195
- width: 300px;
196
- background: var(--sidebar-bg);
197
- border-right: 1px solid var(--border-color);
198
- transform: translateX(-100%);
199
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
200
- z-index: 100;
201
- display: flex; flex-direction: column; padding: 20px;
202
  }
203
  #sidebar.open { transform: translateX(0); }
204
- .overlay {
205
- position: fixed; inset: 0; background: rgba(0,0,0,0.7);
206
- z-index: 90; opacity: 0; pointer-events: none; transition: opacity 0.3s;
207
- }
208
  .overlay.active { opacity: 1; pointer-events: auto; }
209
 
210
- /* Search & History */
211
- .search-box {
212
- background: var(--card-bg); border: 1px solid var(--border-color);
213
- padding: 12px; border-radius: 10px; color: #fff; width: 100%;
214
- margin-bottom: 15px; font-family: 'Outfit', sans-serif; outline: none;
215
- }
216
  #history-list { flex: 1; overflow-y: auto; }
217
- .history-item {
218
- padding: 14px; border-radius: 10px; cursor: pointer;
219
- color: #a1a1aa; font-size: 15px; transition: 0.2s;
220
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
221
- margin-bottom: 5px;
222
- }
223
  .history-item:hover { background: var(--user-msg-bg); color: #fff; }
224
 
225
- /* --- CHAT AREA --- */
226
  #chat-box {
227
- flex: 1;
228
- overflow-y: auto;
229
- padding: 20px 5%; /* Better spacing */
230
- display: flex;
231
- flex-direction: column;
232
- gap: 30px;
233
- scroll-behavior: smooth;
234
- }
235
-
236
- /* Message Styles */
237
- .msg {
238
- width: 100%; /* FIX 1: Use full width */
239
- line-height: 1.7;
240
- font-size: 16px;
241
- position: relative;
242
- animation: fadeIn 0.3s ease;
243
- }
244
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
245
-
246
- /* User Message */
247
- .user-msg { align-self: flex-end; text-align: right; }
248
- .user-bubble {
249
- display: inline-block;
250
- background: var(--user-msg-bg);
251
- color: #fff;
252
- padding: 12px 20px;
253
- border-radius: 20px 20px 4px 20px;
254
- max-width: 90%;
255
- text-align: left;
256
- position: relative;
257
  }
258
 
259
- /* AI Message */
260
- .ai-msg { align-self: flex-start; }
261
- .ai-content { width: 100%; }
262
-
263
- /* Styling for AI Content */
264
- .ai-msg strong { color: #fff; font-weight: 700; }
265
- .ai-msg h1, .ai-msg h2 { margin-top: 20px; color: #fff; font-weight: 700; }
266
 
267
- /* FIX 3: Math Box */
268
- .mjx-chtml {
269
- background: #1e1e22;
270
- padding: 15px;
271
- border-radius: 8px;
272
- border: 1px solid #333;
273
- overflow-x: auto;
274
- margin: 15px 0;
275
- display: block;
276
- text-align: center;
277
- }
278
 
279
- /* Code Block */
280
- pre {
281
- background: var(--code-bg);
282
- border: 1px solid var(--border-color);
283
- border-radius: 10px;
284
- padding: 15px;
285
- overflow-x: auto;
286
- margin: 15px 0;
287
- font-family: 'JetBrains Mono', monospace;
288
- }
289
  code { font-family: 'JetBrains Mono', monospace; font-size: 14px; color: #a5d6ff; }
290
-
291
- /* Diagram Container */
292
- .mermaid { background: #111; padding: 20px; border-radius: 10px; text-align: center; margin: 15px 0; }
293
-
294
- /* Action Buttons (Copy/Edit/Regen) */
295
- .msg-actions {
296
- margin-top: 5px;
297
- opacity: 0;
298
- transition: opacity 0.2s;
299
- font-size: 12px;
300
- display: flex; gap: 10px;
301
- justify-content: flex-end; /* Right align for user */
302
- }
303
- .ai-msg .msg-actions { justify-content: flex-start; }
304
  .msg:hover .msg-actions { opacity: 1; }
305
  .action-btn { cursor: pointer; color: #71717a; display: flex; align-items: center; gap: 4px; }
306
  .action-btn:hover { color: #fff; }
307
 
308
- /* --- INPUT AREA (FIXED) --- */
309
- .input-wrapper {
310
- background: var(--bg-color);
311
- padding: 20px;
312
- border-top: 1px solid var(--border-color);
313
- flex-shrink: 0; z-index: 60;
314
- }
315
  .input-container {
316
- max-width: 900px;
317
- margin: 0 auto;
318
- background: var(--card-bg);
319
- border: 1px solid var(--border-color);
320
- border-radius: 24px;
321
- padding: 10px 15px;
322
- display: flex; align-items: flex-end; gap: 10px;
323
- transition: border 0.2s;
324
- }
325
- .input-container:focus-within { border-color: #666; }
326
-
327
- textarea {
328
- flex: 1;
329
- background: transparent; border: none;
330
- color: #fff; font-size: 16px;
331
- max-height: 150px; padding: 10px 0;
332
- resize: none; outline: none;
333
- font-family: 'Outfit', sans-serif;
334
- line-height: 1.5;
335
  }
 
336
 
337
- /* Upload & Send Buttons */
338
  .icon-btn {
339
- width: 40px; height: 40px;
340
- display: flex; align-items: center; justify-content: center;
341
- border-radius: 50%; border: none;
342
- background: transparent; color: #a1a1aa;
343
- cursor: pointer; transition: 0.2s;
344
- font-size: 18px;
345
  }
346
  .icon-btn:hover { background: #333; color: #fff; }
347
 
348
  .send-btn {
349
- background: #fff; color: #000;
350
- width: 40px; height: 40px;
351
- border-radius: 50%; border: none;
352
- display: flex; align-items: center; justify-content: center;
353
- cursor: pointer; font-size: 18px;
354
- transition: transform 0.2s;
355
- }
356
- .send-btn:hover { transform: scale(1.1); }
357
-
358
- /* Image Preview */
359
- #image-preview-area {
360
- max-width: 900px; margin: 0 auto 10px auto;
361
- display: none; padding: 0 10px;
362
- }
363
- .preview-img-container {
364
- position: relative; display: inline-block;
365
- }
366
- .preview-img {
367
- height: 60px; border-radius: 8px; border: 1px solid #333;
368
- }
369
- .remove-img {
370
- position: absolute; top: -5px; right: -5px;
371
- background: red; color: white; border-radius: 50%;
372
- width: 18px; height: 18px; font-size: 12px;
373
- cursor: pointer; border: none; display: flex; align-items: center; justify-content: center;
374
  }
375
 
376
- /* --- LOGIN --- */
377
- #login-overlay {
378
- position: fixed; inset: 0; background: #000; z-index: 2000;
379
- display: flex; align-items: center; justify-content: center;
380
- }
381
- .login-box {
382
- width: 90%; max-width: 380px; text-align: center;
383
- padding: 40px; border: 1px solid var(--border-color);
384
- border-radius: 20px; background: #0a0a0a;
385
- }
386
 
 
 
 
 
 
387
  </style>
388
  </head>
389
  <body>
@@ -391,10 +228,8 @@ HTML_TEMPLATE = """
391
  <div id="login-overlay">
392
  <div class="login-box">
393
  <h1 class="app-title" style="margin-bottom:10px;">Student's AI</h1>
394
- <p style="color:#666; font-size:14px; margin-bottom:25px;">The Ultimate Learning Companion</p>
395
- <input type="text" id="username-input" placeholder="What's your name?"
396
- style="width:100%; padding:15px; border-radius:10px; border:1px solid #333; background:#111; color:#fff; text-align:center; outline:none; margin-bottom:15px;">
397
- <button onclick="handleLogin()" style="width:100%; padding:15px; border-radius:10px; border:none; background:#fff; font-weight:700; cursor:pointer;">Start Learning</button>
398
  </div>
399
  </div>
400
 
@@ -403,25 +238,18 @@ HTML_TEMPLATE = """
403
 
404
  <div id="sidebar">
405
  <div style="padding-bottom:20px; border-bottom:1px solid #333; margin-bottom:20px; display:flex; align-items:center; gap:12px;">
406
- <div style="width:40px; height:40px; background:#fff; color:#000; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:bold; font-size:18px;" id="avatar">U</div>
407
- <div style="display:flex; flex-direction:column;">
408
- <span id="display-name" style="font-weight:700; font-size:16px;">User</span>
409
- <span style="font-size:12px; color:#666;">Student</span>
410
- </div>
411
  </div>
412
-
413
- <button onclick="newChat()" style="width:100%; padding:12px; background:#fff; border:none; border-radius:8px; cursor:pointer; font-weight:700; margin-bottom:20px; display:flex; align-items:center; justify-content:center; gap:8px;">
414
- <i class="fas fa-plus"></i> New Chat
415
- </button>
416
-
417
- <input type="text" class="search-box" placeholder="Search chats..." onkeyup="filterHistory(this.value)">
418
  <div id="history-list"></div>
419
  <div style="margin-top:auto; padding-top:20px; border-top:1px solid #333; text-align:center; color:#ef4444; cursor:pointer;" onclick="handleLogout()">Log Out</div>
420
  </div>
421
 
422
  <header>
423
  <div style="display:flex; align-items:center; gap:15px;">
424
- <i class="fas fa-bars" style="cursor:pointer; font-size:20px; color:#ccc;" onclick="toggleSidebar()"></i>
425
  <span class="app-title">Student's AI</span>
426
  </div>
427
  </header>
@@ -430,19 +258,24 @@ HTML_TEMPLATE = """
430
 
431
  <div class="input-wrapper">
432
  <div id="image-preview-area">
433
- <div class="preview-img-container">
434
  <img id="preview-img-tag" class="preview-img">
435
  <button class="remove-img" onclick="removeImage()">×</button>
436
  </div>
437
  </div>
438
 
439
  <div class="input-container">
440
- <button class="icon-btn" onclick="document.getElementById('img-upload').click()">
441
  <i class="fas fa-paperclip"></i>
442
  </button>
443
- <input type="file" id="img-upload" accept="image/*" hidden onchange="handleImageSelect(this)">
444
 
445
- <textarea id="input" placeholder="Ask about Diagrams, Math, or Code..." rows="1" oninput="resizeInput(this)"></textarea>
 
 
 
 
 
446
 
447
  <button class="send-btn" onclick="send()">
448
  <i class="fas fa-arrow-up"></i>
@@ -456,7 +289,6 @@ HTML_TEMPLATE = """
456
  let currentChatId = null;
457
  let currentImageBase64 = null;
458
 
459
- // --- AUTH ---
460
  function checkLogin() {
461
  const stored = localStorage.getItem("student_ai_user");
462
  if (stored) { currentUser = stored; showApp(); }
@@ -475,13 +307,11 @@ HTML_TEMPLATE = """
475
  if(!currentChatId) newChat();
476
  }
477
 
478
- // --- UI LOGIC ---
479
  function resizeInput(el) {
480
  el.style.height = 'auto';
481
  el.style.height = Math.min(el.scrollHeight, 150) + 'px';
482
  }
483
 
484
- // --- IMAGE HANDLING ---
485
  function handleImageSelect(input) {
486
  if (input.files && input.files[0]) {
487
  const reader = new FileReader();
@@ -495,21 +325,17 @@ HTML_TEMPLATE = """
495
  }
496
  function removeImage() {
497
  currentImageBase64 = null;
498
- document.getElementById('img-upload').value = "";
 
499
  document.getElementById('image-preview-area').style.display = 'none';
500
  }
501
 
502
- // --- SCROLL LOGIC (FIX 2) ---
503
- function scrollToBottom(force=false) {
504
  const box = document.getElementById('chat-box');
505
- // Smart scroll: Only scroll if user is already near bottom, or if forced
506
- const isNearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 100;
507
- if (isNearBottom || force) {
508
- box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
509
- }
510
  }
511
 
512
- // --- CHAT LOGIC ---
513
  async function send() {
514
  const input = document.getElementById('input');
515
  const text = input.value.trim();
@@ -517,7 +343,6 @@ HTML_TEMPLATE = """
517
 
518
  const chatBox = document.getElementById('chat-box');
519
 
520
- // 1. User Bubble (With Edit)
521
  const userHtml = `
522
  <div class="msg user-msg">
523
  <div class="user-bubble">${text.replace(/</g, "&lt;")}
@@ -532,16 +357,13 @@ HTML_TEMPLATE = """
532
 
533
  const promptText = text;
534
  const imgData = currentImageBase64;
535
-
536
- // Clear Inputs
537
  input.value = ''; input.style.height = 'auto';
538
  removeImage();
539
- scrollToBottom(true);
540
 
541
- // 2. AI Spinner
542
  const msgId = "ai-" + Date.now();
543
  chatBox.insertAdjacentHTML('beforeend', `<div id="${msgId}" class="msg ai-msg"><i class="fas fa-circle-notch fa-spin"></i></div>`);
544
- scrollToBottom(true);
545
 
546
  try {
547
  if (!currentChatId) {
@@ -561,26 +383,23 @@ HTML_TEMPLATE = """
561
  });
562
  const data = await res.json();
563
 
564
- // 3. Render AI Response
565
  const aiDiv = document.getElementById(msgId);
566
- aiDiv.innerHTML = ""; // Clear spinner
567
 
568
- // Content Wrapper
569
  const contentDiv = document.createElement('div');
570
  contentDiv.className = 'ai-content';
571
  aiDiv.appendChild(contentDiv);
572
 
573
- // Typewriter Effect
574
  await typeWriter(contentDiv, data.response);
575
-
576
- // Add Actions (Regenerate/Copy)
577
- const actionsHtml = `
578
  <div class="msg-actions">
579
  <span class="action-btn" onclick="navigator.clipboard.writeText(this.closest('.ai-msg').innerText)"><i class="fas fa-copy"></i> Copy</span>
580
  <span class="action-btn" onclick="regenerate('${promptText}')"><i class="fas fa-redo"></i> Regen</span>
581
- </div>`;
582
- aiDiv.insertAdjacentHTML('beforeend', actionsHtml);
583
 
 
584
  if(data.new_title) loadHistory();
585
 
586
  } catch (e) {
@@ -588,55 +407,46 @@ HTML_TEMPLATE = """
588
  }
589
  }
590
 
591
- // --- REGENERATE & EDIT ---
592
- function editMessage(oldText) {
593
- document.getElementById('input').value = oldText;
594
- document.getElementById('input').focus();
595
- }
596
- function regenerate(text) {
597
- document.getElementById('input').value = text;
598
- send();
599
- }
600
-
601
- // --- TYPEWRITER & RENDERING ---
602
  async function typeWriter(element, markdownText) {
603
- // 1. Parse Markdown
604
  element.innerHTML = marked.parse(markdownText);
605
 
606
- // 2. Render Math (MathJax)
607
- if (window.MathJax) {
608
- await MathJax.typesetPromise([element]);
609
- }
610
-
611
- // 3. Render Diagrams (Mermaid)
612
- const mermaids = element.querySelectorAll('code.language-mermaid');
613
- for (let i = 0; i < mermaids.length; i++) {
614
- const code = mermaids[i].innerText;
615
- const parentPre = mermaids[i].parentElement;
616
-
617
- // Create clean container
618
- const div = document.createElement('div');
619
- div.className = 'mermaid';
620
- div.innerHTML = code;
621
- parentPre.replaceWith(div);
622
  }
623
- if(window.mermaid) window.mermaid.init(undefined, document.querySelectorAll('.mermaid'));
624
 
625
- // 4. Fade In Animation (Line by Line simulation)
 
626
  const children = Array.from(element.children);
627
  children.forEach((child, index) => {
628
  child.style.opacity = '0';
629
  child.style.transform = 'translateY(10px)';
630
- child.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
 
631
  setTimeout(() => {
632
  child.style.opacity = '1';
633
  child.style.transform = 'translateY(0)';
634
  scrollToBottom();
635
- }, index * 100);
636
  });
637
  }
638
 
639
- // --- HISTORY & SIDEBAR ---
 
 
 
 
 
 
 
 
640
  function toggleSidebar() {
641
  document.getElementById('sidebar').classList.toggle('open');
642
  document.getElementById('overlay').classList.toggle('active');
@@ -680,7 +490,6 @@ HTML_TEMPLATE = """
680
  data.messages.forEach(msg => {
681
  const isUser = msg.role === 'user';
682
  let content = msg.content;
683
-
684
  if(isUser) {
685
  box.insertAdjacentHTML('beforeend', `<div class="msg user-msg"><div class="user-bubble">${content.replace(/</g, "&lt;")}</div></div>`);
686
  } else {
@@ -688,7 +497,6 @@ HTML_TEMPLATE = """
688
  div.className = 'msg ai-msg';
689
  div.innerHTML = `<div class="ai-content">${marked.parse(content)}</div>`;
690
  box.appendChild(div);
691
- // Rerender Math/Mermaid for history
692
  if(window.MathJax) MathJax.typesetPromise([div]);
693
  if(window.mermaid) {
694
  const m = div.querySelectorAll('code.language-mermaid');
@@ -742,16 +550,10 @@ def chat():
742
  if u not in user_db: user_db[u] = {}
743
  if cid not in user_db[u]: user_db[u][cid] = {"messages": []}
744
 
745
- # Save User Msg (Note: Image not saved in DB to keep it simple, only text)
746
  user_db[u][cid]["messages"].append({"role": "user", "content": msg})
747
-
748
- # Generate
749
  reply = generate_with_retry(msg, img_data, user_db[u][cid]["messages"][:-1])
750
-
751
- # Save AI Msg
752
  user_db[u][cid]["messages"].append({"role": "model", "content": reply})
753
 
754
- # Title Update
755
  new_title = False
756
  if len(user_db[u][cid]["messages"]) <= 2:
757
  user_db[u][cid]["title"] = " ".join(msg.split()[:4])
@@ -762,4 +564,4 @@ def chat():
762
 
763
  if __name__ == '__main__':
764
  app.run(host='0.0.0.0', port=7860)
765
-
 
31
  current_key_index = 0
32
  app = Flask(__name__)
33
 
34
+ # --- 🧠 SYSTEM INSTRUCTION ---
35
  SYSTEM_INSTRUCTION = """
36
+ ROLE: You are "Student's AI", a helpful tutor.
 
37
  RULES:
38
+ 1. **MATH:** Use LaTeX for formulas ($$ ... $$).
39
+ 2. **DIAGRAMS:** Use Mermaid.js (```mermaid ... ```).
40
+ 3. **LANGUAGE:** English by default. Use Tamil/Tanglish ONLY if requested.
41
+ 4. **FORMAT:** Markdown. Bold key terms.
 
 
 
 
42
  """
43
 
44
  # --- 🧬 MODEL & IMAGE HANDLING ---
 
47
  try:
48
  models = list(genai.list_models())
49
  chat_models = [m for m in models if 'generateContent' in m.supported_generation_methods]
 
50
  for m in chat_models:
51
  if "flash" in m.name.lower() and "1.5" in m.name: return m.name
52
  for m in chat_models:
 
56
  return None
57
 
58
  def process_image(image_data):
 
59
  try:
60
  if "base64," in image_data:
61
  image_data = image_data.split("base64,")[1]
 
67
  global current_key_index
68
  if not API_KEYS: return "🚨 API Keys Missing."
69
 
 
70
  formatted_history = []
71
  for m in history_messages[-6:]:
 
 
72
  role = "user" if m["role"] == "user" else "model"
73
  formatted_history.append({"role": role, "parts": [m["content"]]})
74
 
 
75
  current_parts = [prompt]
76
  if image_data:
77
  img = process_image(image_data)
 
89
  genai.configure(api_key=key)
90
  model = genai.GenerativeModel(model_name=model_name, system_instruction=SYSTEM_INSTRUCTION)
91
 
 
 
92
  if image_data:
93
  response = model.generate_content(current_parts)
94
  else:
 
97
 
98
  return response.text
99
  except Exception as e:
 
100
  current_key_index = (current_key_index + 1) % len(API_KEYS)
101
  time.sleep(1)
102
 
103
  return "⚠️ System Busy. Please try again."
104
 
105
+ # --- UI TEMPLATE (PERFORMANCE OPTIMIZED) ---
106
  HTML_TEMPLATE = """
107
  <!DOCTYPE html>
108
  <html lang="en">
 
116
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
117
 
118
  <script>
119
+ window.MathJax = { tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$']] }, svg: { fontCache: 'global' } };
 
 
 
120
  </script>
121
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
122
 
 
128
 
129
  <style>
130
  :root {
131
+ --bg-color: #09090b; --sidebar-bg: #000000; --card-bg: #18181b;
132
+ --user-msg-bg: #27272a; --text-color: #e4e4e7; --code-bg: #0f0f11;
 
 
 
 
 
 
133
  --border-color: #27272a;
134
  }
 
135
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
136
+ body, html { margin: 0; padding: 0; height: 100%; width: 100%; background: var(--bg-color); color: var(--text-color); font-family: 'Outfit', sans-serif; overflow: hidden; }
 
 
 
 
 
 
 
 
137
 
 
138
  #app-container { display: flex; flex-direction: column; height: 100dvh; position: relative; }
139
 
140
+ /* HEADER */
141
  header {
142
+ height: 65px; padding: 0 20px;
143
+ background: rgba(9,9,11, 0.98); /* Less transparency = Less Lag */
 
 
144
  border-bottom: 1px solid var(--border-color);
145
+ display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; z-index: 50;
 
 
 
 
 
 
 
 
 
146
  }
147
+ .app-title { font-size: 22px; font-weight: 700; background: linear-gradient(90deg, #fff, #a1a1aa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
148
 
149
+ /* SIDEBAR */
150
  #sidebar {
151
+ position: fixed; inset: 0; width: 280px; background: var(--sidebar-bg);
152
+ border-right: 1px solid var(--border-color); transform: translateX(-100%);
153
+ transition: transform 0.3s; z-index: 100; display: flex; flex-direction: column; padding: 20px;
 
 
 
 
 
154
  }
155
  #sidebar.open { transform: translateX(0); }
156
+ .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 90; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
 
 
 
157
  .overlay.active { opacity: 1; pointer-events: auto; }
158
 
159
+ .search-box { background: var(--card-bg); border: 1px solid var(--border-color); padding: 12px; border-radius: 10px; color: #fff; width: 100%; margin-bottom: 15px; outline: none; }
 
 
 
 
 
160
  #history-list { flex: 1; overflow-y: auto; }
161
+ .history-item { padding: 12px; border-radius: 8px; cursor: pointer; color: #a1a1aa; font-size: 14px; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
 
 
 
 
 
162
  .history-item:hover { background: var(--user-msg-bg); color: #fff; }
163
 
164
+ /* CHAT AREA - FIX FOR LAG */
165
  #chat-box {
166
+ flex: 1; overflow-y: auto; padding: 20px 5%;
167
+ display: flex; flex-direction: column; gap: 20px;
168
+ /* CRITICAL FIX FOR MOBILE SCROLLING */
169
+ -webkit-overflow-scrolling: touch;
170
+ overscroll-behavior-y: contain;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  }
172
 
173
+ /* Message Styles */
174
+ .msg { width: 100%; line-height: 1.6; font-size: 16px; }
175
+ .user-msg { text-align: right; animation: fadeInstant 0.3s forwards; }
176
+ .user-bubble { display: inline-block; background: var(--user-msg-bg); padding: 10px 18px; border-radius: 18px 18px 4px 18px; text-align: left; max-width: 85%; }
 
 
 
177
 
178
+ .ai-msg { text-align: left; }
179
+ .ai-content strong { color: #fff; font-weight: 700; }
180
+ .ai-content h1, .ai-content h2 { margin-top: 15px; color: #fff; font-weight: 700; }
 
 
 
 
 
 
 
 
181
 
182
+ /* Boxes */
183
+ .mjx-chtml { background: #1e1e22; padding: 10px; border-radius: 8px; border: 1px solid #333; overflow-x: auto; margin: 10px 0; text-align: center; }
184
+ pre { background: var(--code-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 15px; overflow-x: auto; margin: 15px 0; font-family: 'JetBrains Mono', monospace; }
 
 
 
 
 
 
 
185
  code { font-family: 'JetBrains Mono', monospace; font-size: 14px; color: #a5d6ff; }
186
+ .mermaid { background: #111; padding: 15px; border-radius: 10px; text-align: center; margin: 15px 0; }
187
+
188
+ /* Actions */
189
+ .msg-actions { margin-top: 5px; opacity: 0; transition: opacity 0.2s; font-size: 12px; display: flex; gap: 10px; }
190
+ .user-msg .msg-actions { justify-content: flex-end; }
 
 
 
 
 
 
 
 
 
191
  .msg:hover .msg-actions { opacity: 1; }
192
  .action-btn { cursor: pointer; color: #71717a; display: flex; align-items: center; gap: 4px; }
193
  .action-btn:hover { color: #fff; }
194
 
195
+ /* INPUT AREA */
196
+ .input-wrapper { background: var(--bg-color); padding: 15px; border-top: 1px solid var(--border-color); flex-shrink: 0; z-index: 60; }
 
 
 
 
 
197
  .input-container {
198
+ max-width: 900px; margin: 0 auto; background: var(--card-bg);
199
+ border: 1px solid var(--border-color); border-radius: 24px; padding: 8px 10px;
200
+ display: flex; align-items: flex-end; gap: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
202
+ textarea { flex: 1; background: transparent; border: none; color: #fff; font-size: 16px; max-height: 120px; padding: 10px 5px; resize: none; outline: none; font-family: 'Outfit', sans-serif; }
203
 
 
204
  .icon-btn {
205
+ width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
206
+ border-radius: 50%; border: none; background: transparent; color: #a1a1aa; cursor: pointer; font-size: 16px;
 
 
 
 
207
  }
208
  .icon-btn:hover { background: #333; color: #fff; }
209
 
210
  .send-btn {
211
+ background: #fff; color: #000; width: 36px; height: 36px; border-radius: 50%; border: none;
212
+ display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  }
214
 
215
+ #image-preview-area { max-width: 900px; margin: 0 auto 10px auto; display: none; padding: 0 10px; }
216
+ .preview-img { height: 60px; border-radius: 8px; border: 1px solid #333; }
217
+ .remove-img { position: absolute; top: -5px; right: -5px; background: red; color: white; border-radius: 50%; width: 18px; height: 18px; font-size: 12px; cursor: pointer; border: none; }
 
 
 
 
 
 
 
218
 
219
+ /* LOGIN */
220
+ #login-overlay { position: fixed; inset: 0; background: #000; z-index: 2000; display: flex; align-items: center; justify-content: center; }
221
+ .login-box { width: 90%; max-width: 350px; text-align: center; padding: 30px; border: 1px solid var(--border-color); border-radius: 20px; background: #0a0a0a; }
222
+
223
+ @keyframes fadeInstant { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
224
  </style>
225
  </head>
226
  <body>
 
228
  <div id="login-overlay">
229
  <div class="login-box">
230
  <h1 class="app-title" style="margin-bottom:10px;">Student's AI</h1>
231
+ <input type="text" id="username-input" placeholder="Your Name" style="width:100%; padding:12px; border-radius:10px; border:1px solid #333; background:#111; color:#fff; text-align:center; outline:none; margin-bottom:15px;">
232
+ <button onclick="handleLogin()" style="width:100%; padding:12px; border-radius:10px; border:none; background:#fff; font-weight:700; cursor:pointer;">Start</button>
 
 
233
  </div>
234
  </div>
235
 
 
238
 
239
  <div id="sidebar">
240
  <div style="padding-bottom:20px; border-bottom:1px solid #333; margin-bottom:20px; display:flex; align-items:center; gap:12px;">
241
+ <div style="width:35px; height:35px; background:#fff; color:#000; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:bold;" id="avatar">U</div>
242
+ <span id="display-name" style="font-weight:700;">User</span>
 
 
 
243
  </div>
244
+ <button onclick="newChat()" style="width:100%; padding:10px; background:#fff; border:none; border-radius:8px; cursor:pointer; font-weight:700; margin-bottom:15px;">+ New Chat</button>
245
+ <input type="text" class="search-box" placeholder="Search..." onkeyup="filterHistory(this.value)">
 
 
 
 
246
  <div id="history-list"></div>
247
  <div style="margin-top:auto; padding-top:20px; border-top:1px solid #333; text-align:center; color:#ef4444; cursor:pointer;" onclick="handleLogout()">Log Out</div>
248
  </div>
249
 
250
  <header>
251
  <div style="display:flex; align-items:center; gap:15px;">
252
+ <i class="fas fa-bars" style="cursor:pointer; color:#ccc;" onclick="toggleSidebar()"></i>
253
  <span class="app-title">Student's AI</span>
254
  </div>
255
  </header>
 
258
 
259
  <div class="input-wrapper">
260
  <div id="image-preview-area">
261
+ <div style="position:relative; display:inline-block;">
262
  <img id="preview-img-tag" class="preview-img">
263
  <button class="remove-img" onclick="removeImage()">×</button>
264
  </div>
265
  </div>
266
 
267
  <div class="input-container">
268
+ <button class="icon-btn" onclick="document.getElementById('file-upload').click()">
269
  <i class="fas fa-paperclip"></i>
270
  </button>
271
+ <input type="file" id="file-upload" accept="image/*" hidden onchange="handleImageSelect(this)">
272
 
273
+ <button class="icon-btn" onclick="document.getElementById('camera-upload').click()">
274
+ <i class="fas fa-camera"></i>
275
+ </button>
276
+ <input type="file" id="camera-upload" accept="image/*" capture="environment" hidden onchange="handleImageSelect(this)">
277
+
278
+ <textarea id="input" placeholder="Type a message..." rows="1" oninput="resizeInput(this)"></textarea>
279
 
280
  <button class="send-btn" onclick="send()">
281
  <i class="fas fa-arrow-up"></i>
 
289
  let currentChatId = null;
290
  let currentImageBase64 = null;
291
 
 
292
  function checkLogin() {
293
  const stored = localStorage.getItem("student_ai_user");
294
  if (stored) { currentUser = stored; showApp(); }
 
307
  if(!currentChatId) newChat();
308
  }
309
 
 
310
  function resizeInput(el) {
311
  el.style.height = 'auto';
312
  el.style.height = Math.min(el.scrollHeight, 150) + 'px';
313
  }
314
 
 
315
  function handleImageSelect(input) {
316
  if (input.files && input.files[0]) {
317
  const reader = new FileReader();
 
325
  }
326
  function removeImage() {
327
  currentImageBase64 = null;
328
+ document.getElementById('file-upload').value = "";
329
+ document.getElementById('camera-upload').value = "";
330
  document.getElementById('image-preview-area').style.display = 'none';
331
  }
332
 
333
+ // --- SCROLL OPTIMIZATION ---
334
+ function scrollToBottom() {
335
  const box = document.getElementById('chat-box');
336
+ box.scrollTo({ top: box.scrollHeight, behavior: 'smooth' });
 
 
 
 
337
  }
338
 
 
339
  async function send() {
340
  const input = document.getElementById('input');
341
  const text = input.value.trim();
 
343
 
344
  const chatBox = document.getElementById('chat-box');
345
 
 
346
  const userHtml = `
347
  <div class="msg user-msg">
348
  <div class="user-bubble">${text.replace(/</g, "&lt;")}
 
357
 
358
  const promptText = text;
359
  const imgData = currentImageBase64;
 
 
360
  input.value = ''; input.style.height = 'auto';
361
  removeImage();
362
+ scrollToBottom();
363
 
 
364
  const msgId = "ai-" + Date.now();
365
  chatBox.insertAdjacentHTML('beforeend', `<div id="${msgId}" class="msg ai-msg"><i class="fas fa-circle-notch fa-spin"></i></div>`);
366
+ scrollToBottom();
367
 
368
  try {
369
  if (!currentChatId) {
 
383
  });
384
  const data = await res.json();
385
 
 
386
  const aiDiv = document.getElementById(msgId);
387
+ aiDiv.innerHTML = "";
388
 
 
389
  const contentDiv = document.createElement('div');
390
  contentDiv.className = 'ai-content';
391
  aiDiv.appendChild(contentDiv);
392
 
393
+ // --- RESTORED TYPEWRITER ANIMATION (Optimized) ---
394
  await typeWriter(contentDiv, data.response);
395
+
396
+ aiDiv.insertAdjacentHTML('beforeend', `
 
397
  <div class="msg-actions">
398
  <span class="action-btn" onclick="navigator.clipboard.writeText(this.closest('.ai-msg').innerText)"><i class="fas fa-copy"></i> Copy</span>
399
  <span class="action-btn" onclick="regenerate('${promptText}')"><i class="fas fa-redo"></i> Regen</span>
400
+ </div>`);
 
401
 
402
+ scrollToBottom();
403
  if(data.new_title) loadHistory();
404
 
405
  } catch (e) {
 
407
  }
408
  }
409
 
410
+ // --- ANIMATION LOGIC ---
 
 
 
 
 
 
 
 
 
 
411
  async function typeWriter(element, markdownText) {
412
+ // 1. Render markdown
413
  element.innerHTML = marked.parse(markdownText);
414
 
415
+ // 2. Math & Diagrams
416
+ if (window.MathJax) await MathJax.typesetPromise([element]);
417
+ if (window.mermaid) {
418
+ const m = element.querySelectorAll('code.language-mermaid');
419
+ m.forEach(c => {
420
+ const d = document.createElement('div'); d.className='mermaid'; d.innerHTML=c.innerText; c.parentElement.replaceWith(d);
421
+ });
422
+ window.mermaid.init(undefined, element.querySelectorAll('.mermaid'));
 
 
 
 
 
 
 
 
423
  }
 
424
 
425
+ // 3. FADE IN ANIMATION (Not Typewriter char-by-char) for Smoothness
426
+ // This is "ChatGPT-style" line reveal without the lag.
427
  const children = Array.from(element.children);
428
  children.forEach((child, index) => {
429
  child.style.opacity = '0';
430
  child.style.transform = 'translateY(10px)';
431
+ child.style.transition = 'opacity 0.4s ease, transform 0.4s ease';
432
+
433
  setTimeout(() => {
434
  child.style.opacity = '1';
435
  child.style.transform = 'translateY(0)';
436
  scrollToBottom();
437
+ }, index * 120);
438
  });
439
  }
440
 
441
+ function editMessage(oldText) {
442
+ document.getElementById('input').value = oldText;
443
+ document.getElementById('input').focus();
444
+ }
445
+ function regenerate(text) {
446
+ document.getElementById('input').value = text;
447
+ send();
448
+ }
449
+
450
  function toggleSidebar() {
451
  document.getElementById('sidebar').classList.toggle('open');
452
  document.getElementById('overlay').classList.toggle('active');
 
490
  data.messages.forEach(msg => {
491
  const isUser = msg.role === 'user';
492
  let content = msg.content;
 
493
  if(isUser) {
494
  box.insertAdjacentHTML('beforeend', `<div class="msg user-msg"><div class="user-bubble">${content.replace(/</g, "&lt;")}</div></div>`);
495
  } else {
 
497
  div.className = 'msg ai-msg';
498
  div.innerHTML = `<div class="ai-content">${marked.parse(content)}</div>`;
499
  box.appendChild(div);
 
500
  if(window.MathJax) MathJax.typesetPromise([div]);
501
  if(window.mermaid) {
502
  const m = div.querySelectorAll('code.language-mermaid');
 
550
  if u not in user_db: user_db[u] = {}
551
  if cid not in user_db[u]: user_db[u][cid] = {"messages": []}
552
 
 
553
  user_db[u][cid]["messages"].append({"role": "user", "content": msg})
 
 
554
  reply = generate_with_retry(msg, img_data, user_db[u][cid]["messages"][:-1])
 
 
555
  user_db[u][cid]["messages"].append({"role": "model", "content": reply})
556
 
 
557
  new_title = False
558
  if len(user_db[u][cid]["messages"]) <= 2:
559
  user_db[u][cid]["title"] = " ".join(msg.split()[:4])
 
564
 
565
  if __name__ == '__main__':
566
  app.run(host='0.0.0.0', port=7860)
567
+