Shirpi commited on
Commit
e127816
·
verified ·
1 Parent(s): 7889a7b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -189
app.py CHANGED
@@ -33,13 +33,13 @@ 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
- 5. **FILES:** If a file/image is provided, analyze it thoroughly.
43
  """
44
 
45
  # --- 🧬 MODEL & FILE HANDLING ---
@@ -104,22 +104,13 @@ def generate_with_retry(prompt, image_data=None, file_text=None, history_message
104
 
105
  return "⚠️ System Busy. Please try again."
106
 
107
- # --- PWA MANIFEST (FULL SCREEN CONFIG) ---
108
  @app.route('/manifest.json')
109
  def manifest():
110
  data = {
111
- "name": "Student's AI",
112
- "short_name": "StudentAI",
113
- "start_url": "/",
114
- "display": "standalone", # Idhu dhaan Full Screen-ku mukkiyam
115
- "orientation": "portrait",
116
- "background_color": "#09090b",
117
- "theme_color": "#09090b",
118
- "scope": "/",
119
- "icons": [
120
- {"src": "https://cdn-icons-png.flaticon.com/512/4712/4712035.png", "sizes": "192x192", "type": "image/png"},
121
- {"src": "https://cdn-icons-png.flaticon.com/512/4712/4712035.png", "sizes": "512x512", "type": "image/png"}
122
- ]
123
  }
124
  return Response(json.dumps(data), mimetype='application/json')
125
 
@@ -130,126 +121,165 @@ HTML_TEMPLATE = """
130
  <head>
131
  <meta charset="UTF-8">
132
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content">
133
-
134
  <meta name="mobile-web-app-capable" content="yes">
135
  <meta name="apple-mobile-web-app-capable" content="yes">
136
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
137
  <meta name="theme-color" content="#09090b">
138
  <link rel="manifest" href="/manifest.json">
139
-
140
  <title>Student's AI</title>
141
 
142
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
143
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
144
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
145
 
146
- <script>
147
- window.MathJax = { tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$']] }, svg: { fontCache: 'global' } };
148
- </script>
149
- <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
150
 
 
 
 
151
  <script type="module">
152
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
153
- mermaid.initialize({ startOnLoad: false, theme: 'dark' });
154
  window.mermaid = mermaid;
155
  </script>
156
 
157
  <style>
158
  :root {
159
- --bg-color: #09090b; --sidebar-bg: #000000; --card-bg: #18181b;
160
- --user-msg-bg: #27272a; --text-color: #e4e4e7; --code-bg: #0f0f11;
161
- --border-color: #27272a; --dim-text: #71717a;
162
  }
163
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
164
- 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; user-select: none; }
165
 
166
  #app-container { display: flex; flex-direction: column; height: 100dvh; position: relative; }
167
 
 
168
  header {
169
- height: 65px; padding: 0 20px;
170
  background: rgba(9,9,11, 0.98);
171
  border-bottom: 1px solid var(--border-color);
172
  display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; z-index: 50;
173
- padding-top: env(safe-area-inset-top); /* For iPhone Notch */
 
 
 
 
 
174
  }
