Shirpi commited on
Commit
affd9fd
·
verified ·
1 Parent(s): 33d60f5

Update app.py

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