Sazid2 commited on
Commit
9fac04c
·
verified ·
1 Parent(s): a43e3a8

Create app.py19

Browse files
Files changed (1) hide show
  1. app.py19 +2200 -0
app.py19 ADDED
@@ -0,0 +1,2200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import os
4
+ from datetime import datetime, timedelta
5
+ import re
6
+ import hashlib
7
+ import json
8
+ import time
9
+
10
+ # ===============================
11
+ # SUPABASE CACHE CLASS - FIXED VERSION
12
+ # ===============================
13
+ class SupabaseCache:
14
+ def __init__(self, ttl_days=7):
15
+ """
16
+ Supabase-based cache for multi-user, persistent storage
17
+ """
18
+ self.ttl_days = ttl_days
19
+
20
+ # Get Supabase credentials from environment
21
+ self.supabase_url = os.environ.get("SUPABASE_URL", "")
22
+ self.supabase_key = os.environ.get("SUPABASE_KEY", "")
23
+
24
+ # Initialize Supabase client
25
+ self.supabase = None
26
+ self._init_supabase()
27
+
28
+ # In-memory fallback cache
29
+ self.memory_cache = {}
30
+ self.max_memory_entries = 100
31
+
32
+ def _init_supabase(self):
33
+ """Initialize Supabase client"""
34
+ if self.supabase_url and self.supabase_key:
35
+ try:
36
+ from supabase import create_client
37
+ self.supabase = create_client(self.supabase_url, self.supabase_key)
38
+ # Test connection
39
+ self.supabase.table("seba_cache").select("count", count="exact").limit(1).execute()
40
+ except ImportError:
41
+ # supabase-py not installed
42
+ self.supabase = None
43
+ except Exception as e:
44
+ # Connection failed
45
+ print(f"Supabase connection error: {e}")
46
+ self.supabase = None
47
+ else:
48
+ self.supabase = None
49
+
50
+ def get(self, cache_key):
51
+ """Get cached answer - try Supabase first, then memory"""
52
+ # First check memory cache (fastest)
53
+ if cache_key in self.memory_cache:
54
+ entry = self.memory_cache[cache_key]
55
+ if self._is_valid(entry):
56
+ entry['access_count'] = entry.get('access_count', 0) + 1
57
+ entry['last_accessed'] = datetime.now().isoformat()
58
+ return entry
59
+
60
+ # Try Supabase if available - FIXED: Removed TTL filter from query
61
+ if self.supabase:
62
+ try:
63
+ # Get entry without TTL filter - we'll check TTL in Python
64
+ response = self.supabase.table("seba_cache") \
65
+ .select("*") \
66
+ .eq("key_hash", cache_key) \
67
+ .execute()
68
+
69
+ if response.data and len(response.data) > 0:
70
+ entry = response.data[0]
71
+
72
+ # Check if entry is expired
73
+ created_at_str = entry.get('created_at')
74
+ is_expired = False
75
+
76
+ if created_at_str:
77
+ try:
78
+ # Parse the timestamp
79
+ if 'Z' in created_at_str:
80
+ created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
81
+ else:
82
+ created_at = datetime.fromisoformat(created_at_str)
83
+
84
+ # Check TTL
85
+ if (datetime.now() - created_at).days >= self.ttl_days:
86
+ # Entry expired, delete it
87
+ is_expired = True
88
+ try:
89
+ self.supabase.table("seba_cache") \
90
+ .delete() \
91
+ .eq("key_hash", cache_key) \
92
+ .execute()
93
+ except:
94
+ pass
95
+ except Exception:
96
+ # If we can't parse date, assume not expired
97
+ pass
98
+
99
+ if not is_expired:
100
+ # Convert to standard format
101
+ cached_data = {
102
+ 'answer': entry['answer'],
103
+ 'tokens': entry.get('tokens', 0),
104
+ 'subject': entry.get('subject', ''),
105
+ 'chapter': entry.get('chapter', ''),
106
+ 'question': entry.get('question', ''),
107
+ 'access_count': entry.get('access_count', 0) + 1,
108
+ 'created_at': entry.get('created_at'),
109
+ 'last_accessed': datetime.now().isoformat()
110
+ }
111
+
112
+ # Update access count in Supabase
113
+ try:
114
+ self.supabase.table("seba_cache") \
115
+ .update({
116
+ "last_accessed": datetime.now().isoformat(),
117
+ "access_count": entry.get('access_count', 0) + 1
118
+ }) \
119
+ .eq("key_hash", cache_key) \
120
+ .execute()
121
+ except:
122
+ pass
123
+
124
+ # Store in memory cache for faster access
125
+ self.memory_cache[cache_key] = cached_data
126
+
127
+ # Limit memory cache size
128
+ if len(self.memory_cache) > self.max_memory_entries:
129
+ oldest_key = min(self.memory_cache.keys(),
130
+ key=lambda k: self.memory_cache[k].get('last_accessed', ''))
131
+ del self.memory_cache[oldest_key]
132
+
133
+ return cached_data
134
+ except Exception as e:
135
+ # Silently fail - fall back to memory cache
136
+ print(f"Supabase get error: {e}")
137
+ pass
138
+
139
+ return None
140
+
141
+ def set(self, cache_key, data):
142
+ """Store answer in both Supabase and memory cache"""
143
+ # Prepare data
144
+ cache_data = {
145
+ 'answer': data['answer'],
146
+ 'tokens': data.get('tokens', 0),
147
+ 'subject': data.get('subject', ''),
148
+ 'chapter': data.get('chapter', ''),
149
+ 'question': data.get('question', '')[:200],
150
+ 'access_count': 1,
151
+ 'created_at': datetime.now().isoformat(),
152
+ 'last_accessed': datetime.now().isoformat()
153
+ }
154
+
155
+ # Store in memory cache
156
+ self.memory_cache[cache_key] = cache_data
157
+
158
+ # Limit memory cache size
159
+ if len(self.memory_cache) > self.max_memory_entries:
160
+ oldest_key = min(self.memory_cache.keys(),
161
+ key=lambda k: self.memory_cache[k].get('last_accessed', ''))
162
+ del self.memory_cache[oldest_key]
163
+
164
+ # Store in Supabase if available
165
+ if self.supabase:
166
+ try:
167
+ self.supabase.table("seba_cache").upsert({
168
+ "key_hash": cache_key,
169
+ "question": cache_data['question'],
170
+ "answer": cache_data['answer'],
171
+ "subject": cache_data['subject'],
172
+ "chapter": cache_data['chapter'],
173
+ "tokens": cache_data['tokens'],
174
+ "created_at": cache_data['created_at'],
175
+ "last_accessed": cache_data['last_accessed'],
176
+ "access_count": cache_data['access_count']
177
+ }).execute()
178
+ except Exception as e:
179
+ # Silently fail - at least we have memory cache
180
+ print(f"Supabase set error: {e}")
181
+ pass
182
+
183
+ def _is_valid(self, entry):
184
+ """Check if cache entry is not expired"""
185
+ created_at = entry.get('created_at')
186
+ if isinstance(created_at, str):
187
+ try:
188
+ if 'Z' in created_at:
189
+ created_at = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
190
+ else:
191
+ created_at = datetime.fromisoformat(created_at)
192
+ except:
193
+ return True # If we can't parse, assume valid
194
+
195
+ if created_at and (datetime.now() - created_at).days < self.ttl_days:
196
+ return True
197
+ return False
198
+
199
+ def clear_expired(self):
200
+ """Clear expired entries from memory cache"""
201
+ expired_keys = []
202
+ for key, entry in self.memory_cache.items():
203
+ if not self._is_valid(entry):
204
+ expired_keys.append(key)
205
+
206
+ for key in expired_keys:
207
+ del self.memory_cache[key]
208
+
209
+ return len(expired_keys)
210
+
211
+ def clear_all(self):
212
+ """Clear all cache entries"""
213
+ self.memory_cache = {}
214
+
215
+ # Also clear Supabase cache if available
216
+ if self.supabase:
217
+ try:
218
+ # Delete entries older than 1 day (safer than deleting all)
219
+ self.supabase.table("seba_cache") \
220
+ .delete() \
221
+ .lt("created_at", f"now() - interval '1 day'") \
222
+ .execute()
223
+ except:
224
+ pass
225
+
226
+ def get_stats(self):
227
+ """Get cache statistics"""
228
+ # Memory cache stats
229
+ memory_entries = len(self.memory_cache)
230
+ memory_tokens = sum(entry.get('tokens', 0) for entry in self.memory_cache.values())
231
+
232
+ # Try to get Supabase stats
233
+ supabase_entries = 0
234
+ supabase_tokens = 0
235
+
236
+ if self.supabase:
237
+ try:
238
+ # Get count from Supabase
239
+ response = self.supabase.table("seba_cache") \
240
+ .select("count", count="exact") \
241
+ .execute()
242
+
243
+ supabase_entries = response.count or 0
244
+
245
+ # Get total tokens (might be heavy, so approximate)
246
+ if supabase_entries > 0:
247
+ response = self.supabase.table("seba_cache") \
248
+ .select("tokens") \
249
+ .limit(100) \
250
+ .execute()
251
+
252
+ supabase_tokens = sum(entry.get('tokens', 0) for entry in response.data)
253
+ except:
254
+ pass
255
+
256
+ total_entries = memory_entries + supabase_entries
257
+ total_tokens = memory_tokens + supabase_tokens
258
+
259
+ return {
260
+ 'total_entries': total_entries,
261
+ 'memory_entries': memory_entries,
262
+ 'supabase_entries': supabase_entries,
263
+ 'total_saved_tokens': total_tokens,
264
+ 'ttl_days': self.ttl_days,
265
+ 'storage_mode': 'Supabase + Memory' if self.supabase else 'Memory Only',
266
+ 'supabase_connected': self.supabase is not None
267
+ }
268
+
269
+ # ===============================
270
+ # SUPABASE SUBSCRIPTION MANAGER
271
+ # ===============================
272
+ class SupabaseSubscriptionManager:
273
+ def __init__(self, supabase_client):
274
+ self.supabase = supabase_client
275
+
276
+ def create_user(self, user_id, email=None, phone=None):
277
+ """Create a new user with free plan"""
278
+ try:
279
+ # Create user
280
+ user_data = {
281
+ "user_id": user_id,
282
+ "email": email,
283
+ "phone": phone,
284
+ "is_active": True
285
+ }
286
+
287
+ # Remove None values
288
+ user_data = {k: v for k, v in user_data.items() if v is not None}
289
+
290
+ # Upsert user (insert or update if exists)
291
+ response = self.supabase.table("seba_users").upsert(user_data).execute()
292
+
293
+ # Create free subscription (30 questions lifetime)
294
+ subscription_data = {
295
+ "user_id": user_id,
296
+ "plan_type": "free",
297
+ "price_paid": 0.00,
298
+ "start_date": datetime.now().isoformat(),
299
+ "end_date": (datetime.now() + timedelta(days=3650)).isoformat(), # 10 years
300
+ "total_questions": 30,
301
+ "questions_used": 0,
302
+ "daily_limit": 1,
303
+ "is_active": True
304
+ }
305
+
306
+ response = self.supabase.table("seba_subscriptions").upsert(subscription_data).execute()
307
+
308
+ return True
309
+ except Exception as e:
310
+ print(f"Error creating user: {e}")
311
+ return False
312
+
313
+ def get_user_plan(self, user_id):
314
+ """Get user's current subscription plan"""
315
+ try:
316
+ response = self.supabase.table("seba_subscriptions") \
317
+ .select("*") \
318
+ .eq("user_id", user_id) \
319
+ .eq("is_active", True) \
320
+ .gte("end_date", datetime.now().isoformat()) \
321
+ .order("start_date", desc=True) \
322
+ .limit(1) \
323
+ .execute()
324
+
325
+ if response.data and len(response.data) > 0:
326
+ plan = response.data[0]
327
+ return {
328
+ 'plan_type': plan['plan_type'],
329
+ 'total_questions': plan['total_questions'],
330
+ 'questions_used': plan['questions_used'],
331
+ 'daily_limit': plan['daily_limit'],
332
+ 'end_date': plan['end_date'],
333
+ 'questions_remaining': plan['total_questions'] - plan['questions_used'],
334
+ 'is_active': True,
335
+ 'plan_id': plan['id']
336
+ }
337
+
338
+ # No active subscription found
339
+ return None
340
+
341
+ except Exception as e:
342
+ print(f"Error getting user plan: {e}")
343
+ return None
344
+
345
+ def get_or_create_user_plan(self, user_id, email=None, phone=None):
346
+ """Get user plan, create if doesn't exist"""
347
+ plan = self.get_user_plan(user_id)
348
+
349
+ if plan is None:
350
+ # Create user with free plan
351
+ self.create_user(user_id, email, phone)
352
+ plan = self.get_user_plan(user_id)
353
+
354
+ return plan
355
+
356
+ def can_ask_question(self, user_id):
357
+ """Check if user can ask a new question"""
358
+ try:
359
+ # Get user's plan
360
+ plan = self.get_user_plan(user_id)
361
+
362
+ if not plan:
363
+ return False, "No subscription found. Please sign up."
364
+
365
+ # Check if subscription expired
366
+ try:
367
+ end_date_str = plan['end_date']
368
+ # Handle different timestamp formats
369
+ if 'Z' in end_date_str:
370
+ end_date = datetime.fromisoformat(end_date_str.replace('Z', '+00:00'))
371
+ else:
372
+ end_date = datetime.fromisoformat(end_date_str)
373
+
374
+ if datetime.now() > end_date:
375
+ return False, "Subscription expired. Please renew."
376
+ except Exception as date_error:
377
+ print(f"Date parsing error: {date_error}")
378
+ # Continue if date parsing fails
379
+
380
+ # Check monthly/lifetime quota
381
+ if plan['questions_used'] >= plan['total_questions']:
382
+ if plan['plan_type'] == 'free':
383
+ return False, "Free quota exhausted. Please upgrade to Pro."
384
+ else:
385
+ return False, f"Monthly quota ({plan['total_questions']}) exhausted. Renew or upgrade."
386
+
387
+ # For all plans, check daily limit
388
+ today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
389
+ today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=999999)
390
+
391
+ # Count today's non-cached questions
392
+ response = self.supabase.table("seba_usage_logs") \
393
+ .select("id", count="exact") \
394
+ .eq("user_id", user_id) \
395
+ .eq("cached", False) \
396
+ .gte("timestamp", today_start.isoformat()) \
397
+ .lt("timestamp", today_end.isoformat()) \
398
+ .execute()
399
+
400
+ today_used = response.count or 0
401
+
402
+ if today_used >= plan['daily_limit']:
403
+ return False, f"Daily limit ({plan['daily_limit']}) reached. Try again tomorrow."
404
+
405
+ return True, "Can ask question"
406
+
407
+ except Exception as e:
408
+ print(f"Error checking question allowance: {e}")
409
+ # For now, allow questions if check fails (fail-open)
410
+ return True, "Proceeding with question..."
411
+
412
+ def log_question(self, user_id, question_hash, subject="", chapter="", tokens_used=0, cached=False):
413
+ """Log a question usage"""
414
+ try:
415
+ # Get current plan
416
+ plan = self.get_user_plan(user_id)
417
+
418
+ if not plan:
419
+ plan_type = "free"
420
+ else:
421
+ plan_type = plan['plan_type']
422
+
423
+ # Log usage
424
+ log_data = {
425
+ "user_id": user_id,
426
+ "question_hash": question_hash,
427
+ "subject": subject[:100] if subject else "",
428
+ "chapter": chapter[:100] if chapter else "",
429
+ "tokens_used": tokens_used,
430
+ "cached": cached,
431
+ "plan_type": plan_type
432
+ }
433
+
434
+ response = self.supabase.table("seba_usage_logs").insert(log_data).execute()
435
+
436
+ # Update subscription usage (only for non-cached questions)
437
+ if not cached and plan and plan.get('plan_id'):
438
+ # Increment questions_used
439
+ new_used = plan['questions_used'] + 1
440
+ self.supabase.table("seba_subscriptions") \
441
+ .update({"questions_used": new_used}) \
442
+ .eq("id", plan['plan_id']) \
443
+ .execute()
444
+
445
+ return True
446
+ except Exception as e:
447
+ print(f"Error logging question: {e}")
448
+ return False
449
+
450
+ def upgrade_plan(self, user_id, plan_type, payment_id=None):
451
+ """Upgrade user's subscription plan"""
452
+ plans = {
453
+ 'pro_monthly': {
454
+ 'price': 50,
455
+ 'questions': 450,
456
+ 'daily_limit': 15,
457
+ 'days': 30
458
+ },
459
+ 'pro_plus': {
460
+ 'price': 100,
461
+ 'questions': 1200,
462
+ 'daily_limit': 40,
463
+ 'days': 30
464
+ }
465
+ }
466
+
467
+ if plan_type not in plans:
468
+ return False, "Invalid plan type"
469
+
470
+ plan = plans[plan_type]
471
+
472
+ try:
473
+ # Deactivate current active subscription
474
+ self.supabase.table("seba_subscriptions") \
475
+ .update({"is_active": False}) \
476
+ .eq("user_id", user_id) \
477
+ .eq("is_active", True) \
478
+ .execute()
479
+
480
+ # Create new subscription
481
+ subscription_data = {
482
+ "user_id": user_id,
483
+ "plan_type": plan_type,
484
+ "price_paid": plan['price'],
485
+ "start_date": datetime.now().isoformat(),
486
+ "end_date": (datetime.now() + timedelta(days=plan['days'])).isoformat(),
487
+ "total_questions": plan['questions'],
488
+ "questions_used": 0,
489
+ "daily_limit": plan['daily_limit'],
490
+ "is_active": True,
491
+ "payment_id": payment_id
492
+ }
493
+
494
+ response = self.supabase.table("seba_subscriptions").insert(subscription_data).execute()
495
+
496
+ return True, f"Upgraded to {plan_type.replace('_', ' ').title()} successfully!"
497
+
498
+ except Exception as e:
499
+ return False, f"Error: {str(e)}"
500
+
501
+ def get_user_stats(self, user_id):
502
+ """Get user statistics"""
503
+ try:
504
+ # Get today's usage
505
+ today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
506
+ today_end = datetime.now().replace(hour=23, minute=59, second=59, microsecond=999999)
507
+
508
+ response = self.supabase.table("seba_usage_logs") \
509
+ .select("id", count="exact") \
510
+ .eq("user_id", user_id) \
511
+ .eq("cached", False) \
512
+ .gte("timestamp", today_start.isoformat()) \
513
+ .lt("timestamp", today_end.isoformat()) \
514
+ .execute()
515
+
516
+ today_used = response.count or 0
517
+
518
+ # Get total usage
519
+ response = self.supabase.table("seba_usage_logs") \
520
+ .select("id", count="exact") \
521
+ .eq("user_id", user_id) \
522
+ .eq("cached", False) \
523
+ .execute()
524
+
525
+ total_used = response.count or 0
526
+
527
+ # Get cached usage
528
+ response = self.supabase.table("seba_usage_logs") \
529
+ .select("id", count="exact") \
530
+ .eq("user_id", user_id) \
531
+ .eq("cached", True) \
532
+ .execute()
533
+
534
+ cached_used = response.count or 0
535
+
536
+ return {
537
+ 'today_used': today_used,
538
+ 'total_used': total_used,
539
+ 'cached_used': cached_used
540
+ }
541
+
542
+ except Exception as e:
543
+ print(f"Error getting user stats: {e}")
544
+ return {'today_used': 0, 'total_used': 0, 'cached_used': 0}
545
+
546
+ # ===============================
547
+ # API KEY HANDLING
548
+ # ===============================
549
+ # Get API key from environment variable
550
+ api_key = os.environ.get("DEEPSEEK_API_KEY", "")
551
+
552
+ # Page config - must be first Streamlit command
553
+ st.set_page_config(
554
+ page_title="SEBA দশম শ্ৰেণীৰ AI টিউটাৰ",
555
+ page_icon="🎓",
556
+ layout="wide",
557
+ initial_sidebar_state="collapsed"
558
+ )
559
+
560
+ # Enhanced CSS with streaming and focus features - FIXED LATEX RENDERING
561
+ st.markdown("""
562
+ <style>
563
+ /* Assamese-friendly fonts */
564
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Bengali:wght@400;500;600;700;800&family=Hind+Siliguri:wght@300;400;500;600;700&display=swap');
565
+
566
+ * {
567
+ font-family: 'Noto Sans Bengali', 'Hind Siliguri', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
568
+ }
569
+
570
+ /* Reduced spacing header */
571
+ .header-container {
572
+ background: linear-gradient(135deg, #0d47a1 0%, #1565c0 50%, #1976d2 100%);
573
+ padding: 1.25rem;
574
+ border-radius: 15px;
575
+ margin-bottom: 1rem;
576
+ color: white;
577
+ position: relative;
578
+ overflow: hidden;
579
+ border: 1px solid rgba(255,255,255,0.1);
580
+ box-shadow: 0 5px 15px rgba(13,71,161,0.2);
581
+ }
582
+
583
+ /* The top rainbow line */
584
+ .header-container::before {
585
+ content: '';
586
+ position: absolute;
587
+ top: 0;
588
+ left: 0;
589
+ right: 0;
590
+ height: 3px;
591
+ background: linear-gradient(90deg, #FF5722, #FF9800, #4CAF50);
592
+ }
593
+
594
+ /* Header text */
595
+ .header-container h1 {
596
+ color: #ffffff;
597
+ font-size: 1.5rem;
598
+ font-weight: 800;
599
+ text-shadow: 0px 1px 3px rgba(0,0,0,0.5);
600
+ margin: 0;
601
+ line-height: 1.2;
602
+ }
603
+
604
+ .header-container p {
605
+ color: #f6f9ff;
606
+ font-weight: 600;
607
+ font-size: 0.95rem;
608
+ opacity: 1 !important;
609
+ text-shadow: 0px 1px 2px rgba(0,0,0,0.4);
610
+ margin-top: .2rem;
611
+ }
612
+
613
+ .subject-card {
614
+ background: linear-gradient(145deg, #ffffff 0%, #f0f7ff 100%);
615
+ padding: 0.75rem;
616
+ border-radius: 10px;
617
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08);
618
+ border-left: 4px solid #2196F3;
619
+ margin: 0.5rem 0;
620
+ transition: all 0.3s ease;
621
+ border: 1px solid #e3f2fd;
622
+ }
623
+
624
+ .subject-card:hover {
625
+ transform: translateY(-3px);
626
+ box-shadow: 0 5px 12px rgba(33, 150, 243, 0.15);
627
+ }
628
+
629
+ .answer-box {
630
+ background: linear-gradient(145deg, #f8fdff 0%, #ffffff 100%);
631
+ padding: 1rem;
632
+ border-radius: 10px;
633
+ border: 1px solid #e1f5fe;
634
+ margin: 0.75rem 0;
635
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
636
+ position: relative;
637
+ }
638
+
639
+ .stButton > button {
640
+ background: linear-gradient(135deg, #FF5722 0%, #FF9800 100%);
641
+ color: white;
642
+ border: none;
643
+ padding: 0.4rem 1rem;
644
+ border-radius: 8px;
645
+ font-weight: 600;
646
+ font-size: 0.9rem;
647
+ transition: all 0.3s;
648
+ box-shadow: 0 2px 6px rgba(255, 87, 34, 0.3);
649
+ }
650
+
651
+ .stButton > button:hover {
652
+ transform: translateY(-1px);
653
+ box-shadow: 0 3px 9px rgba(255, 87, 34, 0.4);
654
+ }
655
+
656
+ .sidebar-section {
657
+ background: linear-gradient(145deg, #f8f9fa 0%, #e3f2fd 100%);
658
+ padding: 0.75rem;
659
+ border-radius: 10px;
660
+ margin-bottom: 0.75rem;
661
+ border: 1px solid #bbdefb;
662
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
663
+ }
664
+
665
+ .assamese-highlight {
666
+ background: linear-gradient(120deg, #FFF176 0%, #FFEB3B 100%);
667
+ background-repeat: no-repeat;
668
+ background-size: 100% 0.3em;
669
+ background-position: 0 90%;
670
+ padding: 0.1rem 0.2rem;
671
+ font-weight: 700;
672
+ color: #FF6F00;
673
+ }
674
+
675
+ .assamese-text {
676
+ font-family: 'Noto Sans Bengali', sans-serif;
677
+ font-weight: 500;
678
+ color: #0d47a1;
679
+ line-height: 1.4;
680
+ }
681
+
682
+ .assamese-title {
683
+ font-family: 'Noto Sans Bengali', sans-serif;
684
+ font-weight: 700;
685
+ color: #1565c0;
686
+ }
687
+
688
+ /* Chat bubble styling */
689
+ .user-bubble {
690
+ background: linear-gradient(135deg, #2196F3 0%, #0d47a1 100%) !important;
691
+ color: white;
692
+ padding: 0.5rem 0.75rem;
693
+ border-radius: 12px 12px 0 12px;
694
+ max-width: 80%;
695
+ box-shadow: 0 2px 6px rgba(33, 150, 243, 0.2);
696
+ margin-left: auto;
697
+ }
698
+
699
+ .ai-bubble {
700
+ background: linear-gradient(135deg, #f5f5f5 0%, #ffffff 100%) !important;
701
+ padding: 0.75rem;
702
+ border-radius: 12px 12px 12px 0;
703
+ border: 1px solid #e0e0e0;
704
+ box-shadow: 0 2px 6px rgba(0,0,0,0.05);
705
+ }
706
+
707
+ /* Chat container */
708
+ .chat-container {
709
+ margin-bottom: 1rem;
710
+ scroll-margin-top: 20px;
711
+ }
712
+
713
+ .chat-message {
714
+ margin-bottom: 0.75rem;
715
+ animation: fadeIn 0.3s ease-in;
716
+ }
717
+
718
+ @keyframes fadeIn {
719
+ from { opacity: 0; transform: translateY(5px); }
720
+ to { opacity: 1; transform: translateY(0); }
721
+ }
722
+
723
+ /* LaTeX equation styling - FIXED VERSION */
724
+ .katex {
725
+ font-size: 1.1em !important;
726
+ padding: 0.2rem 0.5rem !important;
727
+ background: rgba(33, 150, 243, 0.1) !important;
728
+ border-radius: 4px !important;
729
+ margin: 0.3rem 0.1rem !important;
730
+ display: inline-block !important;
731
+ vertical-align: middle !important;
732
+ }
733
+
734
+ .katex-display {
735
+ margin: 1rem 0 !important;
736
+ padding: 1rem !important;
737
+ background: linear-gradient(145deg, #f0f7ff 0%, #e3f2fd 100%) !important;
738
+ border-radius: 8px !important;
739
+ border-left: 4px solid #2196F3 !important;
740
+ overflow-x: auto !important;
741
+ overflow-y: hidden !important;
742
+ text-align: center !important;
743
+ }
744
+
745
+ .katex-display .katex {
746
+ background: transparent !important;
747
+ font-size: 1.2em !important;
748
+ padding: 0.5rem !important;
749
+ }
750
+
751
+ /* Ensure LaTeX works inside streaming text */
752
+ .streaming-text .katex {
753
+ animation: none !important;
754
+ background: rgba(33, 150, 243, 0.05) !important;
755
+ }
756
+
757
+ .streaming-text .katex-display {
758
+ margin: 0.5rem 0 !important;
759
+ padding: 0.8rem !important;
760
+ }
761
+
762
+ /* Stop cursor animation on LaTeX elements */
763
+ .katex::after {
764
+ content: '' !important;
765
+ animation: none !important;
766
+ }
767
+
768
+ /* Control panel styling */
769
+ .control-panel {
770
+ background: linear-gradient(145deg, #f8f9fa 0%, #e3f2fd 100%);
771
+ padding: 1rem;
772
+ border-radius: 15px;
773
+ margin: 1rem 0;
774
+ border: 1px solid #bbdefb;
775
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
776
+ }
777
+
778
+ /* Enhanced streaming text animation */
779
+ .streaming-text {
780
+ display: inline-block;
781
+ overflow: hidden;
782
+ white-space: pre-wrap;
783
+ }
784
+
785
+ .streaming-text::after {
786
+ content: '▋';
787
+ animation: cursor-blink 1s infinite;
788
+ font-weight: bold;
789
+ color: #2196F3;
790
+ }
791
+
792
+ .streaming-character {
793
+ display: inline-block;
794
+ animation: charPop 0.1s ease-out;
795
+ }
796
+
797
+ @keyframes charPop {
798
+ 0% { opacity: 0; transform: translateY(2px); }
799
+ 100% { opacity: 1; transform: translateY(0); }
800
+ }
801
+
802
+ @keyframes cursor-blink {
803
+ 0%, 100% { opacity: 1; }
804
+ 50% { opacity: 0; }
805
+ }
806
+
807
+ /* Enhanced progress indicator */
808
+ .progress-indicator {
809
+ display: flex;
810
+ align-items: center;
811
+ justify-content: center;
812
+ gap: 0.5rem;
813
+ color: #0d47a1;
814
+ font-weight: 600;
815
+ padding: 0.5rem;
816
+ min-height: 100px;
817
+ }
818
+
819
+ .thinking-dots {
820
+ display: flex;
821
+ gap: 0.2rem;
822
+ }
823
+
824
+ .thinking-dots span {
825
+ width: 0.4rem;
826
+ height: 0.4rem;
827
+ border-radius: 50%;
828
+ background: #2196F3;
829
+ animation: thinking 1.4s infinite ease-in-out;
830
+ }
831
+
832
+ .thinking-dots span:nth-child(1) { animation-delay: -0.32s; }
833
+ .thinking-dots span:nth-child(2) { animation-delay: -0.16s; }
834
+ .thinking-dots span:nth-child(3) { animation-delay: 0s; }
835
+
836
+ @keyframes thinking {
837
+ 0%, 80%, 100% { transform: scale(0); }
838
+ 40% { transform: scale(1); }
839
+ }
840
+
841
+ /* Cache indicator styling */
842
+ .cache-badge {
843
+ background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
844
+ color: white;
845
+ padding: 0.2rem 0.5rem;
846
+ border-radius: 12px;
847
+ font-size: 0.75rem;
848
+ font-weight: 600;
849
+ display: inline-flex;
850
+ align-items: center;
851
+ gap: 0.2rem;
852
+ }
853
+
854
+ /* Enhanced answer container styling */
855
+ .answer-container {
856
+ background: linear-gradient(135deg, #f8fdff 0%, #ffffff 100%);
857
+ border-radius: 10px;
858
+ padding: 1rem;
859
+ margin: 1rem 0;
860
+ border: 1px solid #e1f5fe;
861
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
862
+ animation: slideIn 0.3s ease-out;
863
+ }
864
+
865
+ @keyframes slideIn {
866
+ from { opacity: 0; transform: translateY(10px); }
867
+ to { opacity: 1; transform: translateY(0); }
868
+ }
869
+
870
+ /* Highlight animation for new answers */
871
+ @keyframes highlightPulse {
872
+ 0% { box-shadow: 0 0 0 0 rgba(33, 150, 243, 0.4); }
873
+ 70% { box-shadow: 0 0 0 10px rgba(33, 150, 243, 0); }
874
+ 100% { box-shadow: 0 0 0 0 rgba(33, 150, 243, 0); }
875
+ }
876
+
877
+ .highlight-answer {
878
+ animation: highlightPulse 1.5s ease-in-out;
879
+ }
880
+
881
+ /* Focus container for auto-scroll */
882
+ .focus-container {
883
+ scroll-margin-top: 100px;
884
+ }
885
+
886
+ /* Responsive adjustments */
887
+ @media (max-width: 768px) {
888
+ .header-container {
889
+ padding: 0.75rem;
890
+ }
891
+ .subject-card {
892
+ padding: 0.5rem;
893
+ }
894
+ .user-bubble, .ai-bubble {
895
+ max-width: 90%;
896
+ }
897
+ .control-panel {
898
+ padding: 0.75rem;
899
+ }
900
+ }
901
+
902
+ /* Subscription banner styling */
903
+ .subscription-banner {
904
+ padding: 0.75rem 1rem;
905
+ border-radius: 10px;
906
+ margin: 0.5rem 0 1.5rem 0;
907
+ border-left: 5px solid;
908
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);
909
+ color: white;
910
+ }
911
+
912
+ .subscription-banner strong {
913
+ font-size: 1rem;
914
+ }
915
+
916
+ .subscription-banner .usage-stats {
917
+ font-size: 0.85rem;
918
+ opacity: 0.9;
919
+ }
920
+
921
+ .subscription-banner .upgrade-btn {
922
+ background: rgba(255,255,255,0.2);
923
+ padding: 0.3rem 0.8rem;
924
+ border-radius: 5px;
925
+ font-size: 0.85rem;
926
+ cursor: pointer;
927
+ }
928
+ </style>
929
+ """, unsafe_allow_html=True)
930
+
931
+ # ===============================
932
+ # HELPER FUNCTIONS - FIXED CACHE KEY
933
+ # ===============================
934
+ def create_cache_key(question, subject, chapter_name):
935
+ """Create a unique cache key for the question"""
936
+ # Normalize the question more aggressively for better cache matching
937
+ normalized_question = question.strip().lower()
938
+
939
+ # Remove extra whitespace
940
+ normalized_question = re.sub(r'\s+', ' ', normalized_question)
941
+
942
+ # Remove punctuation that might vary
943
+ normalized_question = re.sub(r'[^\w\s\u0980-\u09FF]', '', normalized_question)
944
+
945
+ normalized_question = normalized_question[:200]
946
+
947
+ # Normalize subject and chapter
948
+ # Take only the main subject name (before parentheses)
949
+ normalized_subject = subject.split('(')[0].strip() if '(' in subject else subject
950
+ # Take only chapter number/name before colon
951
+ normalized_chapter = chapter_name.split(':')[0].strip() if ':' in chapter_name else chapter_name
952
+
953
+ key_string = f"{normalized_subject}|{normalized_chapter}|{normalized_question}"
954
+ cache_key = hashlib.md5(key_string.encode()).hexdigest()
955
+
956
+ return cache_key
957
+
958
+ def get_question_guidance(question, subject, chapter_name):
959
+ question_lower = question.lower()
960
+
961
+ simple_keywords = [
962
+ "সংজ্ঞা", "কি", "কাক কয়", "মানে", "definition", "what is",
963
+ "নাম", "কেইটা", "কিমান", "count", "number", "কি নাম", "কাক বোলে"
964
+ ]
965
+
966
+ moderate_keywords = [
967
+ "কেনেকৈ", "কেনেকুৱা", "কিয়", "বুজাই দিয়ক", "explain", "how",
968
+ "why", "difference", "পাৰ্থক্য", "উদাহৰণ", "example", "সমাধান",
969
+ "solve", "কোনবোৰ", "তুলনা", "compare", "সাদৃশ্য", "similarity"
970
+ ]
971
+
972
+ complex_keywords = [
973
+ "বিশ্লেষণ", "আলোচনা", "মূল্যায়ন", "বৰ্ণনা", "discuss",
974
+ "analyze", "evaluate", "describe", "প্ৰমাণ", "prove",
975
+ "সমাধান কৰি দেখুৱাওক", "solve and show", "step by step",
976
+ "ধাপে ধাপে", "সম্পূৰ্ণ", "সম্পূৰ্ণ বিৱৰণ", "full explanation",
977
+ "সবিশেষ", "in detail", "detailed", "সবিস্তাৰে"
978
+ ]
979
+
980
+ guidance_text = ""
981
+
982
+ if "📐 গণিত" in subject:
983
+ guidance_text = "গণিতৰ সমস্যাৰ বাবে ধাপে ধা��ে সমাধান দিব লাগে। "
984
+ elif "🔬 বিজ্ঞান" in subject:
985
+ guidance_text = "বিজ্ঞানৰ উত্তৰ বৈজ্ঞানিকভাৱে সঠিক হ'ব লাগে। "
986
+ elif "🌍 সমাজ বিজ্ঞান" in subject:
987
+ guidance_text = "তথ্য সঠিক আৰু বিশ্লেষণাত্মক হ'ব লাগে। "
988
+
989
+ if any(keyword in question_lower for keyword in complex_keywords):
990
+ return f"{guidance_text} প্ৰশ্নটো জটিল, গতিকে বিশদ উত্তৰ দিবা।"
991
+ elif any(keyword in question_lower for keyword in moderate_keywords):
992
+ return f"{guidance_text} প্ৰশ্নটো মধ্যমীয়া, গতিকে সম্পূৰ্ণ উত্তৰ দিবা।"
993
+ elif any(keyword in question_lower for keyword in simple_keywords):
994
+ return f"{guidance_text} প্ৰশ্নটো সৰল, গতিকে সংক্ষিপ্ত উত্তৰ দিবা।"
995
+ else:
996
+ return f"{guidance_text} প্ৰশ্নৰ প্ৰকৃতি অনুসৰি উত্তৰ দিবা।"
997
+
998
+ def get_subject_prompt(subject, chapter_name, question):
999
+ if subject not in SUBJECT_PROMPTS:
1000
+ subject = "📐 গণিত (Mathematics)"
1001
+
1002
+ prompt_template = SUBJECT_PROMPTS[subject]
1003
+ base_prompt = prompt_template["base_prompt"].format(chapter_name=chapter_name)
1004
+ guidance = prompt_template["guidance"]
1005
+
1006
+ if subject == "📐 গণিত (Mathematics)" or subject == "🔬 বিজ্ঞান (Science)":
1007
+ latex_instruction = "\n\n**গুৰুত্বপূৰ্ণ**: সকলো গাণিতিক সূত্ৰ, সমীকৰণ LaTeX ফৰ্মেটত দিবা ($ চিহ্নৰ মাজত)।"
1008
+ else:
1009
+ latex_instruction = ""
1010
+
1011
+ question_guidance = get_question_guidance(question, subject, chapter_name)
1012
+
1013
+ full_prompt = f"""{base_prompt}
1014
+
1015
+ {guidance}{latex_instruction}
1016
+
1017
+ **উত্তৰৰ নিৰ্দেশনা:**
1018
+ {question_guidance}
1019
+ **উত্তৰ যিমান দৰকাৰী সিমান দীঘল হ'ব লাগে।**
1020
+ **শুদ্ধ অসমীয়া ব্যৱহাৰ কৰিবা, বাংলা শব্দ বা বৰ্ণমালা ব্যৱহাৰ নকৰিবা**
1021
+
1022
+ **ছাত্ৰক মাতি লওঁ:**
1023
+ "বন্ধু, এইটো এনেদৰে বুজিব লাগে..."
1024
+ "চিন্তা নকৰিব, এইটো সহজ..."
1025
+
1026
+ এতিয়া এই প্ৰশ্নটোৰ উত্তৰ দিয়া: {question}"""
1027
+
1028
+ return full_prompt
1029
+
1030
+ # ===============================
1031
+ # FIXED: STREAMING TEXT WITH LATEX SUPPORT
1032
+ # ===============================
1033
+ def stream_text_with_animation(text, placeholder, speed=10):
1034
+ """
1035
+ Display text with streaming animation (character by character)
1036
+ WITH PROPER LATEX SUPPORT
1037
+ """
1038
+ display_text = ""
1039
+
1040
+ # Split into characters for animation
1041
+ for i, char in enumerate(text):
1042
+ display_text += char
1043
+
1044
+ # Update streaming display with better animation
1045
+ placeholder.markdown(
1046
+ f'<div class="streaming-text">{display_text}</div>',
1047
+ unsafe_allow_html=True
1048
+ )
1049
+
1050
+ # Control speed (except for whitespace)
1051
+ if char not in [' ', '\n']:
1052
+ time.sleep(1/speed)
1053
+
1054
+ # CRITICAL FIX: After completion, clear and re-render with proper LaTeX support
1055
+ placeholder.empty() # Clear the streaming placeholder
1056
+ # Re-render the complete text with proper markdown/LaTeX support
1057
+ placeholder.markdown(text)
1058
+
1059
+ return True
1060
+
1061
+ # ===============================
1062
+ # ENHANCED: STREAMLIT STREAMING RESPONSE FUNCTION
1063
+ # ===============================
1064
+ def stream_deepseek_response(prompt, question, subject, chapter_name):
1065
+ headers = {
1066
+ "Authorization": f"Bearer {api_key}",
1067
+ "Content-Type": "application/json"
1068
+ }
1069
+
1070
+ payload = {
1071
+ "model": "deepseek-chat",
1072
+ "messages": [
1073
+ {"role": "system", "content": "তুমি এজন বিশেষজ্ঞ SEBA দশম শ্ৰেণীৰ শিক্ষক।"},
1074
+ {"role": "user", "content": prompt}
1075
+ ],
1076
+ "temperature": 0.3,
1077
+ "stream": True
1078
+ }
1079
+
1080
+ try:
1081
+ # Make streaming request
1082
+ response = requests.post(
1083
+ "https://api.deepseek.com/v1/chat/completions",
1084
+ headers=headers,
1085
+ json=payload,
1086
+ stream=True,
1087
+ timeout=180
1088
+ )
1089
+
1090
+ if response.status_code == 200:
1091
+ full_response = ""
1092
+ tokens_used = 0
1093
+
1094
+ # Create a placeholder for streaming text
1095
+ streaming_placeholder = st.empty()
1096
+
1097
+ # Process streaming response
1098
+ for line in response.iter_lines():
1099
+ if line:
1100
+ line = line.decode('utf-8')
1101
+ if line.startswith('data: '):
1102
+ data = line[6:] # Remove 'data: ' prefix
1103
+ if data == '[DONE]':
1104
+ break
1105
+
1106
+ try:
1107
+ chunk = json.loads(data)
1108
+ if 'choices' in chunk and len(chunk['choices']) > 0:
1109
+ delta = chunk['choices'][0].get('delta', {})
1110
+ if 'content' in delta:
1111
+ content = delta['content']
1112
+ full_response += content
1113
+
1114
+ # Update streaming display with better animation
1115
+ streaming_placeholder.markdown(
1116
+ f'<div class="streaming-text">{full_response}</div>',
1117
+ unsafe_allow_html=True
1118
+ )
1119
+
1120
+ # Track tokens
1121
+ if 'usage' in chunk:
1122
+ tokens_used = chunk['usage'].get('total_tokens', 0)
1123
+ except json.JSONDecodeError:
1124
+ continue
1125
+
1126
+ # Clear streaming cursor after completion and re-render with LaTeX support
1127
+ streaming_placeholder.empty()
1128
+
1129
+ # Render the final answer with proper LaTeX support
1130
+ st.markdown(full_response)
1131
+
1132
+ # Store the complete response
1133
+ st.session_state.last_answer = full_response
1134
+ st.session_state.tokens_used = tokens_used
1135
+
1136
+ # Save to cache using manager
1137
+ cache_key = create_cache_key(question, subject, chapter_name)
1138
+ st.session_state.cache_manager.set(cache_key, {
1139
+ 'answer': full_response,
1140
+ 'tokens': tokens_used,
1141
+ 'subject': subject,
1142
+ 'chapter': chapter_name,
1143
+ 'question': question[:200]
1144
+ })
1145
+
1146
+ # Add to history
1147
+ history_entry = {
1148
+ 'subject': subject,
1149
+ 'chapter': chapter_name,
1150
+ 'question': question[:100],
1151
+ 'timestamp': datetime.now().strftime("%H:%M"),
1152
+ 'tokens': tokens_used,
1153
+ 'cached': False
1154
+ }
1155
+ st.session_state.history.append(history_entry)
1156
+
1157
+ # Add JavaScript to scroll to answer and highlight it
1158
+ st.markdown("""
1159
+ <script>
1160
+ // Scroll to answer container smoothly
1161
+ setTimeout(function() {
1162
+ const answerContainers = document.querySelectorAll('.chat-container');
1163
+ if (answerContainers.length > 0) {
1164
+ const lastAnswer = answerContainers[answerContainers.length - 1];
1165
+ lastAnswer.scrollIntoView({ behavior: 'smooth', block: 'center' });
1166
+
1167
+ // Add highlight animation
1168
+ lastAnswer.classList.add('highlight-answer');
1169
+ setTimeout(() => {
1170
+ lastAnswer.classList.remove('highlight-answer');
1171
+ }, 1500);
1172
+ }
1173
+ }, 100);
1174
+ </script>
1175
+ """, unsafe_allow_html=True)
1176
+
1177
+ else:
1178
+ st.error(f"API ত্ৰুটি {response.status_code}: {response.text}")
1179
+
1180
+ except Exception as e:
1181
+ st.error(f"সংযোগ ত্ৰুটি: {str(e)}")
1182
+
1183
+ # ===============================
1184
+ # FIXED: CACHE ANSWER WITH THINKING ANIMATION
1185
+ # ===============================
1186
+ def display_cached_answer_with_animation(cached_data, question, subject, chapter_name, cache_source):
1187
+ """
1188
+ Display cached answer with thinking animation and streaming effect
1189
+ """
1190
+ # Display user question
1191
+ st.markdown(f"""
1192
+ <div class="chat-container">
1193
+ <div style="display: flex; justify-content: flex-end; margin-bottom: 0.3rem;">
1194
+ <div class="user-bubble">
1195
+ <div style="font-weight: 600; margin-bottom: 0.2rem;">👤 আপুনি:</div>
1196
+ <div>{question[:200]}{'...' if len(question) > 200 else ''}</div>
1197
+ </div>
1198
+ </div>
1199
+ """, unsafe_allow_html=True)
1200
+
1201
+ # AI answer header with thinking animation initially
1202
+ st.markdown(f"""
1203
+ <div style="display: flex; align-items: flex-start; margin-bottom: 0.3rem;">
1204
+ <div style="margin-right: 0.5rem; font-size: 1.2rem;">🤖</div>
1205
+ <div style="flex: 1;">
1206
+ <div class="ai-bubble">
1207
+ <div style="display: flex; align-items: center; margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 2px solid #4CAF50;">
1208
+ <div style="display: flex; align-items: center;">
1209
+ <div style="background: #4CAF50; color: white; padding: 0.2rem 0.5rem; border-radius: 8px;
1210
+ font-weight: 600; font-size: 0.8rem; margin-right: 0.5rem;">
1211
+ <span style="margin-right: 0.3rem;">⚡</span> Cached Answer
1212
+ </div>
1213
+ <div style="font-weight: 600; color: #0d47a1; font-size: 0.9rem;">
1214
+ {cached_data.get('subject', subject)} • {cached_data.get('chapter', chapter_name)}
1215
+ </div>
1216
+ </div>
1217
+ <div style="font-size: 0.75rem; color: #666; background: #f1f8e9; padding: 0.2rem 0.5rem; border-radius: 4px;">
1218
+ <span style="margin-right: 0.3rem;">💾</span> From {cache_source}
1219
+ </div>
1220
+ </div>
1221
+ <div id="cached-answer-content" style="color: #333; line-height: 1.5; font-size: 0.95rem;">
1222
+ <!-- Answer will be streamed here -->
1223
+ </div>
1224
+ </div>
1225
+ </div>
1226
+ </div>
1227
+ </div>
1228
+ """, unsafe_allow_html=True)
1229
+
1230
+ # Create a placeholder for the thinking animation
1231
+ thinking_placeholder = st.empty()
1232
+
1233
+ # Show thinking animation for 1 second
1234
+ thinking_placeholder.markdown("""
1235
+ <div class="progress-indicator">
1236
+ <span>উত্তৰ প্ৰস্তুত কৰি আছো...</span>
1237
+ <div class="thinking-dots">
1238
+ <span></span>
1239
+ <span></span>
1240
+ <span></span>
1241
+ </div>
1242
+ </div>
1243
+ """, unsafe_allow_html=True)
1244
+
1245
+ # Wait for 1 second to simulate thinking
1246
+ time.sleep(1)
1247
+
1248
+ # Clear thinking animation
1249
+ thinking_placeholder.empty()
1250
+
1251
+ # Create a new placeholder for streaming answer
1252
+ answer_placeholder = st.empty()
1253
+
1254
+ # Stream the cached answer with animation (USING FIXED FUNCTION)
1255
+ stream_text_with_animation(cached_data['answer'], answer_placeholder, speed=20)
1256
+
1257
+ # Show token usage
1258
+ if cached_data.get('tokens', 0) > 0:
1259
+ st.caption(f"📊 Original token cost (saved): {cached_data['tokens']:,} tokens")
1260
+
1261
+ # Add JavaScript to scroll to answer and highlight it
1262
+ st.markdown("""
1263
+ <script>
1264
+ // Scroll to answer container smoothly
1265
+ setTimeout(function() {
1266
+ const answerContainers = document.querySelectorAll('.chat-container');
1267
+ if (answerContainers.length > 0) {
1268
+ const lastAnswer = answerContainers[answerContainers.length - 1];
1269
+ lastAnswer.scrollIntoView({ behavior: 'smooth', block: 'center' });
1270
+
1271
+ // Add highlight animation
1272
+ lastAnswer.classList.add('highlight-answer');
1273
+ setTimeout(() => {
1274
+ lastAnswer.classList.remove('highlight-answer');
1275
+ }, 1500);
1276
+ }
1277
+ }, 100);
1278
+ </script>
1279
+ """, unsafe_allow_html=True)
1280
+
1281
+ # Add to history
1282
+ history_entry = {
1283
+ 'subject': subject,
1284
+ 'chapter': chapter_name,
1285
+ 'question': question[:100],
1286
+ 'timestamp': datetime.now().strftime("%H:%M"),
1287
+ 'tokens': cached_data['tokens'],
1288
+ 'cached': True,
1289
+ 'cache_source': cache_source
1290
+ }
1291
+ st.session_state.history.append(history_entry)
1292
+
1293
+ # ===============================
1294
+ # SEBA CURRICULUM DATA
1295
+ # ===============================
1296
+ SEBA_CURRICULUM = {
1297
+ "📐 গণিত (Mathematics)": {
1298
+ "অধ্যায় ১": "বাস্তৱ সংখ্যা (Real Numbers)",
1299
+ "অধ্যায় ২": "বহুপদ (Polynomials)",
1300
+ "অধ্যায় ৩": "দ্বিঘাত সমীকৰণ (Quadratic Equations)",
1301
+ "অধ্যায় ৪": "সামান্তৰিক শ্রেণী (Arithmetic Progressions)",
1302
+ "অধ্যায় ৫": "ত্ৰিভুজ (Triangles)",
1303
+ "অধ্যায় ৬": "ত্রিকোণমিতি (Trigonometry)",
1304
+ "অধ্যায় ৭": "বৃত্ত (Circles)",
1305
+ "অধ্যায় ৮": "স্থানাঙ্ক জ্যামিতি (Coordinate Geometry)",
1306
+ "অধ্যায় ৯": "ক্ষেত্রফল আৰু আয়তন (Areas and Volumes)",
1307
+ "অধ্যায় ১০": "পৰিসংখ্যা (Statistics)",
1308
+ "অধ্যায় ১১": "সম্ভাৱিতা (Probability)"
1309
+ },
1310
+ "🔬 বিজ্ঞান (Science)": {
1311
+ "অধ্যায় ১": "ৰাসায়নিক বিক্রিয়া আৰু সমীকৰণ",
1312
+ "অধ্যায় ২": "এছিড, ক্ষাৰক আৰু লৱণ",
1313
+ "অধ্যায় ৩": "ধাতু আৰু অধাতু",
1314
+ "অধ্যায় ৪": "কার্বন আৰু তা�� যৌগ",
1315
+ "অধ্যায় ৫": "পৰ্যাবৃত্ত শ্রেণীবিভাজন",
1316
+ "অধ্যায় ৬": "জীৱন প্ৰক্ৰিয়া",
1317
+ "অধ্যায় ৭": "নিয়ন্ত্ৰণ আৰু সমন্বয়",
1318
+ "অধ্যায় ৮": "জীৱই কেনেদৰে বংশবিস্তাৰ কৰে",
1319
+ "অধ্যায় ৯": "আনুভূমিক আৰু ঊর্ধ্বমুখী বংশগতি",
1320
+ "অধ্যায় ১০": "পোহৰ-প্ৰতিফলন আৰু প্ৰতিসৰণ",
1321
+ "অধ্যায় ১১": "মানুহৰ চকু আৰু বৰ্ণিল পৃথিৱী",
1322
+ "অধ্যায় ১২": "বিদ্যুৎ",
1323
+ "অধ্যায় ১৩": "বিদ্যুৎ-চুম্বকীয় প্ৰভাৱ",
1324
+ "অধ্যায় ১৪": "শক্তিৰ উৎসসমূহ",
1325
+ "অধ্যায় ১৫": "আমাৰ পৰিৱেশ",
1326
+ "অধ্যায় ১৬": "প্রাকৃতিক সম্পদৰ ব্যৱস্থাপনা"
1327
+ },
1328
+ "🌍 সমাজ বিজ্ঞান (Social Science)": {
1329
+ "অধ্যায় ১": "ইউৰোপত ৰাষ্ট্ৰবাদৰ উত্থান",
1330
+ "অধ্যায় ২": "ভাৰতীয় জাতীয়তাবাদৰ উত্থান",
1331
+ "অধ্যায় ৩": "ভূগোল-প্রাকৃতিক আৰু মানৱ",
1332
+ "অধ্যায় ৪": "অৰ্থনীতি-উন্নয়ন",
1333
+ "অধ্যায় ৫": "লোকসাধাৰণৰ সংস্কৃতি আৰু জাতীয়তাবাদ",
1334
+ "অধ্যায় ৬": "উদ্যোগ",
1335
+ "অধ্যায় ৭": "অৰ্থনৈতিক অৱস্থা",
1336
+ "অধ্যায় ৮": "ৰাজনৈতিক দল",
1337
+ "অধ্যায় ৯": "ক্ষমতাৰ ভাগ-বতৰা",
1338
+ "অধ্যায় ১০": "জনসম্পদ"
1339
+ },
1340
+ "📖 ইংৰাজী (English)": {
1341
+ "পাঠ ১": "A Letter to God",
1342
+ "পাঠ ২": "Nelson Mandela: Long Walk to Freedom",
1343
+ "পাঠ ৩": "Two Stories about Flying",
1344
+ "পাঠ ৪": "From the Diary of Anne Frank",
1345
+ "পাঠ ৫": "The Hundred Dresses – I",
1346
+ "পাঠ ৬": "The Hundred Dresses – II",
1347
+ "পাঠ ৭": "Glimpses of India",
1348
+ "পাঠ ৮": "Mijbil the Otter",
1349
+ "পাঠ ৯": "Madam Rides the Bus",
1350
+ "পাঠ ১০": "The Sermon at Benares",
1351
+ "পাঠ ১১": "The Proposal"
1352
+ },
1353
+ "📜 অসমীয়া (Assamese)": {
1354
+ "পাঠ ১": "বৰগীত",
1355
+ "পাঠ ২": "জীৱন-সঙ্গীত",
1356
+ "পাঠ ৩": "প্রশস্তি",
1357
+ "পাঠ ৪": "মোৰ মৰমি জনমভূমি",
1358
+ "পাঠ ৫": "অসমীয়া ভাষাৰ উন্নতি",
1359
+ "পাঠ ৬": "অসমৰ লোক-সংস্কৃতি",
1360
+ "পাঠ ৭": "আমাৰ ঋতু",
1361
+ "পাঠ ৮": "বহাগ বিহু",
1362
+ "পাঠ ৯": "মহাপুরুষীয়া ধৰ্ম",
1363
+ "পাঠ ১০": "সাহিত্যৰ ৰূপ"
1364
+ },
1365
+ "📘 হিন্দী (Hindi)": {
1366
+ "পাঠ ১": "साखी",
1367
+ "পাঠ ২": "पद",
1368
+ "পাঠ ৩": "दोहे",
1369
+ "পাঠ ৪": "मनुष्यता",
1370
+ "পাঠ ५": "पर्वत प्रदेश में पावस",
1371
+ "পাঠ ६": "मधुर-मधुर मेरे दीपक जल",
1372
+ "পাঠ ৭": "तोप",
1373
+ "পাঠ ৮": "कर चले हम फ़िदा",
1374
+ "পাঠ ৯": "आत्मत्राण",
1375
+ "পাঠ ১০": "बड़े भाई साहब"
1376
+ }
1377
+ }
1378
+
1379
+ # Subject-wise prompt templates
1380
+ SUBJECT_PROMPTS = {
1381
+ "📐 গণিত (Mathematics)": {
1382
+ "base_prompt": """তুমি এজন বিশেষজ্ঞ গণিত শিক্ষক। SEBA দশম শ্ৰেণীৰ গণিতৰ পাঠ্যপুথিৰ {chapter_name} অধ্যায়ত থকা সকলো ধাৰণা, সূত্ৰ, আৰু উদাহৰণ তুমি ভালকৈ জানা।
1383
+
1384
+ **গণিতৰ বিশেষ নিৰ্দেশনা:**
1385
+ ১. **সকলো সূত্ৰ LaTeX ফৰ্মেটত দিবা**: $formula$ (দুয়োটা $ চিহ্নৰ মাজত)
1386
+ ২. **ধাপে ধাপে সমাধান দেখুৱাবা**
1387
+ ৩. **প্ৰতিটো ধাপৰ ব্যাখ্যা দিবা**
1388
+ ৪. **সহজ পদ্ধতিৰ�� বুজাবা**
1389
+ ৫. **পৰীক্ষাৰ বাবে গুৰুত্বপূৰ্ণ সূত্ৰবোৰ পৃথকৈ দেখুৱাবা**
1390
+ ৬. **সকলো গাণিতিক সমীকৰণ আৰু সূতৰবোৰ `$` চিহ্নৰ মাজত লিখিবা, আলোচনাৰ বাহিৰত পৃথক লাইনত দেখুৱাবা।**
1391
+
1392
+ **গণিতৰ সূত্ৰৰ উদাহৰণ (LaTeX ফৰ্মেটত):**
1393
+ - দ্বিঘাত সমীকৰণ: $ax^2 + bx + c = 0$
1394
+ - বৃত্তৰ কালি: $A = \\pi r^2$
1395
+ - সম্ভাৱিতা: $P(E) = \\frac{{n(E)}}{{n(S)}}$
1396
+ - পাইথাগোৰাছৰ উপপাদ্য: $a^2 + b^2 = c^2$
1397
+
1398
+ **বক্তব্য শৈলী:**
1399
+ "চিন্তা নকৰিব, এই গণিতৰ সমস্যাটো সহজ।"
1400
+ "ধাপে ধাপে শিকো আহক..."
1401
+ "এই সূত্ৰটো মনত ৰাখিব - পৰীক্ষাত আহিব পাৰে!" """,
1402
+
1403
+ "guidance": "সমীকৰণ, সূত্ৰ আৰু গাণিতিক প্ৰক্ৰিয়া LaTeX ফৰ্মেটত দেখুৱাব লাগে।"
1404
+ },
1405
+
1406
+ "🔬 বিজ্ঞান (Science)": {
1407
+ "base_prompt": """তুমি এজন বিজ্ঞান শিক্ষক। SEBA দশম শ্ৰেণীৰ বিজ্ঞানৰ {chapter_name} অধ্যায়ৰ সকলো বৈজ্ঞানিক ধাৰণা, প্ৰক্ৰয়া, আৰু নীতি তুমি জানা।
1408
+
1409
+ **বিজ্ঞানৰ বিশেষ নিৰ্দেশনা:**
1410
+ ১. **বৈজ্ঞানিক প্ৰক্ৰয়া ধাপে ধাপে বুজাবা**
1411
+ ২. **ৰাসায়নিক সমীকৰণ সঠিকভাৱে দিবা**
1412
+ ৩. **জীৱবিজ্ঞানৰ চিত্ৰ/ৰেখাচিত্ৰৰ বৰ্ণনা দিবা**
1413
+ ৪. **পদাৰ্থবিজ্ঞানৰ সূত্ৰ LaTeX ফৰ্মেটত দিবা**
1414
+
1415
+ **ৰাসায়নিক উদাহৰণ:**
1416
+ $2H_2 + O_2 \\rightarrow 2H_2O$
1417
+
1418
+ **পদাৰ্থবিজ্ঞান সূত্ৰ:**
1419
+ $F = ma$, $v = u + at$
1420
+
1421
+ **বক্তব্য শৈলী:**
1422
+ "এই বৈজ্ঞানিক ধাৰণাটো বুজোৱাৰ বাবে এটা সাধাৰণ উদাহৰণ চাওঁ..."
1423
+ "প্ৰকতিৰ এই ৰহস্যবোৰ মন কৰিছিল নেকি?" """,
1424
+
1425
+ "guidance": "ৰাসায়নিক সমীকৰণ আৰু পদাৰ্থবিজ্ঞানৰ সূত্ৰ LaTeX ফৰ্মেটত দিব লাগে।"
1426
+ },
1427
+
1428
+ "🌍 সমাজ বিজ্ঞান (Social Science)": {
1429
+ "base_prompt": """তুমি এজন সমাজ বিজ্ঞান শিক্ষক। SEBA দশম শ্ৰেণীৰ {chapter_name} অধ্যায়ৰ ঐতিহাসিক ঘটনা, ভৌগোলিক ধাৰণা, অৰ্থনৈতিক নীতি, আৰু ৰাজনৈতিক গঠন তুমি জানা।
1430
+
1431
+ **সমাজ বিজ্ঞানৰ বিশেষ নিৰ্দেশনা:**
1432
+ ১. **সহজ অসমীয়া ভাষা ব্যৱহাৰ কৰিবা**
1433
+ ২. **প্ৰশ্ন অনুসৰি উত্তৰ দিবা**""",
1434
+
1435
+ "guidance": "তথ্য আৰু বিশ্লেষণ স্পষ্টকৈ দিব লাগে।"
1436
+ },
1437
+
1438
+ "📖 ইংৰাজী (English)": {
1439
+ "base_prompt": """তুমি এজন ইংৰাজী শিক্ষক। SEBA দশম শ্ৰেণীৰ {chapter_name} পাঠটোৰ সকলো সাহিত্যিক উপাদান, ব্যাকৰণ, আৰু ভাষা কৌশল তুমি জানা।
1440
+
1441
+ **ইংৰাজীৰ বিশেষ নিৰ্দেশনা:**
1442
+ ১. Answer in English with Assamese translation""",
1443
+
1444
+ "guidance": "ইংৰাজী বাক্যৰ সৈতে অসমীয়া ব্যাখ্যা দিব লাগে।"
1445
+ },
1446
+
1447
+ "📜 অসমীয়া (Assamese)": {
1448
+ "base_prompt": """তুমি এজন অসমীয়া সাহিত্য শিক্ষক। SEBA দশম শ্ৰেণীৰ {chapter_name} পাঠটোৰ সাহিত্যিক মুল্য, ভাষা বৈশিষ্ট্য, আৰু সাংস্কৃতিক প্ৰসংগ তুমি জানা।
1449
+
1450
+ **অসমীয়াৰ বিশেষ নিৰ্দেশনা:**
1451
+ ১. **সাহিত্যিক বিশ্লেষণ অসমীয়াত দিবা**
1452
+ ২. **প্ৰশ্ন অনুসৰি উত্তৰ দিবা**""",
1453
+
1454
+ "guidance": "অসমীয়া ভাষাৰ সৌন্দ��্য্য আৰু গভীৰতা দেখুৱাব লাগে।"
1455
+ },
1456
+
1457
+ "📘 হিন্দী (Hindi)": {
1458
+ "base_prompt": """तुम एक हिंदी शिक्षक हो। SEBA दशम श्रेणी के {chapter_name} पाठ के सभी साहित्यिक तत्व, व्याकरण, और भाषा कौशल तुम जानते हो।
1459
+
1460
+ **हिंदी के विशेष निर्देश:**
1461
+ १. **साहित्यिक विश्लेषण हिंदी में देना, साथ असमिया व्याख्या देना**
1462
+ २. **प्रश्न के अनुसार उत्तर देना**""",
1463
+
1464
+ "guidance": "हिंदी वाक्य के साथ असमिया व्याख्या देना"
1465
+ }
1466
+ }
1467
+
1468
+ # ===============================
1469
+ # SIMPLE AUTHENTICATION UI
1470
+ # ===============================
1471
+ def show_simple_auth():
1472
+ """Show simple login/signup for testing"""
1473
+ st.markdown("""
1474
+ <div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #0d47a1 0%, #1976d2 100%);
1475
+ border-radius: 15px; color: white; margin-bottom: 2rem;">
1476
+ <h1 style="color: white;">🎓 SEBA AI Tutor</h1>
1477
+ <p>Assam's Smart AI Tutor for Class 10 Students</p>
1478
+ </div>
1479
+ """, unsafe_allow_html=True)
1480
+
1481
+ # Simple email-based authentication for testing
1482
+ st.subheader("🔐 Quick Login for Testing")
1483
+
1484
+ # Create two columns
1485
+ col1, col2 = st.columns(2)
1486
+
1487
+ with col1:
1488
+ st.markdown("#### Test with Sample Account")
1489
+ if st.button("Login as Test Student", use_container_width=True, type="primary"):
1490
+ test_email = "student@example.com"
1491
+ user_id = hashlib.md5(test_email.encode()).hexdigest()
1492
+
1493
+ st.session_state.user_id = user_id
1494
+ st.session_state.user_email = test_email
1495
+ st.session_state.logged_in = True
1496
+
1497
+ # Create user in database if subscription manager exists
1498
+ if st.session_state.subscription_manager:
1499
+ st.session_state.subscription_manager.create_user(user_id, email=test_email)
1500
+ st.session_state.user_plan = st.session_state.subscription_manager.get_user_plan(user_id)
1501
+
1502
+ st.success("✅ Logged in as test student!")
1503
+ time.sleep(1)
1504
+ st.rerun()
1505
+
1506
+ with col2:
1507
+ st.markdown("#### Use Your Email")
1508
+ user_email = st.text_input("Enter your email")
1509
+
1510
+ if st.button("Login / Sign Up", use_container_width=True, type="secondary"):
1511
+ if user_email and "@" in user_email:
1512
+ user_id = hashlib.md5(user_email.lower().strip().encode()).hexdigest()
1513
+
1514
+ st.session_state.user_id = user_id
1515
+ st.session_state.user_email = user_email
1516
+ st.session_state.logged_in = True
1517
+
1518
+ # Create user in database if subscription manager exists
1519
+ if st.session_state.subscription_manager:
1520
+ st.session_state.subscription_manager.create_user(user_id, email=user_email)
1521
+ st.session_state.user_plan = st.session_state.subscription_manager.get_user_plan(user_id)
1522
+
1523
+ st.success(f"✅ Welcome {user_email}!")
1524
+ time.sleep(1)
1525
+ st.rerun()
1526
+ else:
1527
+ st.error("Please enter a valid email address")
1528
+
1529
+ # Show features
1530
+ st.markdown("---")
1531
+ st.markdown("### ✨ Features")
1532
+
1533
+ features_col1, features_col2, features_col3 = st.columns(3)
1534
+
1535
+ with features_col1:
1536
+ st.markdown("""
1537
+ <div style="text-align: center; padding: 1rem;">
1538
+ <div style="font-size: 2.5rem;">📚</div>
1539
+ <h4>All SEBA Subjects</h4>
1540
+ <p>Mathematics, Science, Social Science, English, Assamese, Hindi</p>
1541
+ </div>
1542
+ """, unsafe_allow_html=True)
1543
+
1544
+ with features_col2:
1545
+ st.markdown("""
1546
+ <div style="text-align: center; padding: 1rem;">
1547
+ <div style="font-size: 2.5rem;">🤖</div>
1548
+ <h4>AI Tutor</h4>
1549
+ <p>Step-by-step explanations in Assamese & English</p>
1550
+ </div>
1551
+ """, unsafe_allow_html=True)
1552
+
1553
+ with features_col3:
1554
+ st.markdown("""
1555
+ <div style="text-align: center; padding: 1rem;">
1556
+ <div style="font-size: 2.5rem;">⚡</div>
1557
+ <h4>Instant Answers</h4>
1558
+ <p>Cached responses for faster learning</p>
1559
+ </div>
1560
+ """, unsafe_allow_html=True)
1561
+
1562
+ # Pricing info
1563
+ st.markdown("---")
1564
+ st.markdown("### 💰 Pricing Plans")
1565
+
1566
+ pricing_col1, pricing_col2, pricing_col3 = st.columns(3)
1567
+
1568
+ with pricing_col1:
1569
+ st.markdown("""
1570
+ <div style="border: 2px solid #4CAF50; border-radius: 10px; padding: 1rem; text-align: center;">
1571
+ <h3 style="color: #4CAF50;">🆓 Free</h3>
1572
+ <h2>₹0</h2>
1573
+ <p><strong>1 question/day</strong></p>
1574
+ <p>30 lifetime questions</p>
1575
+ <p>Perfect for trying out</p>
1576
+ </div>
1577
+ """, unsafe_allow_html=True)
1578
+
1579
+ with pricing_col2:
1580
+ st.markdown("""
1581
+ <div style="border: 2px solid #FF9800; border-radius: 10px; padding: 1rem; text-align: center; background: #FFF8E1;">
1582
+ <h3 style="color: #FF9800;">⭐ Pro</h3>
1583
+ <h2>₹50/month</h2>
1584
+ <p><strong>15 questions/day</strong></p>
1585
+ <p>450 questions/month</p>
1586
+ <p>Best for regular study</p>
1587
+ </div>
1588
+ """, unsafe_allow_html=True)
1589
+
1590
+ with pricing_col3:
1591
+ st.markdown("""
1592
+ <div style="border: 2px solid #2196F3; border-radius: 10px; padding: 1rem; text-align: center; background: #E3F2FD;">
1593
+ <h3 style="color: #2196F3;">✨ Pro Plus</h3>
1594
+ <h2>₹100/month</h2>
1595
+ <p><strong>40 questions/day</strong></p>
1596
+ <p>1200 questions/month</p>
1597
+ <p>For serious learners</p>
1598
+ </div>
1599
+ """, unsafe_allow_html=True)
1600
+
1601
+ def show_user_dashboard():
1602
+ """Show user dashboard in sidebar"""
1603
+ if st.session_state.get('logged_in'):
1604
+ st.sidebar.markdown("---")
1605
+ st.sidebar.markdown("### 👤 Your Account")
1606
+
1607
+ # Show user info
1608
+ if st.session_state.get('user_email'):
1609
+ st.sidebar.markdown(f"**Email:** {st.session_state.user_email}")
1610
+
1611
+ # Show plan info
1612
+ if st.session_state.get('user_plan'):
1613
+ plan = st.session_state.user_plan
1614
+ plan_name = plan['plan_type'].replace('_', ' ').title()
1615
+
1616
+ st.sidebar.markdown(f"**Plan:** {plan_name}")
1617
+ st.sidebar.markdown(f"**Used:** {plan['questions_used']}/{plan['total_questions']}")
1618
+
1619
+ # Progress bar
1620
+ progress = plan['questions_used'] / plan['total_questions']
1621
+ st.sidebar.progress(min(progress, 1.0))
1622
+
1623
+ # Daily usage stats
1624
+ if st.session_state.subscription_manager:
1625
+ stats = st.session_state.subscription_manager.get_user_stats(st.session_state.user_id)
1626
+ st.sidebar.markdown(f"**Today:** {stats['today_used']}/{plan['daily_limit']}")
1627
+
1628
+ # Upgrade button for free users
1629
+ if plan['plan_type'] == 'free':
1630
+ if st.sidebar.button("🚀 Upgrade Plan", use_container_width=True, type="primary"):
1631
+ st.session_state.show_subscription_plans = True
1632
+ st.rerun()
1633
+
1634
+ # Logout button
1635
+ if st.sidebar.button("🚪 Logout", use_container_width=True):
1636
+ for key in ['user_id', 'user_email', 'user_phone', 'logged_in', 'user_plan', 'show_subscription_plans']:
1637
+ if key in st.session_state:
1638
+ del st.session_state[key]
1639
+ st.rerun()
1640
+
1641
+ # ===============================
1642
+ # INITIALIZE SESSION STATE - FIXED
1643
+ # ===============================
1644
+ if 'history' not in st.session_state:
1645
+ st.session_state.history = []
1646
+ if 'current_subject' not in st.session_state:
1647
+ st.session_state.current_subject = "📐 গণিত (Mathematics)"
1648
+ if 'current_chapter' not in st.session_state:
1649
+ st.session_state.current_chapter = "অধ্যায় ১"
1650
+ if 'processing' not in st.session_state:
1651
+ st.session_state.processing = False
1652
+ if 'last_answer' not in st.session_state:
1653
+ st.session_state.last_answer = None
1654
+ if 'question_text' not in st.session_state:
1655
+ st.session_state.question_text = ""
1656
+ if 'streaming_answer' not in st.session_state:
1657
+ st.session_state.streaming_answer = ""
1658
+ if 'tokens_used' not in st.session_state:
1659
+ st.session_state.tokens_used = 0
1660
+ if 'cache_manager' not in st.session_state:
1661
+ st.session_state.cache_manager = SupabaseCache(ttl_days=7)
1662
+ # Pre-warm cache by checking Supabase connection on startup
1663
+ cache_stats = st.session_state.cache_manager.get_stats()
1664
+ if cache_stats['supabase_connected'] and cache_stats['supabase_entries'] > 0:
1665
+ st.toast(f"📦 Cache loaded: {cache_stats['supabase_entries']} entries available", icon="✅")
1666
+
1667
+ if 'show_cached_answer' not in st.session_state:
1668
+ st.session_state.show_cached_answer = False
1669
+ if 'cached_answer_data' not in st.session_state:
1670
+ st.session_state.cached_answer_data = None
1671
+ if 'current_cache_key' not in st.session_state:
1672
+ st.session_state.current_cache_key = None
1673
+ if 'cache_debug' not in st.session_state:
1674
+ st.session_state.cache_debug = False
1675
+
1676
+ # ===============================
1677
+ # SUBSCRIPTION MANAGER INITIALIZATION
1678
+ # ===============================
1679
+ if 'subscription_manager' not in st.session_state:
1680
+ if 'cache_manager' in st.session_state and st.session_state.cache_manager.supabase:
1681
+ st.session_state.subscription_manager = SupabaseSubscriptionManager(
1682
+ st.session_state.cache_manager.supabase
1683
+ )
1684
+ else:
1685
+ st.session_state.subscription_manager = None
1686
+
1687
+ # Initialize authentication state
1688
+ if 'logged_in' not in st.session_state:
1689
+ st.session_state.logged_in = False
1690
+ if 'user_id' not in st.session_state:
1691
+ st.session_state.user_id = None
1692
+ if 'user_email' not in st.session_state:
1693
+ st.session_state.user_email = None
1694
+ if 'user_phone' not in st.session_state:
1695
+ st.session_state.user_phone = None
1696
+ if 'show_subscription_plans' not in st.session_state:
1697
+ st.session_state.show_subscription_plans = False
1698
+ if 'user_plan' not in st.session_state:
1699
+ st.session_state.user_plan = None
1700
+
1701
+ # ===============================
1702
+ # AUTHENTICATION CHECK
1703
+ # ===============================
1704
+
1705
+ # Check if user needs to login
1706
+ if not st.session_state.get('logged_in'):
1707
+ show_simple_auth()
1708
+ st.stop()
1709
+
1710
+ # Show subscription plans if requested
1711
+ if st.session_state.get('show_subscription_plans'):
1712
+ st.title("📊 Upgrade Your Plan")
1713
+
1714
+ # Simple plans display
1715
+ col1, col2, col3 = st.columns(3)
1716
+
1717
+ with col1:
1718
+ st.markdown("""
1719
+ <div style="border: 2px solid #e0e0e0; border-radius: 10px; padding: 1.5rem; text-align: center; height: 100%;">
1720
+ <h3 style="color: #0d47a1;">🆓 Free</h3>
1721
+ <h1 style="color: #4CAF50;">₹0</h1>
1722
+ <p><strong>1 question/day</strong></p>
1723
+ <p>30 lifetime questions</p>
1724
+ <hr>
1725
+ <p>✓ Basic access</p>
1726
+ <p>✓ Cached answers</p>
1727
+ </div>
1728
+ """, unsafe_allow_html=True)
1729
+
1730
+ if st.session_state.get('user_plan', {}).get('plan_type') == 'free':
1731
+ st.success("✓ Current Plan")
1732
+ else:
1733
+ st.button("Select", disabled=True, use_container_width=True)
1734
+
1735
+ with col2:
1736
+ st.markdown("""
1737
+ <div style="border: 2px solid #FF9800; border-radius: 10px; padding: 1.5rem; text-align: center; height: 100%; background: #FFF8E1;">
1738
+ <h3 style="color: #FF9800;">⭐ Pro Monthly</h3>
1739
+ <h1 style="color: #FF9800;">₹50</h1>
1740
+ <p><strong>450 questions/month</strong></p>
1741
+ <p>≈ 15 questions/day</p>
1742
+ <hr>
1743
+ <p>✓ All Free features</p>
1744
+ <p>✓ Priority access</p>
1745
+ <p>✓ 30-day validity</p>
1746
+ </div>
1747
+ """, unsafe_allow_html=True)
1748
+
1749
+ if st.session_state.get('user_plan', {}).get('plan_type') == 'pro_monthly':
1750
+ st.success("✓ Current Plan")
1751
+ else:
1752
+ if st.button("Upgrade to Pro", type="primary", use_container_width=True):
1753
+ if st.session_state.subscription_manager:
1754
+ success, message = st.session_state.subscription_manager.upgrade_plan(
1755
+ st.session_state.user_id, 'pro_monthly'
1756
+ )
1757
+ if success:
1758
+ st.success(message)
1759
+ st.session_state.user_plan = st.session_state.subscription_manager.get_user_plan(st.session_state.user_id)
1760
+ time.sleep(2)
1761
+ st.session_state.show_subscription_plans = False
1762
+ st.rerun()
1763
+ else:
1764
+ st.error(message)
1765
+
1766
+ with col3:
1767
+ st.markdown("""
1768
+ <div style="border: 2px solid #2196F3; border-radius: 10px; padding: 1.5rem; text-align: center; height: 100%; background: #E3F2FD;">
1769
+ <h3 style="color: #2196F3;">✨ Pro Plus</h3>
1770
+ <h1 style="color: #2196F3;">₹100</h1>
1771
+ <p><strong>1200 questions/month</strong></p>
1772
+ <p>≈ 40 questions/day</p>
1773
+ <hr>
1774
+ <p>✓ All Pro features</p>
1775
+ <p>✓ Highest priority</p>
1776
+ <p>✓ 30-day validity</p>
1777
+ </div>
1778
+ """, unsafe_allow_html=True)
1779
+
1780
+ if st.session_state.get('user_plan', {}).get('plan_type') == 'pro_plus':
1781
+ st.success("✓ Current Plan")
1782
+ else:
1783
+ if st.button("Upgrade to Pro Plus", type="primary", use_container_width=True):
1784
+ if st.session_state.subscription_manager:
1785
+ success, message = st.session_state.subscription_manager.upgrade_plan(
1786
+ st.session_state.user_id, 'pro_plus'
1787
+ )
1788
+ if success:
1789
+ st.success(message)
1790
+ st.session_state.user_plan = st.session_state.subscription_manager.get_user_plan(st.session_state.user_id)
1791
+ time.sleep(2)
1792
+ st.session_state.show_subscription_plans = False
1793
+ st.rerun()
1794
+ else:
1795
+ st.error(message)
1796
+
1797
+ st.markdown("---")
1798
+ st.info("💡 **Note:** For now, this is a simulation. Real payment integration will be added soon.")
1799
+
1800
+ if st.button("← Back to Tutor", use_container_width=True):
1801
+ st.session_state.show_subscription_plans = False
1802
+ st.rerun()
1803
+
1804
+ st.stop()
1805
+
1806
+ # User is logged in - show main app
1807
+ # Load user plan if not loaded
1808
+ if st.session_state.logged_in and not st.session_state.get('user_plan'):
1809
+ if st.session_state.subscription_manager and st.session_state.user_id:
1810
+ st.session_state.user_plan = st.session_state.subscription_manager.get_user_plan(st.session_state.user_id)
1811
+ if not st.session_state.user_plan:
1812
+ # Create user if doesn't exist
1813
+ st.session_state.subscription_manager.create_user(
1814
+ st.session_state.user_id,
1815
+ email=st.session_state.get('user_email')
1816
+ )
1817
+ st.session_state.user_plan = st.session_state.subscription_manager.get_user_plan(st.session_state.user_id)
1818
+
1819
+ # Show user dashboard in sidebar
1820
+ show_user_dashboard()
1821
+
1822
+ # ===============================
1823
+ # HEADER SECTION
1824
+ # ===============================
1825
+ st.markdown("""
1826
+ <div class="header-container">
1827
+ <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem;">
1828
+ <div style="font-size: 2rem;">🎓</div>
1829
+ <div>
1830
+ <h1 class="assamese-title">
1831
+ নমস্কাৰ! মই আপোনাৰ দশম শ্ৰেণীৰ AI শিক্ষক
1832
+ </h1>
1833
+ <p class="assamese-text">
1834
+ <span class="assamese-highlight">SEBAৰ সকলো বিষয় মই জানো</span> – গণিত, বিজ্ঞান, সমাজ বিজ্ঞান, ইংৰাজী, অসমীয়া, হিন্দী ইত্যাদি।
1835
+ </p>
1836
+ </div>
1837
+ </div>
1838
+ </div>
1839
+ """, unsafe_allow_html=True)
1840
+
1841
+ # ===============================
1842
+ # USAGE BANNER
1843
+ # ===============================
1844
+ if st.session_state.get('logged_in') and st.session_state.get('user_plan'):
1845
+ user_plan = st.session_state.user_plan
1846
+ remaining = user_plan['questions_remaining']
1847
+ plan_name = user_plan['plan_type'].replace('_', ' ').title()
1848
+
1849
+ # Get today's usage
1850
+ today_used = 0
1851
+ if st.session_state.subscription_manager:
1852
+ stats = st.session_state.subscription_manager.get_user_stats(st.session_state.user_id)
1853
+ today_used = stats['today_used']
1854
+
1855
+ # Determine banner color based on usage
1856
+ if remaining <= 5 or today_used >= user_plan['daily_limit']:
1857
+ # Warning banner
1858
+ banner_color = "linear-gradient(135deg, #FF5722 0%, #FF9800 100%)"
1859
+ border_color = "#D32F2F"
1860
+ elif user_plan['plan_type'] == 'free':
1861
+ # Info banner for free users
1862
+ banner_color = "linear-gradient(135deg, #2196F3 0%, #1976D2 100%)"
1863
+ border_color = "#0D47A1"
1864
+ else:
1865
+ # Success banner for paid users
1866
+ banner_color = "linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)"
1867
+ border_color = "#1B5E20"
1868
+
1869
+ st.markdown(f"""
1870
+ <div style="background: {banner_color};
1871
+ color: white;
1872
+ padding: 0.75rem 1rem;
1873
+ border-radius: 10px;
1874
+ margin: 0.5rem 0 1.5rem 0;
1875
+ border-left: 5px solid {border_color};
1876
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);">
1877
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1878
+ <div>
1879
+ <strong style="font-size: 1rem;">📊 {plan_name} Plan</strong>
1880
+ <div style="font-size: 0.85rem; opacity: 0.9;">
1881
+ Today: {today_used}/{user_plan['daily_limit']} • Remaining: {remaining}/{user_plan['total_questions']}
1882
+ </div>
1883
+ </div>
1884
+ <div>
1885
+ <span style="background: rgba(255,255,255,0.2);
1886
+ padding: 0.3rem 0.8rem;
1887
+ border-radius: 5px;
1888
+ font-size: 0.85rem;
1889
+ cursor: pointer;"
1890
+ onclick="window.location.href='?show_plans=true'">
1891
+ { 'Upgrade' if user_plan['plan_type'] == 'free' else 'Manage' } →
1892
+ </span>
1893
+ </div>
1894
+ </div>
1895
+ </div>
1896
+ """, unsafe_allow_html=True)
1897
+
1898
+ # ===============================
1899
+ # CONTROL PANEL SECTION
1900
+ # ===============================
1901
+ st.markdown('<div class="control-panel">', unsafe_allow_html=True)
1902
+
1903
+ control_col1, control_col2 = st.columns(2)
1904
+ with control_col1:
1905
+ st.markdown("#### 📚 বিষয় বাছনি কৰক")
1906
+ subject_list = list(SEBA_CURRICULUM.keys())
1907
+ current_subject = st.session_state.current_subject
1908
+ current_index = subject_list.index(current_subject) if current_subject in subject_list else 0
1909
+
1910
+ selected_subject = st.selectbox(
1911
+ "আপুনি কোনটো বিষয় শিকিব বিচাৰে?",
1912
+ subject_list,
1913
+ index=current_index,
1914
+ key="subject_selector",
1915
+ label_visibility="collapsed"
1916
+ )
1917
+
1918
+ if selected_subject != st.session_state.current_subject:
1919
+ st.session_state.current_subject = selected_subject
1920
+ chapters = SEBA_CURRICULUM[selected_subject]
1921
+ st.session_state.current_chapter = list(chapters.keys())[0]
1922
+
1923
+ with control_col2:
1924
+ st.markdown("#### 📖 অধ্যায় বাছনি কৰক")
1925
+ chapters = SEBA_CURRICULUM[selected_subject]
1926
+
1927
+ chapter_options = []
1928
+ chapter_display_map = {}
1929
+ for chap_num, chap_name in chapters.items():
1930
+ display_text = f"{chap_num}: {chap_name}"
1931
+ chapter_options.append(display_text)
1932
+ chapter_display_map[display_text] = chap_num
1933
+
1934
+ current_chapter = st.session_state.current_chapter
1935
+ current_chap_display = next((disp for disp, num in chapter_display_map.items() if num == current_chapter), chapter_options[0])
1936
+ current_chap_index = chapter_options.index(current_chap_display) if current_chap_display in chapter_options else 0
1937
+
1938
+ selected_chapter_display = st.selectbox(
1939
+ "কোন অধ্যায়ৰ পৰা প্ৰশ্ন সুধিব?",
1940
+ chapter_options,
1941
+ index=current_chap_index,
1942
+ key="chapter_selector",
1943
+ label_visibility="collapsed"
1944
+ )
1945
+
1946
+ selected_chapter_key = chapter_display_map[selected_chapter_display]
1947
+ if selected_chapter_key != st.session_state.current_chapter:
1948
+ st.session_state.current_chapter = selected_chapter_key
1949
+
1950
+ st.markdown('</div>', unsafe_allow_html=True)
1951
+
1952
+ # ===============================
1953
+ # CURRENT SELECTION INFO
1954
+ # ===============================
1955
+ current_chapter_name = chapters[selected_chapter_key]
1956
+ st.info(f"""
1957
+ **📚 বৰ্তমানৰ বিষয়:** {selected_subject}
1958
+ **📖 বৰ্তমানৰ অধ্যায়:** {current_chapter_name}
1959
+ """)
1960
+
1961
+ # ===============================
1962
+ # QUESTION INPUT AREA
1963
+ # ===============================
1964
+ st.markdown("---")
1965
+ st.markdown("#### ✍️ আপোনাৰ প্ৰশ্নটো ইয়াত লিখক")
1966
+
1967
+ question = st.text_area(
1968
+ "আপোনাৰ প্ৰশ্নটো ইয়াত লিখক:",
1969
+ value=st.session_state.question_text,
1970
+ height=100,
1971
+ placeholder=f"উদাহৰণ: '{current_chapter_name}' অধ্যায়টো মোৰ বাবে বুজাই দিয়ক...",
1972
+ key="question_input",
1973
+ label_visibility="collapsed"
1974
+ )
1975
+
1976
+ if question != st.session_state.question_text:
1977
+ st.session_state.question_text = question
1978
+
1979
+ # Show API key status
1980
+ if not api_key:
1981
+ st.error("""
1982
+ ⚠️ **API কি ছেট আপ কৰক:**
1983
+
1984
+ **Hugging Face Spaces:**
1985
+ ১. Space Settings → Repository secrets
1986
+ ২. `DEEPSEEK_API_KEY` যোগ কৰক
1987
+ ৩. আপোনাৰ DeepSeek API কি দিয়ক
1988
+
1989
+ **স্থানীয়ভাবে:**
1990
+ ```bash
1991
+ export DEEPSEEK_API_KEY="your-api-key-here"
1992
+ ```
1993
+ """)
1994
+
1995
+ # ===============================
1996
+ # CACHE CHECK AND SUBMIT BUTTON - WITH SUBSCRIPTION CHECK
1997
+ # ===============================
1998
+ submit_disabled = not (question.strip() and api_key)
1999
+ col1, col2, col3 = st.columns([1, 2, 1])
2000
+
2001
+ with col2:
2002
+ if st.button(
2003
+ "🚀 উত্তৰ দিবলৈ দিয়ক!",
2004
+ type="primary",
2005
+ use_container_width=True,
2006
+ disabled=submit_disabled
2007
+ ):
2008
+ if not question.strip():
2009
+ st.error("❌ অনুগ্ৰহ কৰি প্ৰশ্নটো লিখক!")
2010
+ elif not api_key:
2011
+ st.error("❌ API কি ছেট আপ কৰক!")
2012
+ else:
2013
+ # Check subscription if available
2014
+ if st.session_state.subscription_manager and st.session_state.get('user_id'):
2015
+ can_ask, message = st.session_state.subscription_manager.can_ask_question(
2016
+ st.session_state.user_id
2017
+ )
2018
+
2019
+ if not can_ask:
2020
+ st.error(f"❌ {message}")
2021
+
2022
+ # Show upgrade option for free users
2023
+ user_plan = st.session_state.get('user_plan')
2024
+ if user_plan and user_plan.get('plan_type') == 'free':
2025
+ st.info("💡 Upgrade to Pro for more questions!")
2026
+ if st.button("View Upgrade Plans", key="upgrade_from_error"):
2027
+ st.session_state.show_subscription_plans = True
2028
+ st.rerun()
2029
+
2030
+ st.stop()
2031
+
2032
+ # Check cache first
2033
+ cache_key = create_cache_key(question, selected_subject, current_chapter_name)
2034
+
2035
+ # Get cache stats for debugging
2036
+ cache_stats = st.session_state.cache_manager.get_stats()
2037
+
2038
+ cached_entry = st.session_state.cache_manager.get(cache_key)
2039
+
2040
+ if cached_entry:
2041
+ # Determine cache source
2042
+ cache_source = "Memory" if cache_key in st.session_state.cache_manager.memory_cache else "Supabase"
2043
+
2044
+ # Log the cached question usage
2045
+ if st.session_state.subscription_manager and st.session_state.get('user_id'):
2046
+ st.session_state.subscription_manager.log_question(
2047
+ user_id=st.session_state.user_id,
2048
+ question_hash=cache_key,
2049
+ subject=selected_subject,
2050
+ chapter=current_chapter_name,
2051
+ tokens_used=cached_entry.get('tokens', 0),
2052
+ cached=True
2053
+ )
2054
+
2055
+ # Show cached answer
2056
+ st.session_state.show_cached_answer = True
2057
+ st.session_state.cached_answer_data = cached_entry
2058
+ st.session_state.current_cache_key = cache_key
2059
+ st.session_state.processing = False
2060
+ st.session_state.cache_source = cache_source
2061
+ else:
2062
+ # Not in cache, proceed with API call
2063
+ # Log the new question usage
2064
+ if st.session_state.subscription_manager and st.session_state.get('user_id'):
2065
+ st.session_state.subscription_manager.log_question(
2066
+ user_id=st.session_state.user_id,
2067
+ question_hash=cache_key,
2068
+ subject=selected_subject,
2069
+ chapter=current_chapter_name,
2070
+ cached=False
2071
+ )
2072
+
2073
+ st.session_state.processing = True
2074
+ st.session_state.current_cache_key = cache_key
2075
+
2076
+ # ===============================
2077
+ # DISPLAY CACHED ANSWER WITH THINKING ANIMATION
2078
+ # ===============================
2079
+ if st.session_state.get('show_cached_answer') and st.session_state.get('cached_answer_data'):
2080
+ cached_data = st.session_state.cached_answer_data
2081
+ cache_source = st.session_state.get('cache_source', 'Cache')
2082
+
2083
+ # Display cached answer with animation
2084
+ display_cached_answer_with_animation(
2085
+ cached_data,
2086
+ question,
2087
+ selected_subject,
2088
+ current_chapter_name,
2089
+ cache_source
2090
+ )
2091
+
2092
+ # Reset flag
2093
+ st.session_state.show_cached_answer = False
2094
+ if 'cached_answer_data' in st.session_state:
2095
+ del st.session_state.cached_answer_data
2096
+ if 'current_cache_key' in st.session_state:
2097
+ del st.session_state.current_cache_key
2098
+
2099
+ # ===============================
2100
+ # PROCESS QUESTION WITH STREAMING AND THINKING ANIMATION
2101
+ # ===============================
2102
+ if st.session_state.get('processing') and question and api_key:
2103
+ # Display user question
2104
+ st.markdown(f"""
2105
+ <div class="chat-container">
2106
+ <div style="display: flex; justify-content: flex-end; margin-bottom: 0.3rem;">
2107
+ <div class="user-bubble">
2108
+ <div style="font-weight: 600; margin-bottom: 0.2rem;">👤 আপুনি:</div>
2109
+ <div>{question[:200]}{'...' if len(question) > 200 else ''}</div>
2110
+ </div>
2111
+ </div>
2112
+ """, unsafe_allow_html=True)
2113
+
2114
+ # AI answer header with thinking animation initially
2115
+ st.markdown(f"""
2116
+ <div style="display: flex; align-items: flex-start; margin-bottom: 0.3rem;">
2117
+ <div style="margin-right: 0.5rem; font-size: 1.2rem;">🤖</div>
2118
+ <div style="flex: 1;">
2119
+ <div class="ai-bubble">
2120
+ <div style="display: flex; align-items: center; margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 2px solid #2196F3;">
2121
+ <div style="display: flex; align-items: center;">
2122
+ <div style="background: #2196F3; color: white; padding: 0.2rem 0.5rem; border-radius: 8px;
2123
+ font-weight: 600; font-size: 0.8rem; margin-right: 0.5rem;">
2124
+ AI টিউটাৰ
2125
+ </div>
2126
+ <div style="font-weight: 600; color: #0d47a1; font-size: 0.9rem;">
2127
+ {selected_subject} • {current_chapter_name}
2128
+ </div>
2129
+ </div>
2130
+ <div style="font-size: 0.75rem; color: #666; background: #e3f2fd; padding: 0.2rem 0.5rem; border-radius: 4px;">
2131
+ <span style="margin-right: 0.3rem;">⚡</span> Generating...
2132
+ </div>
2133
+ </div>
2134
+ <div id="ai-answer-content" style="color: #333; line-height: 1.5; font-size: 0.95rem;">
2135
+ <!-- Answer will be streamed here -->
2136
+ </div>
2137
+ </div>
2138
+ </div>
2139
+ </div>
2140
+ </div>
2141
+ """, unsafe_allow_html=True)
2142
+
2143
+ # Create a placeholder for the thinking animation
2144
+ thinking_placeholder = st.empty()
2145
+
2146
+ # Show thinking animation
2147
+ thinking_placeholder.markdown("""
2148
+ <div class="progress-indicator">
2149
+ <span>উত্তৰ প্ৰস্তুত কৰি আছো...</span>
2150
+ <div class="thinking-dots">
2151
+ <span></span>
2152
+ <span></span>
2153
+ <span></span>
2154
+ </div>
2155
+ </div>
2156
+ """, unsafe_allow_html=True)
2157
+
2158
+ # Get the prompt and stream the response
2159
+ system_prompt = get_subject_prompt(selected_subject, current_chapter_name, question)
2160
+
2161
+ # Stream the response
2162
+ stream_deepseek_response(system_prompt, question, selected_subject, current_chapter_name)
2163
+
2164
+ st.session_state.processing = False
2165
+
2166
+ # ===============================
2167
+ # HISTORY
2168
+ # ===============================
2169
+ if st.session_state.history:
2170
+ st.markdown("---")
2171
+ st.markdown("#### 📜 আজিৰ প্ৰশ্নাৱলী")
2172
+
2173
+ for i, item in enumerate(reversed(st.session_state.history[-5:]), 1):
2174
+ cache_indicator = " ⚡" if item.get('cached') else " 🤖"
2175
+ cache_source = f" ({item.get('cache_source', 'API')})" if item.get('cached') else ""
2176
+
2177
+ with st.expander(f"প্ৰশ্ন {i}: {item['question']} ({item['timestamp']}{cache_indicator}{cache_source})"):
2178
+ st.write(f"**বিষয়:** {item['subject']}")
2179
+ st.write(f"**অধ্যায়:** {item['chapter']}")
2180
+ st.write(f"**ট'কেন:** {item.get('tokens', 0):,}")
2181
+ if item.get('cached'):
2182
+ st.caption(f"⚡ This answer was served from {item.get('cache_source', 'cache')}")
2183
+
2184
+ # ===============================
2185
+ # FOOTER
2186
+ # ===============================
2187
+ st.markdown("---")
2188
+ st.markdown("""
2189
+ <div style="text-align: center; padding: 0.5rem;">
2190
+ <h3 style="color: #0d47a1; margin-bottom: 0.5rem;">
2191
+ 🎓 আপোনাৰ সফলতাৰ বাবে মই সদায় আছো!
2192
+ </h3>
2193
+ </div>
2194
+ """, unsafe_allow_html=True)
2195
+
2196
+ st.markdown("""
2197
+ <div style="text-align: center; padding: 0.5rem; margin-top: 1rem; color: #1976D2; font-size: 0.8rem;">
2198
+ <p style="margin: 0;">© 2025 Jajabor AI. All rights reserved.</p>
2199
+ </div>
2200
+ """, unsafe_allow_html=True)