Universities commited on
Commit
a044fee
Β·
verified Β·
1 Parent(s): 33ddfb9

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +19 -0
  2. README.md +29 -0
  3. app.py +489 -0
  4. auth.py +190 -0
  5. requirements.txt +8 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ gcc \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ EXPOSE 7860
15
+
16
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
17
+ ENV GRADIO_SERVER_PORT=7860
18
+
19
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: EduClone AI Portal
3
+ emoji: πŸŽ“
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # πŸŽ“ EduClone AI β€” Exam Intelligence Portal
13
+
14
+ ## Default Admin Login
15
+ - **Username:** `abdulqayyum`
16
+ - **Password:** `Admin@EduClone2025`
17
+ > ⚠️ Change `ADMIN_PASSWORD` in `auth.py` before going public.
18
+
19
+ ## Roles
20
+ | Role | Access |
21
+ |---|---|
22
+ | **Admin** | Dashboard Β· User management Β· Activity log Β· Full MCQ tools |
23
+ | **User** | Upload MCQs Β· Format Β· Download Word / PDF / Excel |
24
+
25
+ ## Files
26
+ - `Dockerfile` β€” forces Python 3.11, avoids audioop/pydub conflict
27
+ - `app.py` β€” Gradio portal
28
+ - `auth.py` β€” SQLite + bcrypt auth
29
+ - `mcq_engine.py` β€” MCQ parser and document builder
app.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EduClone AI Portal β€” Login / Register / Admin / User roles
3
+ Tab 1: Generate MCQs from topic (AI, 4 options, answer always provided)
4
+ Tab 2: Upload MCQs β†’ smart answer detection β†’ clone with 4 distractors
5
+ Author: Abdulqayyum MBA
6
+ """
7
+ import gradio as gr
8
+ import pandas as pd
9
+ import datetime
10
+ from auth import (init_db, register_user, login_user, log_activity,
11
+ get_all_users, toggle_user_active, change_user_role,
12
+ delete_user, get_activity_log, get_stats)
13
+ from mcq_engine import (
14
+ generate_from_topic, read_and_parse, clone_mcqs, preview_mcqs,
15
+ make_word, make_pdf, make_excel, BLOOMS
16
+ )
17
+
18
+ init_db()
19
+
20
+ # ── CSS ───────────────────────────────────────────────────────────────────────
21
+ CSS = """
22
+ * { box-sizing: border-box; }
23
+ body { font-family: 'Inter', sans-serif; margin: 0; }
24
+ .portal-banner {
25
+ background: linear-gradient(135deg,#1e3a5f 0%,#1a56db 55%,#059669 100%);
26
+ padding: 20px 28px 18px; border-radius: 14px; color: white; margin-bottom: 18px;
27
+ }
28
+ .portal-banner h1 { margin:0; font-size:1.65rem; letter-spacing:-.3px; font-weight:600; }
29
+ .portal-banner p { margin:5px 0 0; opacity:.88; font-size:.88rem; }
30
+ .auth-card {
31
+ background: var(--color-background-primary);
32
+ border: 1px solid var(--color-border-primary);
33
+ border-radius: 14px; padding: 28px 32px; max-width: 460px;
34
+ margin: 0 auto; box-shadow: 0 4px 24px rgba(0,0,0,.07);
35
+ }
36
+ .auth-card h2 { margin:0 0 18px; font-size:1.25rem; color:#1e3a5f; }
37
+ .auth-divider { text-align:center; color:#9ca3af; margin:10px 0; font-size:.85rem; }
38
+ .user-bar {
39
+ background: var(--color-background-secondary);
40
+ border: 1px solid var(--color-border-tertiary);
41
+ border-radius: 10px; padding: 12px 18px;
42
+ display: flex; align-items: center; justify-content: space-between;
43
+ margin-bottom: 14px; font-size: .9rem;
44
+ }
45
+ .role-badge { font-size:.78rem; font-weight:600; padding:3px 10px; border-radius:20px; }
46
+ .role-admin { background:#dbeafe; color:#1e40af; }
47
+ .role-user { background:#dcfce7; color:#166534; }
48
+ .hint-box {
49
+ background: #fefce8; border: 1px solid #fde68a;
50
+ border-radius: 8px; padding: 10px 14px; font-size:.85rem; line-height:1.7;
51
+ margin-bottom: 12px;
52
+ }
53
+ .detect-box {
54
+ background: #f0fdf4; border: 1px solid #a7f3d0;
55
+ border-radius: 8px; padding: 10px 14px; font-size:.85rem; line-height:1.7;
56
+ margin-bottom: 12px;
57
+ }
58
+ .dl-row { background:#f0fdf4; border:1px solid #a7f3d0; border-radius:8px; padding:12px; margin-top:8px; }
59
+ footer { display:none !important; }
60
+ """
61
+
62
+ BANNER_HTML = """
63
+ <div class="portal-banner">
64
+ <h1>πŸŽ“ EduClone AI β€” Exam Intelligence Portal</h1>
65
+ <p>Generate Β· Upload Β· Clone MCQs Β· Download Word / PDF / Excel
66
+ &nbsp;|&nbsp; <strong>Abdulqayyum MBA</strong> β€” 18 yrs Academic Assessment Β· MBA Β· AI Governance Β· Azure AI</p>
67
+ </div>
68
+ """
69
+
70
+ TAB1_HINT = """<div class="hint-box">
71
+ <b>πŸ“ Tab 1 β€” Generate MCQs from a Topic:</b>
72
+ Enter any subject β†’ choose Bloom's level β†’ AI generates fresh MCQs with
73
+ <b>4 options (A B C D)</b> and a correct answer. Download as Word / PDF / Excel.
74
+ </div>"""
75
+
76
+ TAB2_HINT = """<div class="hint-box">
77
+ <b>πŸ” Tab 2 β€” Upload &amp; Clone your existing MCQs:</b><br>
78
+ Upload <b>.docx / .pdf / .txt</b> file with your questions. The system detects the correct answer using:<br>
79
+ &nbsp;&nbsp;<b>1.</b> Bold option text &nbsp;
80
+ <b>2.</b> Underlined option text &nbsp;
81
+ <b>3.</b> Coloured (non-black) option text &nbsp;
82
+ <b>4.</b> Highlighted option text &nbsp;
83
+ <b>5.</b> <code>Answer: X</code> line at the end<br>
84
+ If none of these are found, the answer is left as <b>⚠️ Unknown</b> β€” no guess is made.
85
+ Then the AI clones each question with <b>4 new distractors</b>.
86
+ </div>"""
87
+
88
+ SAMPLE_MCQS = """1. Your patient is a 44-year-old female, alert and oriented, in moderate distress, complaining of difficulty breathing. She has a 1-week history of fever and malaise, shortness of breath developing 3 days ago. Left-sided chest pain with deep inspiration and a cough with sputum. Examination: hot, pale, dry skin; rhonchi and rales throughout the left lung. Right lung clear.
89
+
90
+ HR = 134, BP = 88/64, RR = 24, SpO2 = 92%.
91
+
92
+ History of two previous myocardial infarctions. Takes nitroglycerin as needed. Which prehospital management is most appropriate?
93
+
94
+ A. Oxygen via venturi mask 24-35%, nebulized albuterol and ipratropium, IV NS KVO, IV corticosteroids
95
+ B. Endotracheal intubation, 100% oxygen, IV NS KVO, nebulized albuterol and Atrovent, IV corticosteroids
96
+ C. Albuterol via nebulizer with 100% oxygen, IV NS KVO
97
+ D. Oxygen via nonrebreathing mask 15 LPM, IV NS with fluid challenge
98
+
99
+ Answer: D
100
+
101
+ 2. A 60-year-old male presents with sudden crushing chest pain radiating to left arm and jaw. Diaphoretic and pale. BP = 90/60, HR = 110, RR = 22, SpO2 = 94%. Most appropriate prehospital intervention?
102
+
103
+ A. Aspirin 324 mg PO, nitroglycerin 0.4 mg SL, oxygen via NRB mask, IV access, transport immediately
104
+ B. Morphine IV, 12-lead ECG, transport to nearest hospital without further intervention
105
+ C. Apply CPAP at 10 cmH2O, furosemide IV, IV access
106
+ D. Endotracheal intubation, lidocaine IV, transport Code 3
107
+
108
+ Answer: A
109
+ """
110
+
111
+ # ── session helpers ───────────────────────────────────────────────────────────
112
+ def _logged_in(s): return s.get("logged_in", False)
113
+ def _is_admin(s): return s.get("role") == "admin"
114
+
115
+ def _welcome_html(session):
116
+ role = session.get("role","user")
117
+ name = session.get("full_name") or session.get("username","")
118
+ bc = "role-admin" if role=="admin" else "role-user"
119
+ bl = "πŸ”‘ Admin" if role=="admin" else "πŸ‘€ User"
120
+ return (f'<div class="user-bar">'
121
+ f'<span>πŸ‘‹ Welcome, <strong>{name}</strong> &nbsp;|&nbsp;'
122
+ f'<span class="role-badge {bc}">{bl}</span></span>'
123
+ f'<span style="font-size:.82rem;color:#6b7280;">'
124
+ f'{datetime.datetime.now():%d %b %Y %H:%M}</span></div>')
125
+
126
+
127
+ # ── auth handlers ─────────────────────────────────────────────────────────────
128
+ def do_login(username, password, session):
129
+ ok, msg, user = login_user(username, password)
130
+ if ok:
131
+ session.update({"logged_in": True, "username": user["username"],
132
+ "role": user["role"], "full_name": user.get("full_name",""),
133
+ "user_id": user["id"]})
134
+ return (msg, session,
135
+ gr.update(visible=False),
136
+ gr.update(visible=True),
137
+ _welcome_html(session),
138
+ gr.update(visible=_is_admin(session)),
139
+ gr.update(visible=True))
140
+ return msg, session, gr.update(visible=True), gr.update(visible=False), \
141
+ "", gr.update(visible=False), gr.update(visible=False)
142
+
143
+ def do_register(username, email, password, confirm, full_name, institution, session):
144
+ if password != confirm:
145
+ return "❌ Passwords do not match.", session
146
+ ok, msg = register_user(username, email, password, full_name, institution)
147
+ return msg, session
148
+
149
+ def do_logout(session):
150
+ if session.get("username"):
151
+ log_activity(session.get("user_id"), session.get("username"), "logout", "")
152
+ session.clear()
153
+ return (session, gr.update(visible=True), gr.update(visible=False),
154
+ "", gr.update(visible=False), gr.update(visible=False))
155
+
156
+
157
+ # ── Tab 1: generate from topic ────────────────────────────────────────────────
158
+ def handle_generate(topic, num_q, blooms, difficulty, want_word, want_pdf, want_excel, session):
159
+ if not _logged_in(session):
160
+ return "⚠️ Please log in first.", None, None, None, ""
161
+ if not topic.strip():
162
+ return "⚠️ Please enter a topic name.", None, None, None, ""
163
+
164
+ mcqs, source = generate_from_topic(topic, int(num_q), blooms, difficulty)
165
+ if not mcqs:
166
+ return "❌ Could not generate questions. Please try again.", None, None, None, ""
167
+
168
+ log_activity(session.get("user_id"), session.get("username"),
169
+ "generate_topic", f"{len(mcqs)} Qs Β· {topic} Β· {blooms}")
170
+
171
+ note = ("⚑ AI was busy β€” template questions shown. Re-run for AI results.\n\n"
172
+ if source == "template" else "βœ… AI-generated questions:\n\n")
173
+ preview = preview_mcqs(mcqs, f"Topic: {topic} | Bloom's: {blooms} | Difficulty: {difficulty}")
174
+
175
+ title = f"MCQs: {topic}"
176
+ w = make_word(mcqs, title) if want_word else None
177
+ p = make_pdf(mcqs, title) if want_pdf else None
178
+ e = make_excel(mcqs, title) if want_excel else None
179
+
180
+ metrics = (f"πŸ“Š Questions: **{len(mcqs)}** Β· Bloom's: **{blooms}** Β· "
181
+ f"Difficulty: **{difficulty}** Β· Source: **{source}**")
182
+ return note + preview, w, p, e, metrics
183
+
184
+
185
+ # ── Tab 2: upload + clone ─────────────────────────────────────────────────────
186
+ def handle_upload_preview(file_obj, paste_text, session):
187
+ """Step 1: parse uploaded file, show what was detected."""
188
+ if not _logged_in(session):
189
+ return "⚠️ Please log in first.", session
190
+
191
+ mcqs, err = [], ""
192
+ if file_obj:
193
+ mcqs, err = read_and_parse(file_obj)
194
+ elif paste_text.strip():
195
+ from mcq_engine import _parse_plain
196
+ mcqs = _parse_plain(paste_text.strip())
197
+ else:
198
+ return "⚠️ Please upload a file or paste MCQs.", session
199
+
200
+ if err:
201
+ return f"❌ {err}", session
202
+ if not mcqs:
203
+ return ("❌ Could not detect MCQ structure.\n\n"
204
+ "Ensure questions start with `1.` or `Q1.` and options with `A.` / `A)`"), session
205
+
206
+ # Store in session for cloning step
207
+ session["_mcqs"] = mcqs
208
+ detected = sum(1 for m in mcqs if m.get("answer"))
209
+ unknown = len(mcqs) - detected
210
+
211
+ summary = f"βœ… **{len(mcqs)} question(s) parsed**"
212
+ if detected:
213
+ summary += f" Β· **{detected}** answer(s) detected"
214
+ if unknown:
215
+ summary += f" · **{unknown}** answer(s) ⚠️ unknown (no formatting mark or key found)"
216
+ summary += "\n\n"
217
+ summary += preview_mcqs(mcqs)
218
+ return summary, session
219
+
220
+
221
+ def handle_clone(num_clones, blooms, want_word, want_pdf, want_excel, exam_title, session):
222
+ """Step 2: clone the already-parsed MCQs."""
223
+ if not _logged_in(session):
224
+ return "⚠️ Please log in first.", None, None, None, ""
225
+ originals = session.get("_mcqs", [])
226
+ if not originals:
227
+ return "⚠️ Please parse/upload MCQs first (click Preview above).", None, None, None, ""
228
+
229
+ clones = clone_mcqs(originals, int(num_clones), blooms)
230
+ log_activity(session.get("user_id"), session.get("username"),
231
+ "clone_mcqs", f"{len(originals)} orig β†’ {len(clones)} clones")
232
+
233
+ title = exam_title.strip() or "Cloned MCQs"
234
+ w = make_word(clones, title) if want_word else None
235
+ p = make_pdf(clones, title) if want_pdf else None
236
+ e = make_excel(clones, title) if want_excel else None
237
+
238
+ metrics = (f"πŸ“Š Original: **{len(originals)}** Β· "
239
+ f"Clones: **{len(clones)}** Β· "
240
+ f"Bloom's: **{blooms}**")
241
+ return preview_mcqs(clones, "Generated Clones"), w, p, e, metrics
242
+
243
+
244
+ # ── admin handlers (unchanged) ────────────────────────────────────────────────
245
+ def admin_get_stats(session):
246
+ if not _is_admin(session): return "❌ Admin only."
247
+ s = get_stats()
248
+ return (f"## πŸ“Š Portal Statistics\n|Metric|Value|\n|---|---|\n"
249
+ f"|Total Users|**{s['total']}**|\n|Admins|**{s['admins']}**|\n"
250
+ f"|Active|**{s['active']}**|\n|New Today|**{s['new_today']}**|\n"
251
+ f"|Logins Today|**{s['logins_today']}**|")
252
+
253
+ def admin_get_users(session):
254
+ if not _is_admin(session): return pd.DataFrame()
255
+ users = get_all_users()
256
+ if not users: return pd.DataFrame()
257
+ df = pd.DataFrame(users)[["id","username","email","full_name","institution",
258
+ "role","is_active","created_at","last_login"]]
259
+ df.columns = ["ID","Username","Email","Full Name","Institution",
260
+ "Role","Active","Registered","Last Login"]
261
+ df["Active"] = df["Active"].map({1:"βœ… Yes", 0:"❌ No"})
262
+ df["Last Login"] = df["Last Login"].fillna("Never")
263
+ return df
264
+
265
+ def admin_toggle(uid_str, activate, session):
266
+ if not _is_admin(session): return "❌ Admin only.", admin_get_users(session)
267
+ try:
268
+ msg = toggle_user_active(int(uid_str), activate)
269
+ return msg, admin_get_users(session)
270
+ except ValueError:
271
+ return "❌ Enter a numeric User ID.", admin_get_users(session)
272
+
273
+ def admin_change_role(uid_str, role, session):
274
+ if not _is_admin(session): return "❌ Admin only.", admin_get_users(session)
275
+ try:
276
+ msg = change_user_role(int(uid_str), role)
277
+ return msg, admin_get_users(session)
278
+ except ValueError:
279
+ return "❌ Enter a numeric User ID.", admin_get_users(session)
280
+
281
+ def admin_delete(uid_str, session):
282
+ if not _is_admin(session): return "❌ Admin only.", admin_get_users(session)
283
+ try:
284
+ msg = delete_user(int(uid_str))
285
+ return msg, admin_get_users(session)
286
+ except ValueError:
287
+ return "❌ Enter a numeric User ID.", admin_get_users(session)
288
+
289
+ def admin_get_log(session):
290
+ if not _is_admin(session): return pd.DataFrame()
291
+ logs = get_activity_log(200)
292
+ if not logs: return pd.DataFrame()
293
+ df = pd.DataFrame(logs)[["timestamp","username","action","detail"]]
294
+ df.columns = ["Timestamp","User","Action","Detail"]
295
+ df["Timestamp"] = df["Timestamp"].str[:19].str.replace("T"," ")
296
+ return df
297
+
298
+
299
+ # ══════════════════════════════════════════════════════════════════════════════
300
+ # GRADIO UI
301
+ # ══════════════════════════════════════════════════════════════════════════════
302
+ def create_app():
303
+ with gr.Blocks(css=CSS, title="EduClone AI Portal") as demo:
304
+ session_state = gr.State({})
305
+ gr.HTML(BANNER_HTML)
306
+
307
+ # ── Login / Register ──────────────────────────────────────────────────
308
+ with gr.Column(visible=True) as login_panel:
309
+ with gr.Tabs():
310
+ with gr.TabItem("πŸ” Login"):
311
+ with gr.Column(elem_classes=["auth-card"]):
312
+ gr.HTML("<h2>Sign in to EduClone AI</h2>")
313
+ li_user = gr.Textbox(label="Username or Email")
314
+ li_pass = gr.Textbox(label="Password", type="password")
315
+ li_btn = gr.Button("πŸ” Login", variant="primary", size="lg")
316
+ li_msg = gr.Markdown()
317
+ with gr.TabItem("πŸ“ Register"):
318
+ with gr.Column(elem_classes=["auth-card"]):
319
+ gr.HTML("<h2>Create your account</h2>")
320
+ rg_name = gr.Textbox(label="Full Name")
321
+ rg_inst = gr.Textbox(label="Institution / Organisation")
322
+ rg_user = gr.Textbox(label="Username (3–30 chars, no spaces)")
323
+ rg_email = gr.Textbox(label="Email Address")
324
+ rg_pass = gr.Textbox(label="Password (8+ chars, 1 uppercase, 1 number)", type="password")
325
+ rg_conf = gr.Textbox(label="Confirm Password", type="password")
326
+ rg_btn = gr.Button("πŸ“ Create Account", variant="primary", size="lg")
327
+ rg_msg = gr.Markdown()
328
+
329
+ # ── Main Portal ───────────────────────────────────────────────────────
330
+ with gr.Column(visible=False) as main_portal:
331
+ welcome_html = gr.HTML()
332
+ logout_btn = gr.Button("πŸšͺ Logout", size="sm", variant="secondary")
333
+
334
+ # ── Admin Panel ───────────────────────────────────────────────────
335
+ with gr.Column(visible=False) as admin_tabs:
336
+ gr.HTML("<h3 style='color:#1e3a5f;margin:8px 0 4px'>πŸ”‘ Admin Panel</h3>")
337
+ with gr.Tabs():
338
+ with gr.TabItem("πŸ“Š Dashboard"):
339
+ dash_btn = gr.Button("πŸ”„ Refresh Stats", variant="secondary")
340
+ dash_out = gr.Markdown()
341
+ with gr.TabItem("πŸ‘₯ User Management"):
342
+ refresh_users_btn = gr.Button("πŸ”„ Refresh List", variant="secondary")
343
+ users_table = gr.DataFrame(interactive=False, wrap=True)
344
+ gr.Markdown("**Actions β€” enter User ID:**")
345
+ with gr.Row():
346
+ action_uid = gr.Textbox(label="User ID", scale=1, placeholder="e.g. 3")
347
+ action_role = gr.Dropdown(choices=["user","admin"], value="user",
348
+ label="Set Role", scale=1)
349
+ with gr.Row():
350
+ btn_activate = gr.Button("βœ… Activate", variant="secondary")
351
+ btn_deactivate = gr.Button("❌ Deactivate", variant="secondary")
352
+ btn_set_role = gr.Button("πŸ”„ Set Role", variant="secondary")
353
+ btn_del_user = gr.Button("πŸ—‘οΈ Delete", variant="stop")
354
+ action_msg = gr.Markdown()
355
+ with gr.TabItem("πŸ“‹ Activity Log"):
356
+ refresh_log_btn = gr.Button("πŸ”„ Refresh", variant="secondary")
357
+ log_table = gr.DataFrame(interactive=False, wrap=True)
358
+
359
+ # ── USER SECTION β€” Tab 1 & 2 ─────────────────────────────────────
360
+ with gr.Column(visible=False) as user_tabs:
361
+ with gr.Tabs():
362
+
363
+ # ══ TAB 1: Generate from Topic ═══════════════════════════
364
+ with gr.TabItem("πŸ“ Generate from Topic"):
365
+ gr.HTML(TAB1_HINT)
366
+ with gr.Row():
367
+ with gr.Column(scale=1):
368
+ t1_topic = gr.Textbox(
369
+ label="Topic / Subject Name",
370
+ placeholder="e.g. Mitochondria Β· Supply Chain Β· Newton's Laws",
371
+ lines=2)
372
+ t1_num = gr.Slider(1, 10, value=5, step=1,
373
+ label="Number of Questions")
374
+ t1_blooms = gr.Dropdown(
375
+ choices=list(BLOOMS.keys()),
376
+ value="Mixed (All)",
377
+ label="πŸ“š Bloom's Taxonomy Level")
378
+ t1_diff = gr.Radio(
379
+ choices=["Easy","Medium","Hard","Mixed"],
380
+ value="Mixed", label="Difficulty")
381
+ gr.Markdown("**Download formats:**")
382
+ with gr.Row():
383
+ t1_word = gr.Checkbox(label="πŸ“„ Word", value=True)
384
+ t1_pdf = gr.Checkbox(label="πŸ“‘ PDF", value=True)
385
+ t1_excel = gr.Checkbox(label="πŸ“Š Excel", value=False)
386
+ t1_btn = gr.Button("πŸš€ Generate MCQs",
387
+ variant="primary", size="lg")
388
+ with gr.Column(scale=2):
389
+ t1_preview = gr.Markdown(label="Preview")
390
+ with gr.Row(elem_classes=["dl-row"]):
391
+ t1_w = gr.File(label="πŸ“„ Word")
392
+ t1_p = gr.File(label="πŸ“‘ PDF")
393
+ t1_e = gr.File(label="πŸ“Š Excel")
394
+ t1_metrics = gr.Markdown()
395
+
396
+ # ══ TAB 2: Upload & Clone ═════════════════════════════════
397
+ with gr.TabItem("πŸ” Upload & Clone"):
398
+ gr.HTML(TAB2_HINT)
399
+ with gr.Row():
400
+ with gr.Column(scale=1):
401
+ gr.Markdown("### Step 1 β€” Upload your MCQs")
402
+ t2_file = gr.File(
403
+ label="Upload File (.docx / .pdf / .txt) β€” formatting detected from .docx",
404
+ file_types=[".docx",".doc",".pdf",".txt"])
405
+ t2_paste = gr.Textbox(
406
+ label="Or paste plain-text MCQs here",
407
+ lines=10, value=SAMPLE_MCQS)
408
+ t2_preview_btn = gr.Button("πŸ” Parse & Preview",
409
+ variant="secondary", size="lg")
410
+ gr.Markdown("---\n### Step 2 β€” Clone settings")
411
+ t2_clones = gr.Slider(1, 3, value=2, step=1,
412
+ label="Clones per Question")
413
+ t2_blooms = gr.Dropdown(
414
+ choices=list(BLOOMS.keys()),
415
+ value="Mixed (All)",
416
+ label="πŸ“š Bloom's Level for Clones")
417
+ t2_title = gr.Textbox(
418
+ label="Exam / Output Title",
419
+ value="Cloned MCQs")
420
+ gr.Markdown("**Download formats:**")
421
+ with gr.Row():
422
+ t2_word = gr.Checkbox(label="πŸ“„ Word", value=True)
423
+ t2_pdf = gr.Checkbox(label="πŸ“‘ PDF", value=True)
424
+ t2_excel = gr.Checkbox(label="πŸ“Š Excel", value=False)
425
+ t2_clone_btn = gr.Button("πŸ” Generate Clones",
426
+ variant="primary", size="lg")
427
+ with gr.Column(scale=2):
428
+ t2_preview = gr.Markdown(label="Parsed / Clone Preview")
429
+ with gr.Row(elem_classes=["dl-row"]):
430
+ t2_w = gr.File(label="πŸ“„ Word")
431
+ t2_p = gr.File(label="πŸ“‘ PDF")
432
+ t2_e = gr.File(label="πŸ“Š Excel")
433
+ t2_metrics = gr.Markdown()
434
+
435
+ # ── Wire events ───────────────────────────────────────────────────────
436
+
437
+ # Auth
438
+ li_btn.click(fn=do_login,
439
+ inputs=[li_user, li_pass, session_state],
440
+ outputs=[li_msg, session_state, login_panel, main_portal,
441
+ welcome_html, admin_tabs, user_tabs])
442
+ li_pass.submit(fn=do_login,
443
+ inputs=[li_user, li_pass, session_state],
444
+ outputs=[li_msg, session_state, login_panel, main_portal,
445
+ welcome_html, admin_tabs, user_tabs])
446
+ rg_btn.click(fn=do_register,
447
+ inputs=[rg_user, rg_email, rg_pass, rg_conf,
448
+ rg_name, rg_inst, session_state],
449
+ outputs=[rg_msg, session_state])
450
+ logout_btn.click(fn=do_logout,
451
+ inputs=[session_state],
452
+ outputs=[session_state, login_panel, main_portal,
453
+ welcome_html, admin_tabs, user_tabs])
454
+
455
+ # Admin
456
+ dash_btn.click(fn=admin_get_stats, inputs=[session_state], outputs=[dash_out])
457
+ refresh_users_btn.click(fn=admin_get_users, inputs=[session_state], outputs=[users_table])
458
+ btn_activate.click(fn=lambda u,s: admin_toggle(u,True,s),
459
+ inputs=[action_uid,session_state], outputs=[action_msg,users_table])
460
+ btn_deactivate.click(fn=lambda u,s: admin_toggle(u,False,s),
461
+ inputs=[action_uid,session_state], outputs=[action_msg,users_table])
462
+ btn_set_role.click(fn=admin_change_role,
463
+ inputs=[action_uid,action_role,session_state],
464
+ outputs=[action_msg,users_table])
465
+ btn_del_user.click(fn=admin_delete,
466
+ inputs=[action_uid,session_state], outputs=[action_msg,users_table])
467
+ refresh_log_btn.click(fn=admin_get_log, inputs=[session_state], outputs=[log_table])
468
+
469
+ # Tab 1 β€” Generate
470
+ t1_btn.click(fn=handle_generate,
471
+ inputs=[t1_topic, t1_num, t1_blooms, t1_diff,
472
+ t1_word, t1_pdf, t1_excel, session_state],
473
+ outputs=[t1_preview, t1_w, t1_p, t1_e, t1_metrics])
474
+
475
+ # Tab 2 β€” Parse then Clone (two separate buttons)
476
+ t2_preview_btn.click(fn=handle_upload_preview,
477
+ inputs=[t2_file, t2_paste, session_state],
478
+ outputs=[t2_preview, session_state])
479
+ t2_clone_btn.click(fn=handle_clone,
480
+ inputs=[t2_clones, t2_blooms, t2_word, t2_pdf,
481
+ t2_excel, t2_title, session_state],
482
+ outputs=[t2_preview, t2_w, t2_p, t2_e, t2_metrics])
483
+
484
+ return demo
485
+
486
+
487
+ if __name__ == "__main__":
488
+ app = create_app()
489
+ app.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True)
auth.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EduClone AI β€” Authentication & User Management
3
+ SQLite WAL mode, bcrypt passwords, roles: admin / user
4
+ """
5
+ import sqlite3, bcrypt, datetime, os, re, threading
6
+
7
+ DB_PATH = os.environ.get("EDUCLONE_DB", "educlone_users.db")
8
+ ADMIN_USERNAME = "abdulqayyum"
9
+ ADMIN_EMAIL = "admin@educlone.ai"
10
+ ADMIN_PASSWORD = "Admin@EduClone2025"
11
+
12
+ _lock = threading.Lock()
13
+
14
+ def _conn():
15
+ """Return a fresh connection each call β€” WAL so reads never block."""
16
+ c = sqlite3.connect(DB_PATH, timeout=30)
17
+ c.row_factory = sqlite3.Row
18
+ c.execute("PRAGMA journal_mode=WAL")
19
+ c.execute("PRAGMA busy_timeout=10000")
20
+ return c
21
+
22
+ def init_db():
23
+ with _lock:
24
+ c = _conn()
25
+ c.execute("""CREATE TABLE IF NOT EXISTS users (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ username TEXT UNIQUE NOT NULL,
28
+ email TEXT UNIQUE NOT NULL,
29
+ password_hash TEXT NOT NULL,
30
+ role TEXT NOT NULL DEFAULT 'user',
31
+ full_name TEXT DEFAULT '',
32
+ institution TEXT DEFAULT '',
33
+ is_approved INTEGER DEFAULT 1,
34
+ is_active INTEGER DEFAULT 1,
35
+ created_at TEXT NOT NULL,
36
+ last_login TEXT)""")
37
+ c.execute("""CREATE TABLE IF NOT EXISTS activity_log (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ user_id INTEGER,
40
+ username TEXT,
41
+ action TEXT,
42
+ detail TEXT,
43
+ timestamp TEXT)""")
44
+ # seed admin
45
+ if not c.execute("SELECT id FROM users WHERE username=?",
46
+ (ADMIN_USERNAME,)).fetchone():
47
+ ph = bcrypt.hashpw(ADMIN_PASSWORD.encode(), bcrypt.gensalt()).decode()
48
+ c.execute("""INSERT INTO users
49
+ (username,email,password_hash,role,full_name,institution,is_approved,created_at)
50
+ VALUES(?,?,?,?,?,?,?,?)""",
51
+ (ADMIN_USERNAME, ADMIN_EMAIL, ph, "admin",
52
+ "Abdulqayyum MBA", "EduClone AI", 1,
53
+ datetime.datetime.now().isoformat()))
54
+ c.commit(); c.close()
55
+
56
+ def _valid_email(e): return bool(re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', e))
57
+ def _valid_user(u): return bool(re.match(r'^[a-zA-Z0-9_]{3,30}$', u))
58
+ def _valid_pass(p): return len(p)>=8 and re.search(r'[A-Z]',p) and re.search(r'[0-9]',p)
59
+
60
+ def register_user(username, email, password, full_name="", institution=""):
61
+ username = username.strip().lower(); email = email.strip().lower()
62
+ if not _valid_user(username): return False,"Username: 3–30 chars, letters/numbers/underscore only."
63
+ if not _valid_email(email): return False,"Please enter a valid email address."
64
+ if not _valid_pass(password): return False,"Password: 8+ chars, 1 uppercase, 1 number."
65
+ with _lock:
66
+ c = _conn()
67
+ try:
68
+ if c.execute("SELECT id FROM users WHERE username=?",(username,)).fetchone():
69
+ return False,"Username already taken."
70
+ if c.execute("SELECT id FROM users WHERE email=?",(email,)).fetchone():
71
+ return False,"Email already registered."
72
+ ph = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
73
+ c.execute("""INSERT INTO users
74
+ (username,email,password_hash,role,full_name,institution,is_approved,created_at)
75
+ VALUES(?,?,?,?,?,?,?,?)""",
76
+ (username,email,ph,"user",full_name.strip(),institution.strip(),1,
77
+ datetime.datetime.now().isoformat()))
78
+ c.commit()
79
+ return True, f"βœ… Account created! Welcome, {username}. You can now log in."
80
+ except Exception as ex:
81
+ return False, f"Registration error: {ex}"
82
+ finally:
83
+ c.close()
84
+
85
+ def login_user(username, password):
86
+ username = username.strip().lower()
87
+ with _lock:
88
+ c = _conn()
89
+ try:
90
+ row = c.execute(
91
+ "SELECT * FROM users WHERE username=? OR email=?",
92
+ (username, username)).fetchone()
93
+ if not row:
94
+ return False, "❌ Username or password incorrect.", None
95
+ if not row["is_active"]:
96
+ return False, "❌ Account deactivated. Contact admin.", None
97
+ if not bcrypt.checkpw(password.encode(), row["password_hash"].encode()):
98
+ return False, "❌ Username or password incorrect.", None
99
+ c.execute("UPDATE users SET last_login=? WHERE id=?",
100
+ (datetime.datetime.now().isoformat(), row["id"]))
101
+ c.commit()
102
+ user = {k: row[k] for k in row.keys() if k != "password_hash"}
103
+ return True, f"βœ… Welcome back, {row['full_name'] or row['username']}!", user
104
+ except Exception as ex:
105
+ return False, f"Login error: {ex}", None
106
+ finally:
107
+ c.close()
108
+
109
+ def log_activity(user_id, username, action, detail=""):
110
+ with _lock:
111
+ c = _conn()
112
+ try:
113
+ c.execute("""INSERT INTO activity_log(user_id,username,action,detail,timestamp)
114
+ VALUES(?,?,?,?,?)""",
115
+ (user_id, username, action, detail, datetime.datetime.now().isoformat()))
116
+ c.commit()
117
+ finally:
118
+ c.close()
119
+
120
+ def get_all_users():
121
+ with _lock:
122
+ c = _conn()
123
+ try:
124
+ rows = c.execute("""SELECT id,username,email,role,full_name,institution,
125
+ is_approved,is_active,created_at,last_login
126
+ FROM users ORDER BY created_at DESC""").fetchall()
127
+ return [dict(r) for r in rows]
128
+ finally:
129
+ c.close()
130
+
131
+ def toggle_user_active(user_id, active):
132
+ with _lock:
133
+ c = _conn()
134
+ try:
135
+ c.execute("UPDATE users SET is_active=? WHERE id=?", (1 if active else 0, user_id))
136
+ c.commit()
137
+ return "βœ… User status updated."
138
+ finally:
139
+ c.close()
140
+
141
+ def change_user_role(user_id, role):
142
+ if role not in ("admin","user"): return "❌ Invalid role."
143
+ with _lock:
144
+ c = _conn()
145
+ try:
146
+ c.execute("UPDATE users SET role=? WHERE id=?", (role, user_id))
147
+ c.commit()
148
+ return f"βœ… Role updated to {role}."
149
+ finally:
150
+ c.close()
151
+
152
+ def delete_user(user_id):
153
+ with _lock:
154
+ c = _conn()
155
+ try:
156
+ c.execute("DELETE FROM users WHERE id=? AND role != 'admin'", (user_id,))
157
+ c.commit()
158
+ return "βœ… User deleted."
159
+ finally:
160
+ c.close()
161
+
162
+ def get_activity_log(limit=200):
163
+ with _lock:
164
+ c = _conn()
165
+ try:
166
+ rows = c.execute(
167
+ "SELECT * FROM activity_log ORDER BY timestamp DESC LIMIT ?",
168
+ (limit,)).fetchall()
169
+ return [dict(r) for r in rows]
170
+ finally:
171
+ c.close()
172
+
173
+ def get_stats():
174
+ with _lock:
175
+ c = _conn()
176
+ try:
177
+ today = datetime.date.today().isoformat()
178
+ return {
179
+ "total": c.execute("SELECT COUNT(*) FROM users").fetchone()[0],
180
+ "admins": c.execute("SELECT COUNT(*) FROM users WHERE role='admin'").fetchone()[0],
181
+ "active": c.execute("SELECT COUNT(*) FROM users WHERE is_active=1").fetchone()[0],
182
+ "new_today": c.execute("SELECT COUNT(*) FROM users WHERE created_at LIKE ?",
183
+ (today+"%",)).fetchone()[0],
184
+ "logins_today": c.execute("SELECT COUNT(*) FROM activity_log WHERE action='login' AND timestamp LIKE ?",
185
+ (today+"%",)).fetchone()[0],
186
+ }
187
+ finally:
188
+ c.close()
189
+
190
+ init_db()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.0,<6.0
2
+ bcrypt>=4.0.0
3
+ pandas>=2.0.0
4
+ python-docx>=0.8.11
5
+ reportlab>=4.0.0
6
+ openpyxl>=3.1.0
7
+ pdfplumber>=0.9.0
8
+ requests>=2.28.0