175
- .app-title { font-size: 22px; font-weight: 700; background: linear-gradient(90deg, #fff, #a1a1aa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
 
176
 
 
177
  #sidebar {
178
- position: fixed; inset: 0; width: 280px; background: var(--sidebar-bg);
179
- border-right: 1px solid var(--border-color); transform: translateX(-100%);
180
- transition: transform 0.3s; z-index: 100; display: flex; flex-direction: column; padding: 20px;
181
- padding-top: max(20px, env(safe-area-inset-top));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
- #sidebar.open { transform: translateX(0); }
184
- .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 90; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
185
- .overlay.active { opacity: 1; pointer-events: auto; }
186
-
187
- .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; }
188
 
189
- #history-list { flex: 1; overflow-y: auto; }
190
  .history-item {
191
  display: flex; justify-content: space-between; align-items: center;
192
- padding: 12px; border-radius: 8px; cursor: pointer; color: #a1a1aa; font-size: 14px; margin-bottom: 5px;
 
193
  }
194
- .history-item:hover { background: var(--user-msg-bg); color: #fff; }
195
- .h-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px; }
196
- .h-actions { display: flex; gap: 8px; opacity: 0; transition: 0.2s; }
197
- .history-item:hover .h-actions { opacity: 1; }
198
- .h-icon { font-size: 12px; color: #71717a; }
199
- .h-icon:hover { color: #fff; }
 
 
200
 
201
- .brand-section { text-align: center; margin-top: auto; padding-top: 15px; border-top: 1px solid #222; padding-bottom: env(safe-area-inset-bottom); }
202
- .brand-name { font-family: 'Outfit', sans-serif; font-weight: 600; font-size: 11px; color: #52525b; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 10px; opacity: 0.8; }
203
- .logout-btn { color: #ef4444; cursor: pointer; font-size: 14px; font-weight: 500; }
 
204
 
 
205
  #chat-box {
206
  flex: 1; overflow-y: auto; padding: 20px 5%;
207
- display: flex; flex-direction: column; gap: 20px;
208
  -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain;
209
  }
210
 
211
- .msg { width: 100%; line-height: 1.6; font-size: 16px; opacity: 0; animation: fadeInstant 0.3s forwards; }
212
- @keyframes fadeInstant { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
213
 
214
  .user-msg { text-align: right; }
215
- .user-content { display: inline-block; background: var(--user-msg-bg); padding: 10px 18px; border-radius: 18px 18px 4px 18px; text-align: left; max-width: 85%; user-select: text; }
 
216
  .ai-msg { text-align: left; }
217
- .ai-content { user-select: text; }
218
  .ai-content strong { color: #fff; font-weight: 700; }
219
- .ai-content h1, .ai-content h2 { margin-top: 15px; color: #fff; font-weight: 700; }
220
 
221
- .mjx-chtml { background: #1e1e22; padding: 10px; border-radius: 8px; border: 1px solid #333; overflow-x: auto; margin: 10px 0; text-align: center; }
222
- 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; }
223
- code { font-family: 'JetBrains Mono', monospace; font-size: 14px; color: #a5d6ff; }
 
 
 
224
  .mermaid { background: #111; padding: 15px; border-radius: 10px; text-align: center; margin: 15px 0; }
225
 
226
- .msg-actions { margin-top: 5px; opacity: 0; transition: opacity 0.2s; font-size: 12px; display: flex; gap: 10px; }
 
227
  .user-msg .msg-actions { justify-content: flex-end; }
228
  .msg:hover .msg-actions { opacity: 1; }
229
- .action-btn { cursor: pointer; color: #71717a; display: flex; align-items: center; gap: 4px; }
230
- .action-btn:hover { color: #fff; }
231
 
232
- .input-wrapper { background: var(--bg-color); padding: 15px; border-top: 1px solid var(--border-color); flex-shrink: 0; z-index: 60; padding-bottom: max(15px, env(safe-area-inset-bottom)); }
 
233
  .input-container {
234
- max-width: 900px; margin: 0 auto; background: var(--card-bg);
235
- border: 1px solid var(--border-color); border-radius: 24px; padding: 8px 10px;
236
- display: flex; align-items: flex-end; gap: 8px;
237
  }
238
- 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; }
239
 
240
- .icon-btn { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; border: none; background: transparent; color: #a1a1aa; cursor: pointer; font-size: 16px; }
241
  .icon-btn:hover { background: #333; color: #fff; }
242
- .send-btn { background: #fff; color: #000; width: 36px; height: 36px; border-radius: 50%; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 16px; }
243
 
244
- #preview-area { max-width: 900px; margin: 0 auto 10px auto; display: none; background: #111; padding: 10px; border-radius: 12px; border: 1px solid #333; }
245
- .preview-item { display: flex; align-items: center; gap: 10px; }
246
- .preview-thumb { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; border: 1px solid #444; }
247
- .preview-file-icon { width: 40px; height: 40px; border-radius: 6px; background: #222; color: #fff; display: flex; align-items: center; justify-content: center; border: 1px solid #444; }
248
- .preview-name { font-size: 13px; color: #ccc; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
249
- .remove-preview { cursor: pointer; color: #ef4444; padding: 5px; }
 
 
 
 
 
 
 
 
 
 
 
250
 
 
251
  #login-overlay { position: fixed; inset: 0; background: #000; z-index: 2000; display: flex; align-items: center; justify-content: center; }
252
- .login-box { width: 90%; max-width: 350px; text-align: center; padding: 30px; border: 1px solid var(--border-color); border-radius: 20px; background: #0a0a0a; }
253
  </style>
254
  </head>
255
  <body>
@@ -257,46 +287,44 @@ HTML_TEMPLATE = """
257
  <div id="login-overlay">
258
  <div class="login-box">
259
  <h1 class="app-title" style="margin-bottom:10px;">Student's AI</h1>
260
- <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;">
261
- <button onclick="handleLogin()" style="width:100%; padding:12px; border-radius:10px; border:none; background:#fff; font-weight:700; cursor:pointer;">Start</button>
262
  </div>
263
  </div>
264
 
265
- <div id="app-container">
266
- <div class="overlay" id="overlay" onclick="toggleSidebar()"></div>
267
-
268
- <div id="sidebar">
269
- <div style="padding-bottom:20px; border-bottom:1px solid #333; margin-bottom:20px; display:flex; align-items:center; gap:12px;">
270
- <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>
271
- <span id="display-name" style="font-weight:700;">User</span>
272
- </div>
273
- <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>
274
- <input type="text" class="search-box" placeholder="Search..." onkeyup="filterHistory(this.value)">
275
-
276
- <div id="history-list"></div>
277
-
278
- <div class="brand-section">
279
- <div class="brand-name">Designed by Shirpi</div>
280
- <div class="logout-btn" onclick="handleLogout()">Log Out</div>
281
  </div>
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
 
283
 
 
284
  <header>
285
- <div style="display:flex; align-items:center; gap:15px;">
286
- <i class="fas fa-bars" style="cursor:pointer; color:#ccc;" onclick="toggleSidebar()"></i>
287
- <span class="app-title">Student's AI</span>
288
- </div>
289
- </header>
290
 
291
  <div id="chat-box"></div>
292
 
293
  <div class="input-wrapper">
294
  <div id="preview-area">
295
- <div class="preview-item">
296
  <div id="preview-visual"></div>
297
- <span id="preview-text" class="preview-name"></span>
298
- <i class="fas fa-times remove-preview" onclick="clearAttachment()"></i>
299
  </div>
 
300
  </div>
301
 
302
  <div class="input-container">
@@ -317,6 +345,7 @@ HTML_TEMPLATE = """
317
  let currentUser = null;
318
  let currentChatId = null;
319
  let currentAttachment = { type: null, data: null, name: null };
 
320
 
321
  function checkLogin() {
322
  const stored = localStorage.getItem("student_ai_user");
@@ -330,10 +359,14 @@ HTML_TEMPLATE = """
330
 
331
  function showApp() {
332
  document.getElementById("login-overlay").style.display = "none";
333
- document.getElementById("display-name").innerText = currentUser;
334
- document.getElementById("avatar").innerText = currentUser[0].toUpperCase();
335
  loadHistory();
336
- if(!currentChatId) newChat();
 
 
 
 
 
337
  }
338
 
339
  function resizeInput(el) {
@@ -348,17 +381,11 @@ HTML_TEMPLATE = """
348
  reader.onload = function(e) {
349
  const result = e.target.result;
350
  const isImage = file.type.startsWith('image/');
351
- currentAttachment = {
352
- type: isImage ? 'image' : 'file',
353
- data: isImage ? result : atob(result.split(',')[1]),
354
- name: file.name
355
- };
356
  const previewArea = document.getElementById('preview-area');
357
  const visual = document.getElementById('preview-visual');
358
- const name = document.getElementById('preview-text');
359
  previewArea.style.display = 'block';
360
- name.innerText = file.name;
361
- visual.innerHTML = isImage ? `<img src="${result}" class="preview-thumb">` : `<div class="preview-file-icon"><i class="fas fa-file-alt"></i></div>`;
362
  }
363
  reader.readAsDataURL(file);
364
  }
@@ -381,23 +408,18 @@ HTML_TEMPLATE = """
381
  if (!text && !currentAttachment.data) return;
382
 
383
  const chatBox = document.getElementById('chat-box');
384
-
385
  let attachHtml = '';
386
- if (currentAttachment.type === 'image') {
387
- attachHtml = `<br><img src="${currentAttachment.data}" style="max-height:100px; margin-top:5px; border-radius:5px;">`;
388
- } else if (currentAttachment.type === 'file') {
389
- attachHtml = `<br><div style="font-size:12px; color:#aaa; margin-top:5px;"><i class="fas fa-file"></i> ${currentAttachment.name}</div>`;
390
- }
391
 
392
  const userHtml = `
393
  <div class="msg user-msg">
394
  <div class="user-content">${text.replace(/</g, "&lt;")}${attachHtml}</div>
395
  <div class="msg-actions">
396
- <span class="action-btn" onclick="copyText('${text}')"><i class="fas fa-copy"></i></span>
397
- <span class="action-btn" onclick="editMessage('${text}')"><i class="fas fa-pen"></i></span>
398
  </div>
399
  </div>`;
400
-
401
  chatBox.insertAdjacentHTML('beforeend', userHtml);
402
 
403
  const promptText = text;
@@ -418,16 +440,9 @@ HTML_TEMPLATE = """
418
  const d = await r.json(); currentChatId = d.chat_id;
419
  loadHistory();
420
  }
421
-
422
  const res = await fetch('/chat', {
423
  method: 'POST', headers: {'Content-Type': 'application/json'},
424
- body: JSON.stringify({
425
- message: promptText,
426
- image: imgData,
427
- file_text: fileText,
428
- username: currentUser,
429
- chat_id: currentChatId
430
- })
431
  });
432
  const data = await res.json();
433
 
@@ -441,19 +456,17 @@ HTML_TEMPLATE = """
441
 
442
  aiDiv.insertAdjacentHTML('beforeend', `
443
  <div class="msg-actions">
444
- <span class="action-btn" onclick="copyAiResponse(this)"><i class="fas fa-copy"></i> Copy</span>
445
- <span class="action-btn" onclick="shareResponse(this)"><i class="fas fa-share-alt"></i> Share</span>
446
- <span class="action-btn" onclick="regenerate('${promptText}')"><i class="fas fa-redo"></i> Regen</span>
447
  </div>`);
448
 
449
  scrollToBottom();
450
  if(data.new_title) loadHistory();
451
-
452
- } catch (e) {
453
- document.getElementById(msgId).innerHTML = "⚠️ Error: " + e.message;
454
- }
455
  }
456
 
 
457
  function copyText(text) { navigator.clipboard.writeText(text); }
458
  function copyAiResponse(btn) {
459
  const text = btn.closest('.ai-msg').querySelector('.ai-content').innerText;
@@ -461,34 +474,28 @@ HTML_TEMPLATE = """
461
  }
462
  function shareResponse(btn) {
463
  const text = btn.closest('.ai-msg').querySelector('.ai-content').innerText;
464
- if (navigator.share) { navigator.share({ title: 'Student AI', text: text }); }
465
- else { navigator.clipboard.writeText(text); alert("Copied to clipboard!"); }
466
- }
467
- function editMessage(oldText) {
468
- document.getElementById('input').value = oldText;
469
- document.getElementById('input').focus();
470
  }
471
- function regenerate(text) {
472
- document.getElementById('input').value = text;
473
- send();
 
 
 
 
 
474
  }
475
-
 
476
  async function renameChat(cid, oldTitle) {
477
- const newTitle = prompt("Rename chat:", oldTitle);
478
- if(newTitle && newTitle !== oldTitle) {
479
- await fetch('/rename_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid, title:newTitle})});
480
- loadHistory();
481
- }
482
- event.stopPropagation();
483
  }
484
  async function deleteChat(cid) {
485
- if(confirm("Delete this chat?")) {
486
- await fetch('/delete_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})});
487
- if(currentChatId === cid) newChat();
488
- loadHistory();
489
- }
490
- event.stopPropagation();
491
  }
 
492
  async function loadHistory() {
493
  try {
494
  const res = await fetch('/get_history', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
@@ -498,11 +505,12 @@ HTML_TEMPLATE = """
498
  Object.keys(data.chats).reverse().forEach(cid => {
499
  const title = data.chats[cid].title || "New Chat";
500
  list.innerHTML += `
501
- <div class="history-item" onclick="loadChat('${cid}')">
 
502
  <span class="h-title">${title}</span>
503
  <div class="h-actions">
504
- <i class="fas fa-edit h-icon" onclick="renameChat('${cid}', '${title}')"></i>
505
- <i class="fas fa-trash h-icon" onclick="deleteChat('${cid}')"></i>
506
  </div>
507
  </div>`;
508
  });
@@ -510,41 +518,31 @@ HTML_TEMPLATE = """
510
  } catch(e) {}
511
  }
512
 
 
513
  async function typeWriter(element, markdownText) {
514
  element.innerHTML = marked.parse(markdownText);
 
515
  if (window.MathJax) await MathJax.typesetPromise([element]);
516
  if (window.mermaid) {
517
  const m = element.querySelectorAll('code.language-mermaid');
518
  m.forEach(c => { const d = document.createElement('div'); d.className='mermaid'; d.innerHTML=c.innerText; c.parentElement.replaceWith(d); });
519
  window.mermaid.init(undefined, element.querySelectorAll('.mermaid'));
520
  }
521
- element.style.opacity = 0;
522
- element.style.transition = 'opacity 0.5s';
523
  setTimeout(() => element.style.opacity = 1, 50);
524
  }
525
 
526
- function toggleSidebar() {
527
- document.getElementById('sidebar').classList.toggle('open');
528
- document.getElementById('overlay').classList.toggle('active');
529
- }
530
- function filterHistory(q) {
531
- const items = document.querySelectorAll('.history-item');
532
- items.forEach(i => i.style.display = i.innerText.toLowerCase().includes(q.toLowerCase()) ? 'flex' : 'none');
533
- }
534
  async function newChat() {
535
- currentChatId = null;
536
- document.getElementById('chat-box').innerHTML = "";
537
  const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
538
- const d = await r.json(); currentChatId = d.chat_id;
539
- loadHistory();
540
  document.getElementById('sidebar').classList.remove('open');
541
- document.getElementById('overlay').classList.remove('active');
542
  }
543
  async function loadChat(cid) {
544
- currentChatId = cid;
545
- const res = await fetch('/get_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})});
546
- const data = await res.json();
547
- const box = document.getElementById('chat-box'); box.innerHTML = "";
548
  data.messages.forEach(msg => {
549
  const isUser = msg.role === 'user';
550
  if(isUser) {
@@ -553,17 +551,12 @@ HTML_TEMPLATE = """
553
  const div = document.createElement('div'); div.className = 'msg ai-msg';
554
  div.innerHTML = `<div class="ai-content">${marked.parse(msg.content)}</div>`;
555
  box.appendChild(div);
 
556
  if(window.MathJax) MathJax.typesetPromise([div]);
557
- if(window.mermaid) {
558
- const m = div.querySelectorAll('code.language-mermaid');
559
- m.forEach(c => { const d = document.createElement('div'); d.className='mermaid'; d.innerHTML=c.innerText; c.parentElement.replaceWith(d); });
560
- window.mermaid.init(undefined, div.querySelectorAll('.mermaid'));
561
- }
562
- div.insertAdjacentHTML('beforeend', `<div class="msg-actions"><span class="action-btn" onclick="copyAiResponse(this)"><i class="fas fa-copy"></i> Copy</span></div>`);
563
  }
564
  });
565
  document.getElementById('sidebar').classList.remove('open');
566
- document.getElementById('overlay').classList.remove('active');
567
  box.scrollTop = box.scrollHeight;
568
  }
569
 
@@ -636,5 +629,15 @@ def chat():
636
  save_db(user_db)
637
  return jsonify({"response": reply, "new_title": new_title})
638
 
 
 
 
 
 
 
 
 
 
639
  if __name__ == '__main__':
640
  app.run(host='0.0.0.0', port=7860)
 
 
33
 
34
  # --- 🧠 SYSTEM INSTRUCTION ---
35
  SYSTEM_INSTRUCTION = """
36
+ ROLE: You are "Student's AI", a professional academic 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
+ 5. **CODE:** Use Python/Java/C++ blocks. Explain logic briefly.
43
  """
44
 
45
  # --- 🧬 MODEL & FILE HANDLING ---
 
104
 
105
  return "⚠️ System Busy. Please try again."
106
 
107
+ # --- PWA MANIFEST ---
108
  @app.route('/manifest.json')
109
  def manifest():
110
  data = {
111
+ "name": "Student's AI", "short_name": "StudentAI", "start_url": "/", "display": "standalone",
112
+ "orientation": "portrait", "background_color": "#09090b", "theme_color": "#09090b",
113
+ "icons": [{"src": "https://cdn-icons-png.flaticon.com/512/4712/4712035.png", "sizes": "192x192", "type": "image/png"}]
 
 
 
 
 
 
 
 
 
114
  }
115
  return Response(json.dumps(data), mimetype='application/json')
116
 
 
121
  <head>
122
  <meta charset="UTF-8">
123
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content">
 
124
  <meta name="mobile-web-app-capable" content="yes">
125
  <meta name="apple-mobile-web-app-capable" content="yes">
 
126
  <meta name="theme-color" content="#09090b">
127
  <link rel="manifest" href="/manifest.json">
 
128
  <title>Student's AI</title>
129
 
130
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
131
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 
132
 
133
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
134
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
 
 
135
 
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
+ body, html { margin: 0; padding: 0; height: 100%; width: 100%; background: var(--bg); color: var(--text); font-family: 'Outfit', sans-serif; overflow: hidden; user-select: none; font-size: 17px; }
152
 
153
  #app-container { display: flex; flex-direction: column; height: 100dvh; position: relative; }
154
 
155
+ /* HEADER (Professional) */
156
  header {
157
+ height: 70px; padding: 0 20px;
158
  background: rgba(9,9,11, 0.98);
159
  border-bottom: 1px solid var(--border-color);
160
  display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; z-index: 50;
161
+ padding-top: env(safe-area-inset-top);
162
+ }
163
+ .menu-btn {
164
+ width: 40px; height: 40px; border-radius: 50%; border: 1px solid #333;
165
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
166
+ transition: 0.2s; color: #fff;
167
  }
168
+ .menu-btn:active { transform: scale(0.95); background: #222; }
169
+ .app-title { font-size: 24px; font-weight: 800; letter-spacing: -0.5px; color: #fff; }
170
 
171
+ /* SIDEBAR (Top to Bottom Animation) */
172
  #sidebar {
173
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
174
+ background: var(--bg); z-index: 100;
175
+ display: flex; flex-direction: column; padding: 25px;
176
+ padding-top: calc(70px + env(safe-area-inset-top));
177
+ transform: translateY(-100%); /* Start hidden above */
178
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
179
+ }
180
+ #sidebar.open { transform: translateY(0); }
181
+
182
+ /* Sidebar Elements */
183
+ .user-info { margin-bottom: 30px; font-size: 20px; font-weight: 700; color: #fff; display: flex; align-items: center; gap: 15px; }
184
+ .new-chat-btn {
185
+ width: 100%; padding: 15px; background: #fff; color: #000; border: none;
186
+ border-radius: 12px; font-weight: 700; font-size: 16px; cursor: pointer; margin-bottom: 25px;
187
+ display: flex; justify-content: center; align-items: center;
188
+ }
189
+ .new-chat-btn:active { transform: scale(0.98); opacity: 0.9; }
190
+
191
+ .history-label { color: var(--dim); font-size: 13px; font-weight: 600; margin-bottom: 10px; letter-spacing: 1px; text-transform: uppercase; }
192
+ #history-list {
193
+ flex: 1; overflow-y: auto; background: var(--card); border-radius: 16px;
194
+ padding: 10px; border: 1px solid var(--border);
195
  }
 
 
 
 
 
196
 
 
197
  .history-item {
198
  display: flex; justify-content: space-between; align-items: center;
199
+ padding: 15px; border-bottom: 1px solid #222; cursor: pointer; color: #a1a1aa;
200
+ font-size: 15px; transition: 0.2s;
201
  }
202
+ .history-item:last-child { border-bottom: none; }
203
+ .history-item:active { background: #222; color: #fff; }
204
+ .h-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; }
205
+
206
+ /* Action Logos (Hidden by default, shown on long press/active) */
207
+ .h-actions { display: none; gap: 15px; }
208
+ .history-item.active-mode .h-actions { display: flex; }
209
+ .h-icon { font-size: 16px; color: #fff; padding: 5px; }
210
 
211
+ /* BRANDING */
212
+ .brand-section { text-align: center; margin-top: 20px; padding-bottom: env(safe-area-inset-bottom); }
213
+ .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; }
214
+ .logout-btn { color: #ef4444; cursor: pointer; font-size: 15px; font-weight: 600; padding: 10px; }
215
 
216
+ /* CHAT AREA */
217
  #chat-box {
218
  flex: 1; overflow-y: auto; padding: 20px 5%;
219
+ display: flex; flex-direction: column; gap: 30px;
220
  -webkit-overflow-scrolling: touch; overscroll-behavior-y: contain;
221
  }
222
 
223
+ .msg { width: 100%; line-height: 1.7; font-size: 17px; opacity: 0; animation: fadeInstant 0.3s forwards; }
224
+ @keyframes fadeInstant { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
225
 
226
  .user-msg { text-align: right; }
227
+ .user-content { display: inline-block; background: var(--user-msg); padding: 12px 20px; border-radius: 20px 20px 4px 20px; text-align: left; max-width: 85%; user-select: text; color: #fff; }
228
+
229
  .ai-msg { text-align: left; }
230
+ .ai-content { user-select: text; color: #d4d4d8; }
231
  .ai-content strong { color: #fff; font-weight: 700; }
232
+ .ai-content h1, .ai-content h2, .ai-content h3 { margin-top: 20px; color: #fff; font-weight: 700; }
233
 
234
+ /* Code Blocks (Colored) */
235
+ pre { background: #1e1e1e !important; border-radius: 12px; padding: 15px; overflow-x: auto; margin: 15px 0; border: 1px solid #333; }
236
+ code { font-family: 'JetBrains Mono', monospace; font-size: 14px; }
237
+
238
+ /* Math & Diagrams */
239
+ .mjx-chtml { background: #18181b; padding: 10px; border-radius: 8px; border: 1px solid #333; overflow-x: auto; margin: 10px 0; text-align: center; }
240
  .mermaid { background: #111; padding: 15px; border-radius: 10px; text-align: center; margin: 15px 0; }
241
 
242
+ /* Action LOGOS (Big & Spaced) */
243
+ .msg-actions { margin-top: 10px; opacity: 0; transition: opacity 0.2s; display: flex; gap: 20px; align-items: center; }
244
  .user-msg .msg-actions { justify-content: flex-end; }
245
  .msg:hover .msg-actions { opacity: 1; }
246
+ .action-icon { cursor: pointer; color: var(--dim); font-size: 18px; transition: 0.2s; }
247
+ .action-icon:hover { color: #fff; transform: scale(1.1); }
248
 
249
+ /* INPUT AREA */
250
+ .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)); }
251
  .input-container {
252
+ max-width: 900px; margin: 0 auto; background: var(--card);
253
+ border: 1px solid var(--border); border-radius: 24px; padding: 8px 12px;
254
+ display: flex; align-items: flex-end; gap: 12px;
255
  }
256
+ 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; }
257
 
258
+ .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; }
259
  .icon-btn:hover { background: #333; color: #fff; }
260
+ .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; }
261
 
262
+ /* SMALL CURVED PREVIEW */
263
+ #preview-area {
264
+ position: absolute; bottom: 85px; left: 20px;
265
+ display: none; z-index: 70;
266
+ }
267
+ .preview-box {
268
+ width: 60px; height: 60px; border-radius: 12px; border: 2px solid #fff;
269
+ background: #222; overflow: hidden; position: relative;
270
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
271
+ }
272
+ .preview-img { width: 100%; height: 100%; object-fit: cover; }
273
+ .preview-file-icon { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #fff; }
274
+ .remove-preview {
275
+ position: absolute; top: -8px; right: -8px; background: red; color: white;
276
+ border-radius: 50%; width: 20px; height: 20px; font-size: 12px;
277
+ cursor: pointer; border: none; display: flex; align-items: center; justify-content: center;
278
+ }
279
 
280
+ /* LOGIN */
281
  #login-overlay { position: fixed; inset: 0; background: #000; z-index: 2000; display: flex; align-items: center; justify-content: center; }
282
+ .login-box { width: 90%; max-width: 350px; text-align: center; padding: 40px; border: 1px solid var(--border); border-radius: 20px; background: #0a0a0a; }
283
  </style>
284
  </head>
285
  <body>
 
287
  <div id="login-overlay">
288
  <div class="login-box">
289
  <h1 class="app-title" style="margin-bottom:10px;">Student's AI</h1>
290
+ <input type="text" id="username-input" placeholder="Your Name" style="width:100%; padding:15px; border-radius:12px; border:1px solid #333; background:#111; color:#fff; text-align:center; outline:none; margin-bottom:20px; font-size: 16px;">
291
+ <button onclick="handleLogin()" style="width:100%; padding:15px; border-radius:12px; border:none; background:#fff; font-weight:800; cursor:pointer; font-size: 16px;">Start Learning</button>
292
  </div>
293
  </div>
294
 
295
+ <div id="sidebar">
296
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
297
+ <div class="user-info">
298
+ <span id="display-name">User</span>
 
 
 
 
 
 
 
 
 
 
 
 
299
  </div>
300
+ <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-times"></i></div>
301
+ </div>
302
+
303
+ <button class="new-chat-btn" onclick="newChat()">New Chat</button>
304
+
305
+ <div class="history-label">Chat History</div>
306
+ <div id="history-list"></div>
307
+
308
+ <div class="brand-section">
309
+ <div class="brand-name">Designed by Shirpi</div>
310
+ <div class="logout-btn" onclick="handleLogout()">Log Out</div>
311
  </div>
312
+ </div>
313
 
314
+ <div id="app-container">
315
  <header>
316
+ <div class="menu-btn" onclick="toggleSidebar()"><i class="fas fa-bars"></i></div>
317
+ <span class="app-title">Student's AI</span>
318
+ <div style="width:40px;"></div> </header>
 
 
319
 
320
  <div id="chat-box"></div>
321
 
322
  <div class="input-wrapper">
323
  <div id="preview-area">
324
+ <div class="preview-box">
325
  <div id="preview-visual"></div>
 
 
326
  </div>
327
+ <button class="remove-preview" onclick="clearAttachment()">×</button>
328
  </div>
329
 
330
  <div class="input-container">
 
345
  let currentUser = null;
346
  let currentChatId = null;
347
  let currentAttachment = { type: null, data: null, name: null };
348
+ let longPressTimer;
349
 
350
  function checkLogin() {
351
  const stored = localStorage.getItem("student_ai_user");
 
359
 
360
  function showApp() {
361
  document.getElementById("login-overlay").style.display = "none";
362
+ document.getElementById("display-name").innerText = "Hi " + currentUser;
 
363
  loadHistory();
364
+ if(!currentChatId) {
365
+ const box = document.getElementById("chat-box");
366
+ if(box.innerHTML === "") {
367
+ box.innerHTML = `<div class="msg ai-msg"><div class="ai-content"><h1>Hi ${currentUser},</h1><p>How can I help you with your <strong>Studies</strong> today?</p></div></div>`;
368
+ }
369
+ }
370
  }
371
 
372
  function resizeInput(el) {
 
381
  reader.onload = function(e) {
382
  const result = e.target.result;
383
  const isImage = file.type.startsWith('image/');
384
+ currentAttachment = { type: isImage ? 'image' : 'file', data: isImage ? result : atob(result.split(',')[1]), name: file.name };
 
 
 
 
385
  const previewArea = document.getElementById('preview-area');
386
  const visual = document.getElementById('preview-visual');
 
387
  previewArea.style.display = 'block';
388
+ visual.innerHTML = isImage ? `<img src="${result}" class="preview-img">` : `<div class="preview-file-icon"><i class="fas fa-file-alt"></i></div>`;
 
389
  }
390
  reader.readAsDataURL(file);
391
  }
 
408
  if (!text && !currentAttachment.data) return;
409
 
410
  const chatBox = document.getElementById('chat-box');
 
411
  let attachHtml = '';
412
+ if (currentAttachment.type === 'image') attachHtml = `<br><img src="${currentAttachment.data}" style="max-height:100px; margin-top:10px; border-radius:8px;">`;
413
+ if (currentAttachment.type === 'file') attachHtml = `<br><small>📄 ${currentAttachment.name}</small>`;
 
 
 
414
 
415
  const userHtml = `
416
  <div class="msg user-msg">
417
  <div class="user-content">${text.replace(/</g, "&lt;")}${attachHtml}</div>
418
  <div class="msg-actions">
419
+ <i class="fas fa-copy action-icon" onclick="copyText('${text}')"></i>
420
+ <i class="fas fa-pen action-icon" onclick="editMessage('${text}')"></i>
421
  </div>
422
  </div>`;
 
423
  chatBox.insertAdjacentHTML('beforeend', userHtml);
424
 
425
  const promptText = text;
 
440
  const d = await r.json(); currentChatId = d.chat_id;
441
  loadHistory();
442
  }
 
443
  const res = await fetch('/chat', {
444
  method: 'POST', headers: {'Content-Type': 'application/json'},
445
+ body: JSON.stringify({ message: promptText, image: imgData, file_text: fileText, username: currentUser, chat_id: currentChatId })
 
 
 
 
 
 
446
  });
447
  const data = await res.json();
448
 
 
456
 
457
  aiDiv.insertAdjacentHTML('beforeend', `
458
  <div class="msg-actions">
459
+ <i class="fas fa-copy action-icon" onclick="copyAiResponse(this)"></i>
460
+ <i class="fas fa-share-alt action-icon" onclick="shareResponse(this)"></i>
461
+ <i class="fas fa-redo action-icon" onclick="regenerate('${promptText}')"></i>
462
  </div>`);
463
 
464
  scrollToBottom();
465
  if(data.new_title) loadHistory();
466
+ } catch (e) { document.getElementById(msgId).innerHTML = "⚠️ Error: " + e.message; }
 
 
 
467
  }
468
 
469
+ // --- ACTIONS ---
470
  function copyText(text) { navigator.clipboard.writeText(text); }
471
  function copyAiResponse(btn) {
472
  const text = btn.closest('.ai-msg').querySelector('.ai-content').innerText;
 
474
  }
475
  function shareResponse(btn) {
476
  const text = btn.closest('.ai-msg').querySelector('.ai-content').innerText;
477
+ if (navigator.share) navigator.share({ title: 'Student AI', text: text });
478
+ else navigator.clipboard.writeText(text);
 
 
 
 
479
  }
480
+ function editMessage(oldText) { document.getElementById('input').value = oldText; document.getElementById('input').focus(); }
481
+ function regenerate(text) { document.getElementById('input').value = text; send(); }
482
+
483
+ // --- HISTORY (LONG PRESS) ---
484
+ function handleHistoryTouchStart(e, cid, title) {
485
+ longPressTimer = setTimeout(() => {
486
+ e.target.closest('.history-item').classList.add('active-mode');
487
+ }, 600); // 0.6s hold
488
  }
489
+ function handleHistoryTouchEnd(e) { clearTimeout(longPressTimer); }
490
+
491
  async function renameChat(cid, oldTitle) {
492
+ const newTitle = prompt("Rename:", oldTitle);
493
+ if(newTitle) { await fetch('/rename_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid, title:newTitle})}); loadHistory(); }
 
 
 
 
494
  }
495
  async function deleteChat(cid) {
496
+ if(confirm("Delete?")) { await fetch('/delete_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})}); if(currentChatId === cid) newChat(); loadHistory(); }
 
 
 
 
 
497
  }
498
+
499
  async function loadHistory() {
500
  try {
501
  const res = await fetch('/get_history', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
 
505
  Object.keys(data.chats).reverse().forEach(cid => {
506
  const title = data.chats[cid].title || "New Chat";
507
  list.innerHTML += `
508
+ <div class="history-item" onclick="loadChat('${cid}')" oncontextmenu="return false;"
509
+ ontouchstart="handleHistoryTouchStart(event, '${cid}', '${title}')" ontouchend="handleHistoryTouchEnd(event)">
510
  <span class="h-title">${title}</span>
511
  <div class="h-actions">
512
+ <i class="fas fa-pen h-icon" onclick="event.stopPropagation(); renameChat('${cid}', '${title}')"></i>
513
+ <i class="fas fa-trash h-icon" onclick="event.stopPropagation(); deleteChat('${cid}')"></i>
514
  </div>
515
  </div>`;
516
  });
 
518
  } catch(e) {}
519
  }
520
 
521
+ // --- ANIMATION & HIGHLIGHT ---
522
  async function typeWriter(element, markdownText) {
523
  element.innerHTML = marked.parse(markdownText);
524
+ hljs.highlightAll(); // Highlight Code
525
  if (window.MathJax) await MathJax.typesetPromise([element]);
526
  if (window.mermaid) {
527
  const m = element.querySelectorAll('code.language-mermaid');
528
  m.forEach(c => { const d = document.createElement('div'); d.className='mermaid'; d.innerHTML=c.innerText; c.parentElement.replaceWith(d); });
529
  window.mermaid.init(undefined, element.querySelectorAll('.mermaid'));
530
  }
531
+ element.style.opacity = 0; element.style.transition = 'opacity 0.4s';
 
532
  setTimeout(() => element.style.opacity = 1, 50);
533
  }
534
 
535
+ function toggleSidebar() { document.getElementById('sidebar').classList.toggle('open'); }
 
 
 
 
 
 
 
536
  async function newChat() {
537
+ currentChatId = null; document.getElementById('chat-box').innerHTML = "";
 
538
  const r = await fetch('/new_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser})});
539
+ const d = await r.json(); currentChatId = d.chat_id; loadHistory();
 
540
  document.getElementById('sidebar').classList.remove('open');
541
+ showApp(); // Reset Intro
542
  }
543
  async function loadChat(cid) {
544
+ currentChatId = cid; const res = await fetch('/get_chat', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:currentUser, chat_id:cid})});
545
+ const data = await res.json(); const box = document.getElementById('chat-box'); box.innerHTML = "";
 
 
546
  data.messages.forEach(msg => {
547
  const isUser = msg.role === 'user';
548
  if(isUser) {
 
551
  const div = document.createElement('div'); div.className = 'msg ai-msg';
552
  div.innerHTML = `<div class="ai-content">${marked.parse(msg.content)}</div>`;
553
  box.appendChild(div);
554
+ hljs.highlightAll();
555
  if(window.MathJax) MathJax.typesetPromise([div]);
556
+ div.insertAdjacentHTML('beforeend', `<div class="msg-actions"><i class="fas fa-copy action-icon" onclick="copyAiResponse(this)"></i></div>`);
 
 
 
 
 
557
  }
558
  });
559
  document.getElementById('sidebar').classList.remove('open');
 
560
  box.scrollTop = box.scrollHeight;
561
  }
562
 
 
629
  save_db(user_db)
630
  return jsonify({"response": reply, "new_title": new_title})
631
 
632
+ @app.route('/manifest.json')
633
+ def manifest():
634
+ data = {
635
+ "name": "Student's AI", "short_name": "StudentAI", "start_url": "/", "display": "standalone",
636
+ "orientation": "portrait", "background_color": "#09090b", "theme_color": "#09090b",
637
+ "icons": [{"src": "https://cdn-icons-png.flaticon.com/512/4712/4712035.png", "sizes": "192x192", "type": "image/png"}]
638
+ }
639
+ return Response(json.dumps(data), mimetype='application/json')
640
+
641
  if __name__ == '__main__':
642
  app.run(host='0.0.0.0', port=7860)
643
+