mfirat007 commited on
Commit
1e18157
·
verified ·
1 Parent(s): 01334ca

Upload 39 files

Browse files
README.md CHANGED
@@ -1,13 +1,13 @@
1
  ---
2
  title: AIDidact
3
- emoji: 🎓
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
9
- license: mit
10
  ---
 
11
  # AIDidact
12
 
13
  AIDidact is a scalable AI-powered microlearning ecosystem for personalized, self-directed learning. The platform is organized around modular 5 ECTS learning units, hybrid recommendations, cheating-resistant assessments, and analytics-driven learner support.
@@ -107,4 +107,4 @@ Main deployment files:
107
  5. Learner completes chunked units, activities, and formative checks.
108
  6. Events stream to analytics and update mastery, dashboards, and next recommendations.
109
  7. Learner completes a unique summative assessment with integrity controls.
110
- 8. Module is marked complete only if score, engagement, and integrity thresholds are satisfied.
 
1
  ---
2
  title: AIDidact
3
+ emoji: "🎓"
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
 
9
  ---
10
+
11
  # AIDidact
12
 
13
  AIDidact is a scalable AI-powered microlearning ecosystem for personalized, self-directed learning. The platform is organized around modular 5 ECTS learning units, hybrid recommendations, cheating-resistant assessments, and analytics-driven learner support.
 
107
  5. Learner completes chunked units, activities, and formative checks.
108
  6. Events stream to analytics and update mastery, dashboards, and next recommendations.
109
  7. Learner completes a unique summative assessment with integrity controls.
110
+ 8. Module is marked complete only if score, engagement, and integrity thresholds are satisfied.
backend/app/api/routes/auth.py CHANGED
@@ -5,6 +5,8 @@ from app.api.deps import get_db_session
5
  from app.models.schemas import (
6
  LearnerPreferences,
7
  LearnerRecord,
 
 
8
  LearnerRegistrationResponse,
9
  RegisterLearnerRequest,
10
  )
@@ -33,3 +35,16 @@ async def register_learner(
33
  )
34
  await repository.create_learner(session, engines.build_profile(learner))
35
  return LearnerRegistrationResponse(learner_id=learner.id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from app.models.schemas import (
6
  LearnerPreferences,
7
  LearnerRecord,
8
+ LoginRequest,
9
+ LoginResponse,
10
  LearnerRegistrationResponse,
11
  RegisterLearnerRequest,
12
  )
 
35
  )
36
  await repository.create_learner(session, engines.build_profile(learner))
37
  return LearnerRegistrationResponse(learner_id=learner.id)
38
+
39
+
40
+ @router.post("/login", response_model=LoginResponse)
41
+ async def login_learner(
42
+ payload: LoginRequest, session: AsyncSession = Depends(get_db_session)
43
+ ) -> LoginResponse:
44
+ learner = await repository.get_learner_by_email(session, str(payload.email))
45
+ return LoginResponse(
46
+ learner_id=learner.id,
47
+ first_name=learner.first_name,
48
+ dashboard_path=f"/dashboard/{learner.id}",
49
+ recommendations_path=f"/v1/learners/{learner.id}/recommendations",
50
+ )
backend/app/main.py CHANGED
@@ -1,4 +1,5 @@
1
  from contextlib import asynccontextmanager
 
2
 
3
  from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
@@ -45,149 +46,708 @@ def root() -> HTMLResponse:
45
  return HTMLResponse(
46
  f"""
47
  <!DOCTYPE html>
48
- <html lang="en">
49
  <head>
50
  <meta charset="utf-8">
51
  <meta name="viewport" content="width=device-width, initial-scale=1">
52
  <title>{settings.app_name}</title>
53
  <style>
54
  :root {{
55
- --bg: #0b1020;
56
- --panel: #121933;
57
- --soft: #1a2447;
58
- --text: #eef2ff;
59
- --muted: #aab5d6;
60
- --accent: #58c4ff;
61
- --accent-2: #7ef0c1;
62
- }}
63
- * {{
64
- box-sizing: border-box;
65
  }}
 
66
  body {{
67
  margin: 0;
68
- font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
69
  background:
70
- radial-gradient(circle at top left, rgba(88, 196, 255, 0.16), transparent 32%),
71
- radial-gradient(circle at bottom right, rgba(126, 240, 193, 0.14), transparent 30%),
72
  var(--bg);
73
- color: var(--text);
74
  }}
75
- .wrap {{
76
- max-width: 980px;
77
- margin: 0 auto;
78
- padding: 48px 20px 72px;
79
  }}
80
- .hero {{
81
- background: linear-gradient(135deg, rgba(18, 25, 51, 0.96), rgba(26, 36, 71, 0.92));
82
- border: 1px solid rgba(255, 255, 255, 0.08);
83
- border-radius: 22px;
84
- padding: 32px;
85
- box-shadow: 0 18px 60px rgba(0, 0, 0, 0.28);
 
 
 
 
86
  }}
87
- .badge {{
 
88
  display: inline-block;
89
- padding: 8px 12px;
90
  border-radius: 999px;
91
- background: rgba(88, 196, 255, 0.12);
 
 
 
 
 
92
  color: var(--accent);
93
- font-size: 14px;
94
- margin-bottom: 14px;
 
 
 
 
 
 
 
95
  }}
96
  h1 {{
97
- margin: 0 0 10px;
98
- font-size: clamp(32px, 5vw, 56px);
99
- line-height: 1.05;
 
100
  }}
101
- p {{
 
 
102
  color: var(--muted);
103
- line-height: 1.6;
104
- font-size: 17px;
105
  }}
106
- .grid {{
107
- display: grid;
108
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
109
- gap: 16px;
110
- margin-top: 28px;
111
- }}
112
- .card {{
113
- background: rgba(255, 255, 255, 0.03);
114
- border: 1px solid rgba(255, 255, 255, 0.08);
115
- border-radius: 18px;
116
- padding: 18px;
117
- }}
118
- .card h3 {{
119
- margin-top: 0;
120
- margin-bottom: 8px;
121
- font-size: 18px;
122
- }}
123
- .card a, .cta {{
124
- color: var(--accent);
125
- text-decoration: none;
126
- font-weight: 600;
127
- }}
128
- .card a:hover, .cta:hover {{
129
- text-decoration: underline;
130
  }}
131
- .actions {{
132
  display: flex;
133
  flex-wrap: wrap;
134
- gap: 12px;
135
- margin-top: 24px;
136
  }}
137
- .button {{
 
138
  display: inline-block;
 
 
139
  padding: 12px 16px;
140
- border-radius: 12px;
 
141
  text-decoration: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  font-weight: 700;
143
- color: #07111f;
144
- background: linear-gradient(135deg, var(--accent), var(--accent-2));
145
  }}
146
- .button.secondary {{
147
- color: var(--text);
148
- background: rgba(255, 255, 255, 0.06);
149
- border: 1px solid rgba(255, 255, 255, 0.08);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  }}
151
- code {{
152
- color: var(--accent-2);
153
- background: rgba(255, 255, 255, 0.06);
154
- padding: 2px 6px;
155
- border-radius: 6px;
156
  }}
157
  </style>
158
  </head>
159
  <body>
160
  <div class="wrap">
161
  <section class="hero">
162
- <div class="badge">AIDidact Microlearning Platform</div>
163
- <h1>{settings.app_name}</h1>
164
- <p>
165
- Personalized AI-powered microlearning backend is running successfully.
166
- Use this service to explore modules, learner profiles, assessments, and analytics APIs.
167
- </p>
168
-
169
- <div class="actions">
170
- <a class="button" href="/docs">Open API Docs</a>
171
- <a class="button secondary" href="/openapi.json">Open OpenAPI JSON</a>
172
- <a class="button secondary" href="/health">Health Check</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  </div>
 
174
 
175
- <div class="grid">
176
- <div class="card">
177
- <h3>API Status</h3>
178
- <p>Service status: <code>running</code><br>Version: <code>{settings.app_version}</code></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  </div>
180
- <div class="card">
181
- <h3>Base Prefix</h3>
182
- <p>All core endpoints are available under <code>{settings.api_prefix}</code>.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  </div>
184
- <div class="card">
185
- <h3>Starter Endpoint</h3>
186
- <p><a href="{settings.api_prefix}/modules">{settings.api_prefix}/modules</a></p>
 
 
 
 
 
187
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
188
  </div>
189
  </section>
190
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </body>
192
  </html>
193
  """
 
1
  from contextlib import asynccontextmanager
2
+ from uuid import UUID
3
 
4
  from fastapi import FastAPI
5
  from fastapi.middleware.cors import CORSMiddleware
 
46
  return HTMLResponse(
47
  f"""
48
  <!DOCTYPE html>
49
+ <html lang="tr">
50
  <head>
51
  <meta charset="utf-8">
52
  <meta name="viewport" content="width=device-width, initial-scale=1">
53
  <title>{settings.app_name}</title>
54
  <style>
55
  :root {{
56
+ --bg: #f6f0e2;
57
+ --ink: #132122;
58
+ --muted: #5a6668;
59
+ --panel: rgba(255, 251, 244, 0.92);
60
+ --line: rgba(19, 33, 34, 0.10);
61
+ --accent: #0f8a79;
62
+ --deep: #173d6f;
63
+ --warm: #efa844;
 
 
64
  }}
65
+ * {{ box-sizing: border-box; }}
66
  body {{
67
  margin: 0;
68
+ font-family: Georgia, "Times New Roman", serif;
69
  background:
70
+ radial-gradient(circle at top left, rgba(15,138,121,0.16), transparent 28%),
71
+ radial-gradient(circle at top right, rgba(239,168,68,0.20), transparent 24%),
72
  var(--bg);
73
+ color: var(--ink);
74
  }}
75
+ .wrap {{ max-width: 1180px; margin: 0 auto; padding: 28px 20px 72px; }}
76
+ .hero, .split, .modules, .form-grid {{
77
+ display: grid;
78
+ gap: 18px;
79
  }}
80
+ .hero {{ grid-template-columns: 1.15fr 0.85fr; }}
81
+ .split {{ grid-template-columns: 1fr 1fr; margin-top: 20px; }}
82
+ .modules {{ grid-template-columns: repeat(3, 1fr); margin-top: 20px; }}
83
+ .form-grid {{ grid-template-columns: 1fr 1fr; }}
84
+ .panel, .card {{
85
+ background: var(--panel);
86
+ border: 1px solid var(--line);
87
+ border-radius: 24px;
88
+ padding: 24px;
89
+ box-shadow: 0 24px 60px rgba(18, 27, 29, 0.10);
90
  }}
91
+ .card {{ border-radius: 18px; padding: 18px; }}
92
+ .badge, .mini {{
93
  display: inline-block;
 
94
  border-radius: 999px;
95
+ font-family: "Segoe UI", Tahoma, sans-serif;
96
+ font-weight: 700;
97
+ }}
98
+ .badge {{
99
+ padding: 8px 14px;
100
+ background: rgba(15,138,121,0.10);
101
  color: var(--accent);
102
+ font-size: 13px;
103
+ text-transform: uppercase;
104
+ letter-spacing: 0.05em;
105
+ }}
106
+ .mini {{
107
+ padding: 7px 11px;
108
+ background: rgba(239,168,68,0.16);
109
+ color: #8e5a0a;
110
+ font-size: 12px;
111
  }}
112
  h1 {{
113
+ margin: 14px 0;
114
+ font-size: clamp(38px, 6vw, 74px);
115
+ line-height: 0.96;
116
+ letter-spacing: -0.03em;
117
  }}
118
+ h2 {{ margin: 0 0 12px; font-size: clamp(28px, 4vw, 42px); }}
119
+ h3 {{ margin: 10px 0; font-family: "Segoe UI", Tahoma, sans-serif; }}
120
+ p, li {{
121
  color: var(--muted);
122
+ line-height: 1.65;
123
+ font-size: 16px;
124
  }}
125
+ .eyebrow {{
126
+ margin-bottom: 10px;
127
+ color: var(--deep);
128
+ font-family: "Segoe UI", Tahoma, sans-serif;
129
+ font-size: 12px;
130
+ font-weight: 800;
131
+ text-transform: uppercase;
132
+ letter-spacing: 0.08em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }}
134
+ .actions, .meta, .tabs, .wizard-nav {{
135
  display: flex;
136
  flex-wrap: wrap;
137
+ gap: 10px;
 
138
  }}
139
+ .meta {{ margin-top: 18px; }}
140
+ .button, .tab-btn {{
141
  display: inline-block;
142
+ border-radius: 14px;
143
+ border: 1px solid var(--line);
144
  padding: 12px 16px;
145
+ font-family: "Segoe UI", Tahoma, sans-serif;
146
+ font-weight: 700;
147
  text-decoration: none;
148
+ cursor: pointer;
149
+ }}
150
+ .button {{
151
+ background: linear-gradient(135deg, var(--accent), var(--deep));
152
+ color: #fffef8;
153
+ border: 0;
154
+ }}
155
+ .button.secondary, .tab-btn {{
156
+ background: rgba(255,255,255,0.52);
157
+ color: var(--deep);
158
+ }}
159
+ .tab-btn.active {{
160
+ background: linear-gradient(135deg, rgba(15,138,121,0.12), rgba(239,168,68,0.14));
161
+ }}
162
+ .tab-pane, .wizard-pane {{ display: none; }}
163
+ .tab-pane.active, .wizard-pane.active {{ display: block; }}
164
+ .steps {{
165
+ display: flex;
166
+ gap: 8px;
167
+ margin: 14px 0 8px;
168
+ }}
169
+ .step {{
170
+ flex: 1;
171
+ text-align: center;
172
+ padding: 8px 10px;
173
+ border-radius: 12px;
174
+ border: 1px solid var(--line);
175
+ background: rgba(255,255,255,0.52);
176
+ color: var(--muted);
177
+ font-family: "Segoe UI", Tahoma, sans-serif;
178
+ font-size: 12px;
179
  font-weight: 700;
 
 
180
  }}
181
+ .step.active {{
182
+ color: var(--deep);
183
+ background: linear-gradient(135deg, rgba(15,138,121,0.12), rgba(239,168,68,0.14));
184
+ }}
185
+ label {{
186
+ display: grid;
187
+ gap: 6px;
188
+ margin-top: 10px;
189
+ color: var(--deep);
190
+ font-family: "Segoe UI", Tahoma, sans-serif;
191
+ font-size: 14px;
192
+ font-weight: 700;
193
+ }}
194
+ input, select, textarea {{
195
+ width: 100%;
196
+ padding: 12px 14px;
197
+ border-radius: 14px;
198
+ border: 1px solid rgba(19, 33, 34, 0.16);
199
+ background: rgba(255,255,255,0.84);
200
+ color: var(--ink);
201
+ font: inherit;
202
+ }}
203
+ textarea {{ min-height: 104px; resize: vertical; }}
204
+ .notice {{
205
+ display: none;
206
+ margin-top: 12px;
207
+ padding: 12px 14px;
208
+ border-radius: 14px;
209
+ background: rgba(15,138,121,0.10);
210
+ border: 1px solid rgba(15,138,121,0.18);
211
+ color: var(--deep);
212
+ font-family: "Segoe UI", Tahoma, sans-serif;
213
+ }}
214
+ .video {{
215
+ min-height: 260px;
216
+ background: linear-gradient(135deg, rgba(15,138,121,0.96), rgba(23,61,111,0.94));
217
+ color: white;
218
+ display: flex;
219
+ flex-direction: column;
220
+ justify-content: space-between;
221
+ }}
222
+ .play {{
223
+ width: 76px;
224
+ height: 76px;
225
+ border-radius: 50%;
226
+ background: rgba(255,255,255,0.14);
227
+ border: 1px solid rgba(255,255,255,0.24);
228
+ display: grid;
229
+ place-items: center;
230
+ }}
231
+ .tri {{
232
+ width: 0;
233
+ height: 0;
234
+ border-top: 11px solid transparent;
235
+ border-bottom: 11px solid transparent;
236
+ border-left: 18px solid white;
237
+ margin-left: 3px;
238
  }}
239
+ .inline-link {{ color: var(--deep); text-decoration: none; font-weight: 700; }}
240
+ .inline-link:hover {{ text-decoration: underline; }}
241
+ @media (max-width: 920px) {{
242
+ .hero, .split, .modules, .form-grid {{ grid-template-columns: 1fr; }}
 
243
  }}
244
  </style>
245
  </head>
246
  <body>
247
  <div class="wrap">
248
  <section class="hero">
249
+ <div class="panel">
250
+ <div class="badge">AI-Powered Microlearning Ecosystem</div>
251
+ <h1>Kisisellestirilmis ogrenme, kisa ama derin modullerle.</h1>
252
+ <p>
253
+ AIDidact; ogrenme gecmisi, hedefler, hazirbulunusluk ve ilerleme verisini birlikte kullanarak
254
+ kisiye ozel moduller, odevler, olcumleme ve oneriler sunan tam tesekkullu bir ogrenme platformudur.
255
+ </p>
256
+ <div class="actions">
257
+ <a class="button" href="#auth-box">Kayit Ol ve Profilini Olustur</a>
258
+ <a class="button secondary" href="{settings.api_prefix}/modules">Modulleri Incele</a>
259
+ </div>
260
+ <div class="meta">
261
+ <span class="mini">5 ECTS moduller</span>
262
+ <span class="mini">Uyarlanabilir oneriler</span>
263
+ <span class="mini">Kopya direncli olcme</span>
264
+ <span class="mini">Ogrenme analitigi</span>
265
+ </div>
266
+ </div>
267
+ <div class="panel video">
268
+ <div class="play"><div class="tri"></div></div>
269
+ <div>
270
+ <h3>Tanitim videosu alani</h3>
271
+ <p style="color: rgba(255,255,255,0.86); margin: 0;">
272
+ Bu panel, ekosistemin nasil isledigini, moduller arasi gecisleri ve profil tabanli
273
+ kisisellestirmeyi gosteren giris videosu icin ayrildi.
274
+ </p>
275
+ </div>
276
  </div>
277
+ </section>
278
 
279
+ <section class="split">
280
+ <div class="panel">
281
+ <div class="eyebrow">Ekosistem</div>
282
+ <h2>AIDidact ne sunar?</h2>
283
+ <ul>
284
+ <li>Bloom hizali mikro ogrenme birimleri</li>
285
+ <li>Profil vektorune dayali tavsiye sistemi</li>
286
+ <li>Her ogrenene ozgu degerlendirme akisi</li>
287
+ <li>Zaman, etkilesim ve basari izleme analitigi</li>
288
+ <li>Milyonlarca kullaniciya olceklenebilir servis mimarisi</li>
289
+ </ul>
290
+ </div>
291
+ <div class="panel">
292
+ <div class="eyebrow">Ilk Ogrenme Yolu</div>
293
+ <h2>Uc ayri 5 ECTS AI temel modulu</h2>
294
+ <p>
295
+ Ilk ogrenme yolu artik tek bir giris modulu degil; birbiri ardina alinabilecek
296
+ uc bagimsiz 5 ECTS modulden olusur.
297
+ </p>
298
+ <div class="actions">
299
+ <a class="button secondary" href="/docs">API Docs</a>
300
+ <a class="button secondary" href="/health">Health</a>
301
  </div>
302
+ </div>
303
+ </section>
304
+
305
+ <section class="modules">
306
+ <div class="card">
307
+ <div class="mini">5 ECTS • Modul 1</div>
308
+ <h3>AI Nedir?</h3>
309
+ <p>Yapay zekanin tanimi, kapsami, alt alanlari ve gercek yasam kullanim alanlari.</p>
310
+ </div>
311
+ <div class="card">
312
+ <div class="mini">5 ECTS • Modul 2</div>
313
+ <h3>AI Nasil Calisir?</h3>
314
+ <p>Veri, ozellik, model, tahmin ve karar mantiginin temelleri.</p>
315
+ </div>
316
+ <div class="card">
317
+ <div class="mini">5 ECTS • Modul 3</div>
318
+ <h3>AI Nasil Egitilir?</h3>
319
+ <p>Egitim, dogrulama, test, hata analizi ve etik riskleri ele alan modul.</p>
320
+ </div>
321
+ </section>
322
+
323
+ <section class="split" id="auth-box">
324
+ <div class="panel">
325
+ <div class="eyebrow">Kayit ve Giris</div>
326
+ <h2>Calisan demo onboarding</h2>
327
+ <div class="tabs">
328
+ <button class="tab-btn active" data-tab="signup">Kayit Ol</button>
329
+ <button class="tab-btn" data-tab="signin">Giris Yap</button>
330
+ </div>
331
+
332
+ <div class="tab-pane active" id="signup-pane">
333
+ <div class="steps">
334
+ <div class="step active" data-chip="1">1. Kimlik</div>
335
+ <div class="step" data-chip="2">2. Hedefler</div>
336
+ <div class="step" data-chip="3">3. Tercihler</div>
337
+ </div>
338
+ <form id="registration-form">
339
+ <div class="wizard-pane active" data-step="1">
340
+ <div class="form-grid">
341
+ <label>Ad
342
+ <input name="first_name" placeholder="Adiniz" required>
343
+ </label>
344
+ <label>Soyad
345
+ <input name="last_name" placeholder="Soyadiniz" required>
346
+ </label>
347
+ </div>
348
+ <div class="form-grid">
349
+ <label>E-posta
350
+ <input type="email" name="email" placeholder="ornek@mail.com" required>
351
+ </label>
352
+ <label>Egitim Duzeyi
353
+ <select name="educational_level" required>
354
+ <option value="secondary">Ortaogretim</option>
355
+ <option value="undergraduate" selected>Lisans</option>
356
+ <option value="graduate">Lisansustu</option>
357
+ <option value="professional">Profesyonel Gelisim</option>
358
+ </select>
359
+ </label>
360
+ </div>
361
+ </div>
362
+ <div class="wizard-pane" data-step="2">
363
+ <div class="form-grid">
364
+ <label>Kisa Vadeli Hedef
365
+ <input name="short_goal" placeholder="Orn. AI kavramlarini anlamak" required>
366
+ </label>
367
+ <label>Uzun Vadeli Hedef
368
+ <input name="long_goal" placeholder="Orn. AI ile ders tasarlamak" required>
369
+ </label>
370
+ </div>
371
+ <label>Onceki Ogrenmeler
372
+ <textarea name="prior_learning" placeholder="Dersler, sertifikalar, kendi kendine ogrenme veya is deneyimi"></textarea>
373
+ </label>
374
+ </div>
375
+ <div class="wizard-pane" data-step="3">
376
+ <div class="form-grid">
377
+ <label>Tercih Edilen Bicim
378
+ <select name="preferred_modality">
379
+ <option value="interactive">Etkilesimli etkinlikler</option>
380
+ <option value="video">Kisa videolar</option>
381
+ <option value="reading">Metin ve ozetler</option>
382
+ <option value="reflection">Yansitici gorevler</option>
383
+ </select>
384
+ </label>
385
+ <label>Oturum Suresi
386
+ <select name="session_length">
387
+ <option value="10">10 dakika</option>
388
+ <option value="15" selected>15 dakika</option>
389
+ <option value="20">20 dakika</option>
390
+ </select>
391
+ </label>
392
+ </div>
393
+ </div>
394
+ <div class="wizard-nav">
395
+ <button class="button secondary" type="button" id="prev-step">Geri</button>
396
+ <button class="button secondary" type="button" id="next-step">Ileri</button>
397
+ <button class="button" type="submit" id="submit-registration" style="display:none;">Profili Olustur</button>
398
+ </div>
399
+ </form>
400
+ <div id="registration-result" class="notice"></div>
401
+ </div>
402
+
403
+ <div class="tab-pane" id="signin-pane">
404
+ <form id="login-form">
405
+ <label>E-posta
406
+ <input type="email" name="login_email" placeholder="ornek@mail.com" required>
407
+ </label>
408
+ <button class="button" type="submit">Giris Yap</button>
409
+ </form>
410
+ <div id="login-result" class="notice"></div>
411
+ </div>
412
+ </div>
413
+
414
+ <div class="panel">
415
+ <div class="eyebrow">Ornek Ders Akisi</div>
416
+ <h2>Calisan platform demosu</h2>
417
+ <ul>
418
+ <li>Profil bilgisi toplanir ve learner profili olusturulur</li>
419
+ <li>Moduller ve öneriler API uzerinden alinabilir</li>
420
+ <li>Giris yapan kullanici dashboard ve recommendation endpointlerine yonlenir</li>
421
+ <li>Space uzerinde tek sayfadan tum akisin demosu gorulur</li>
422
+ </ul>
423
+ <p>
424
+ Canli endpointler:
425
+ <a class="inline-link" href="{settings.api_prefix}/modules">{settings.api_prefix}/modules</a>,
426
+ <a class="inline-link" href="/docs">/docs</a>,
427
+ <a class="inline-link" href="/health">/health</a>
428
+ </p>
429
+ </div>
430
+ </section>
431
+ </div>
432
+ <script>
433
+ const tabs = document.querySelectorAll(".tab-btn");
434
+ const signupPane = document.getElementById("signup-pane");
435
+ const signinPane = document.getElementById("signin-pane");
436
+ const steps = [...document.querySelectorAll(".wizard-pane")];
437
+ const chips = [...document.querySelectorAll(".step")];
438
+ const prevStep = document.getElementById("prev-step");
439
+ const nextStep = document.getElementById("next-step");
440
+ const submitButton = document.getElementById("submit-registration");
441
+ const form = document.getElementById("registration-form");
442
+ const result = document.getElementById("registration-result");
443
+ const loginForm = document.getElementById("login-form");
444
+ const loginResult = document.getElementById("login-result");
445
+ let currentStep = 1;
446
+
447
+ function setTab(name) {{
448
+ tabs.forEach((tab) => tab.classList.toggle("active", tab.dataset.tab === name));
449
+ signupPane.classList.toggle("active", name === "signup");
450
+ signinPane.classList.toggle("active", name === "signin");
451
+ }}
452
+
453
+ tabs.forEach((tab) => tab.addEventListener("click", () => setTab(tab.dataset.tab)));
454
+
455
+ function renderStep() {{
456
+ steps.forEach((pane) => pane.classList.toggle("active", Number(pane.dataset.step) === currentStep));
457
+ chips.forEach((chip) => chip.classList.toggle("active", Number(chip.dataset.chip) === currentStep));
458
+ prevStep.style.visibility = currentStep === 1 ? "hidden" : "visible";
459
+ nextStep.style.display = currentStep === steps.length ? "none" : "inline-block";
460
+ submitButton.style.display = currentStep === steps.length ? "inline-block" : "inline-block";
461
+ if (currentStep === steps.length) {{
462
+ nextStep.style.display = "none";
463
+ submitButton.style.display = "inline-block";
464
+ }} else {{
465
+ nextStep.style.display = "inline-block";
466
+ submitButton.style.display = "none";
467
+ }}
468
+ }}
469
+
470
+ nextStep.addEventListener("click", () => {{
471
+ if (currentStep < steps.length) {{
472
+ currentStep += 1;
473
+ renderStep();
474
+ }}
475
+ }});
476
+
477
+ prevStep.addEventListener("click", () => {{
478
+ if (currentStep > 1) {{
479
+ currentStep -= 1;
480
+ renderStep();
481
+ }}
482
+ }});
483
+
484
+ renderStep();
485
+
486
+ form.addEventListener("submit", async (event) => {{
487
+ event.preventDefault();
488
+ const data = new FormData(form);
489
+ const priorLearning = data.get("prior_learning")?.toString().trim();
490
+
491
+ const payload = {{
492
+ email: data.get("email"),
493
+ first_name: data.get("first_name"),
494
+ last_name: data.get("last_name"),
495
+ timezone: "Europe/Istanbul",
496
+ educational_level: data.get("educational_level"),
497
+ consent_personalization: true,
498
+ consent_analytics: true,
499
+ prior_learning: priorLearning ? [{{
500
+ learning_type: "informal",
501
+ title: "Self-reported prior learning",
502
+ description: priorLearning
503
+ }}] : [],
504
+ goals: [
505
+ {{ goal_horizon: "short_term", goal_text: data.get("short_goal") }},
506
+ {{ goal_horizon: "long_term", goal_text: data.get("long_goal") }}
507
+ ],
508
+ preferences: {{
509
+ preferred_modalities: [data.get("preferred_modality")],
510
+ preferred_session_length_minutes: Number(data.get("session_length"))
511
+ }}
512
+ }};
513
+
514
+ result.style.display = "block";
515
+ result.textContent = "Profil olusturuluyor...";
516
+
517
+ try {{
518
+ const response = await fetch("{settings.api_prefix}/auth/register", {{
519
+ method: "POST",
520
+ headers: {{ "Content-Type": "application/json" }},
521
+ body: JSON.stringify(payload)
522
+ }});
523
+
524
+ if (!response.ok) {{
525
+ const errorData = await response.json().catch(() => ({{ detail: "Bilinmeyen hata" }}));
526
+ throw new Error(errorData.detail || "Kayit basarisiz oldu");
527
+ }}
528
+
529
+ const json = await response.json();
530
+ result.innerHTML = `Profil olusturuldu. Learner ID: <code>${{json.learner_id}}</code>. <a class="inline-link" href="/dashboard/${{json.learner_id}}">Dashboard'u ac</a>`;
531
+ }} catch (error) {{
532
+ result.textContent = `Kayit sirasinda hata olustu: ${{error.message}}`;
533
+ }}
534
+ }});
535
+
536
+ loginForm.addEventListener("submit", async (event) => {{
537
+ event.preventDefault();
538
+ const data = new FormData(loginForm);
539
+ loginResult.style.display = "block";
540
+ loginResult.textContent = "Profil araniyor...";
541
+
542
+ try {{
543
+ const response = await fetch("{settings.api_prefix}/auth/login", {{
544
+ method: "POST",
545
+ headers: {{ "Content-Type": "application/json" }},
546
+ body: JSON.stringify({{ email: data.get("login_email") }})
547
+ }});
548
+
549
+ if (!response.ok) {{
550
+ const errorData = await response.json().catch(() => ({{ detail: "Bilinmeyen hata" }}));
551
+ throw new Error(errorData.detail || "Giris basarisiz");
552
+ }}
553
+
554
+ const json = await response.json();
555
+ loginResult.innerHTML = `Hos geldin <strong>${{json.first_name}}</strong>. <a class="inline-link" href="${{json.dashboard_path}}">Dashboard</a> ve <a class="inline-link" href="${{json.recommendations_path}}">oneriler</a> hazir.`;
556
+ }} catch (error) {{
557
+ loginResult.textContent = `Giris sirasinda hata olustu: ${{error.message}}`;
558
+ }}
559
+ }});
560
+ </script>
561
+ </body>
562
+ </html>
563
+ """
564
+ )
565
+
566
+
567
+ @app.get("/dashboard/{learner_id}", response_class=HTMLResponse)
568
+ def learner_dashboard_page(learner_id: UUID) -> HTMLResponse:
569
+ dashboard_api = f"{settings.api_prefix}/learners/{learner_id}/dashboard"
570
+ recommendations_api = f"{settings.api_prefix}/learners/{learner_id}/recommendations"
571
+ return HTMLResponse(
572
+ f"""
573
+ <!DOCTYPE html>
574
+ <html lang="tr">
575
+ <head>
576
+ <meta charset="utf-8">
577
+ <meta name="viewport" content="width=device-width, initial-scale=1">
578
+ <title>AIDidact Dashboard</title>
579
+ <style>
580
+ body {{ margin:0; font-family: "Segoe UI", Tahoma, sans-serif; background:#e8f3fb; color:#1a2630; }}
581
+ .wrap {{ max-width: 1320px; margin:0 auto; padding:32px 18px 48px; }}
582
+ .top {{ display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:20px; }}
583
+ .card-grid {{ display:grid; grid-template-columns: repeat(4, 1fr); gap:18px; margin-bottom:18px; }}
584
+ .card, .panel {{
585
+ background:white; border-radius:20px; padding:22px; box-shadow:0 8px 22px rgba(26,38,48,.08);
586
+ }}
587
+ .metric-title {{ color:#6b7686; font-size:15px; margin-bottom:10px; }}
588
+ .metric-value {{ font-size:28px; font-weight:800; margin-bottom:6px; }}
589
+ .metric-sub {{ color:#7c8696; font-size:14px; }}
590
+ .panels {{ display:grid; grid-template-columns: 1.1fr .9fr; gap:18px; }}
591
+ svg {{ width:100%; height:auto; display:block; }}
592
+ .legend {{ display:flex; gap:16px; flex-wrap:wrap; color:#617080; font-size:14px; margin-bottom:10px; }}
593
+ .dot {{ width:14px; height:14px; display:inline-block; border-radius:4px; margin-right:6px; }}
594
+ .reco-list {{ margin:0; padding-left:18px; line-height:1.7; color:#5d6876; }}
595
+ a {{ color:#145bb5; text-decoration:none; font-weight:700; }}
596
+ @media (max-width: 1024px) {{ .card-grid, .panels {{ grid-template-columns:1fr; }} }}
597
+ </style>
598
+ </head>
599
+ <body>
600
+ <div class="wrap">
601
+ <div class="top">
602
+ <div>
603
+ <div style="color:#6b7686; font-size:14px;">AIDidact Learning Analytics</div>
604
+ <h1 style="margin:6px 0 0; font-size:40px;">Ogrenme Dashboard'u</h1>
605
+ </div>
606
+ <div style="display:flex; gap:10px; align-items:center;">
607
+ <a href="/">Anasayfa</a>
608
+ <a href="/docs">API Docs</a>
609
+ </div>
610
+ </div>
611
+
612
+ <section class="card-grid">
613
+ <div class="card"><div class="metric-title">Tahmini Ayrilma Riski</div><div class="metric-value" id="dropoutRisk">--</div><div class="metric-sub" id="dropoutSub">Yukleniyor</div></div>
614
+ <div class="card"><div class="metric-title">Ogrenme Aktivite Endeksi</div><div class="metric-value" id="activityIndex">--</div><div class="metric-sub" id="activitySub">Yukleniyor</div></div>
615
+ <div class="card"><div class="metric-title">AI Katilim Puani</div><div class="metric-value" id="engagementScore">--</div><div class="metric-sub" id="engagementSub">Yukleniyor</div></div>
616
+ <div class="card"><div class="metric-title">Bilissel Yuk Tahmini</div><div class="metric-value" id="cognitiveLoad">--</div><div class="metric-sub" id="cognitiveSub">Yukleniyor</div></div>
617
+ </section>
618
+
619
+ <section class="panels">
620
+ <div class="panel">
621
+ <h2 style="margin-top:0;">Katilim ve Ayrilma Riski Egitimi</h2>
622
+ <div class="legend">
623
+ <span><i class="dot" style="background:#28a8eb;"></i>Katilim Puani</span>
624
+ <span><i class="dot" style="background:#ef5b61;"></i>Ayrilma Riski</span>
625
  </div>
626
+ <svg viewBox="0 0 640 330" id="trendChart"></svg>
627
+ </div>
628
+ <div class="panel">
629
+ <h2 style="margin-top:0;">Ogrenme Davranislari</h2>
630
+ <div class="legend">
631
+ <span><i class="dot" style="background:#18c48f;"></i>Yuksek Performanslilar</span>
632
+ <span><i class="dot" style="background:#f1b132;"></i>Orta Performanslilar</span>
633
+ <span><i class="dot" style="background:#ef5b61;"></i>Dusuk Performanslilar</span>
634
  </div>
635
+ <svg viewBox="0 0 420 330" id="radarChart"></svg>
636
+ </div>
637
+ </section>
638
+
639
+ <section class="panels" style="margin-top:18px;">
640
+ <div class="panel">
641
+ <h2 style="margin-top:0;">Uyarlanabilir Geri Bildirim</h2>
642
+ <ul class="reco-list" id="feedbackList"></ul>
643
+ </div>
644
+ <div class="panel">
645
+ <h2 style="margin-top:0;">Onerilen Moduller</h2>
646
+ <ul class="reco-list" id="recommendationList"></ul>
647
  </div>
648
  </section>
649
  </div>
650
+
651
+ <script>
652
+ function setText(id, value) {{
653
+ document.getElementById(id).textContent = value;
654
+ }}
655
+
656
+ function renderTrendChart(trend) {{
657
+ const svg = document.getElementById("trendChart");
658
+ const width = 640, height = 330, left = 54, right = 20, top = 30, bottom = 44;
659
+ const innerW = width - left - right, innerH = height - top - bottom;
660
+ const maxY = 100;
661
+ const stepX = innerW / (trend.length - 1 || 1);
662
+ const y = (v) => top + innerH - (v / maxY) * innerH;
663
+ let engagementPath = "";
664
+ let riskPath = "";
665
+ let labels = "";
666
+ let grid = "";
667
+
668
+ for (let i = 0; i <= 5; i++) {{
669
+ const gy = top + (innerH / 5) * i;
670
+ grid += `<line x1="${{left}}" y1="${{gy}}" x2="${{width-right}}" y2="${{gy}}" stroke="#dbe7f0"/>`;
671
+ }}
672
+
673
+ trend.forEach((point, index) => {{
674
+ const x = left + stepX * index;
675
+ engagementPath += `${{index === 0 ? "M" : "L"}}${{x}},${{y(point.engagement_score)}} `;
676
+ riskPath += `${{index === 0 ? "M" : "L"}}${{x}},${{y(point.dropout_risk)}} `;
677
+ labels += `<text x="${{x}}" y="${{height-16}}" text-anchor="middle" font-size="12" fill="#657487">${{point.label}}</text>`;
678
+ }});
679
+
680
+ svg.innerHTML = `
681
+ <rect x="0" y="0" width="${{width}}" height="${{height}}" fill="white"/>
682
+ ${{grid}}
683
+ <path d="${{engagementPath}}" fill="none" stroke="#28a8eb" stroke-width="4"/>
684
+ <path d="${{riskPath}}" fill="none" stroke="#ef5b61" stroke-width="4"/>
685
+ ${{labels}}
686
+ `;
687
+ }}
688
+
689
+ function renderRadarChart(clusters) {{
690
+ const svg = document.getElementById("radarChart");
691
+ const cx = 210, cy = 170, radius = 118;
692
+ const axes = ["video_izleme","forum_katilimi","odev_teslimi","quiz_performansi","notlara_erisim","canli_ders"];
693
+ const axisLabels = {{
694
+ video_izleme:"Video Izleme", forum_katilimi:"Forum Katilimi", odev_teslimi:"Odev Teslimi",
695
+ quiz_performansi:"Quiz Performansi", notlara_erisim:"Notlara Erisim", canli_ders:"Canli Ders"
696
+ }};
697
+ const angleStep = (Math.PI * 2) / axes.length;
698
+ const colors = ["#18c48f","#f1b132","#ef5b61"];
699
+ let markup = "";
700
+
701
+ for (let level = 1; level <= 5; level++) {{
702
+ const r = (radius / 5) * level;
703
+ const points = axes.map((_, i) => `${{cx + Math.cos(-Math.PI/2 + angleStep*i) * r}},${{cy + Math.sin(-Math.PI/2 + angleStep*i) * r}}`).join(" ");
704
+ markup += `<polygon points="${{points}}" fill="none" stroke="#dfe7ef"/>`;
705
+ }}
706
+
707
+ axes.forEach((axis, i) => {{
708
+ const x = cx + Math.cos(-Math.PI/2 + angleStep*i) * radius;
709
+ const y = cy + Math.sin(-Math.PI/2 + angleStep*i) * radius;
710
+ const lx = cx + Math.cos(-Math.PI/2 + angleStep*i) * (radius + 24);
711
+ const ly = cy + Math.sin(-Math.PI/2 + angleStep*i) * (radius + 24);
712
+ markup += `<line x1="${{cx}}" y1="${{cy}}" x2="${{x}}" y2="${{y}}" stroke="#dfe7ef"/>`;
713
+ markup += `<text x="${{lx}}" y="${{ly}}" text-anchor="middle" font-size="12" fill="#667487">${{axisLabels[axis]}}</text>`;
714
+ }});
715
+
716
+ clusters.forEach((cluster, idx) => {{
717
+ const points = axes.map((axis, i) => {{
718
+ const r = radius * ((cluster.values[axis] || 0) / 100);
719
+ return `${{cx + Math.cos(-Math.PI/2 + angleStep*i) * r}},${{cy + Math.sin(-Math.PI/2 + angleStep*i) * r}}`;
720
+ }}).join(" ");
721
+ markup += `<polygon points="${{points}}" fill="${{colors[idx]}}22" stroke="${{colors[idx]}}" stroke-width="3"/>`;
722
+ }});
723
+
724
+ svg.innerHTML = markup;
725
+ }}
726
+
727
+ async function loadDashboard() {{
728
+ const dashboard = await fetch("{dashboard_api}").then((r) => r.json());
729
+ const recommendations = await fetch("{recommendations_api}").then((r) => r.json()).catch(() => ({{ items: [] }}));
730
+
731
+ setText("dropoutRisk", `%${{(dashboard.dropout_risk * 100).toFixed(1)}}`);
732
+ setText("dropoutSub", `Basari olasiligi %${{(dashboard.success_probability * 100).toFixed(1)}}`);
733
+ setText("activityIndex", dashboard.activity_index.toFixed(1));
734
+ setText("activitySub", `${{dashboard.time_on_task_minutes_7d.toFixed(1)}} dakika / son 7 gun`);
735
+ setText("engagementScore", `${{dashboard.ai_engagement_score.toFixed(0)}}/100`);
736
+ setText("engagementSub", `Ortalama ustu AI etkilesimi`);
737
+ setText("cognitiveLoad", dashboard.cognitive_load_estimate);
738
+ setText("cognitiveSub", `%${{dashboard.cognitive_load_share.toFixed(0)}} ogrenci profili ile benzer`);
739
+
740
+ document.getElementById("feedbackList").innerHTML = dashboard.adaptive_feedback.map((item) => `<li>${{item}}</li>`).join("");
741
+ document.getElementById("recommendationList").innerHTML = recommendations.items.map((item) => `<li>Modul <strong>${{item.module_id}}</strong> — skor ${{item.score}}</li>`).join("");
742
+
743
+ renderTrendChart(dashboard.trend);
744
+ renderRadarChart(dashboard.behavior_clusters);
745
+ }}
746
+
747
+ loadDashboard().catch((error) => {{
748
+ document.getElementById("feedbackList").innerHTML = `<li>Dashboard yuklenemedi: ${{error.message}}</li>`;
749
+ }});
750
+ </script>
751
  </body>
752
  </html>
753
  """
backend/app/models/schemas.py CHANGED
@@ -58,6 +58,17 @@ class LearnerRegistrationResponse(BaseModel):
58
  next_step: str = "complete_diagnostic_assessment"
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
61
  class DiagnosticSubmission(BaseModel):
62
  responses: list[dict[str, Any]]
63
 
@@ -254,6 +265,21 @@ class AssessmentAttemptResult(BaseModel):
254
  human_review_required: bool = False
255
 
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  class LearnerDashboard(BaseModel):
258
  model_config = ConfigDict(from_attributes=True)
259
 
@@ -262,6 +288,12 @@ class LearnerDashboard(BaseModel):
262
  time_on_task_minutes_7d: float
263
  dropout_risk: float
264
  success_probability: float
 
 
 
 
 
 
265
  adaptive_feedback: list[str]
266
 
267
 
 
58
  next_step: str = "complete_diagnostic_assessment"
59
 
60
 
61
+ class LoginRequest(BaseModel):
62
+ email: EmailStr
63
+
64
+
65
+ class LoginResponse(BaseModel):
66
+ learner_id: UUID
67
+ first_name: str
68
+ dashboard_path: str
69
+ recommendations_path: str
70
+
71
+
72
  class DiagnosticSubmission(BaseModel):
73
  responses: list[dict[str, Any]]
74
 
 
265
  human_review_required: bool = False
266
 
267
 
268
+ class DashboardTrendPoint(BaseModel):
269
+ model_config = ConfigDict(from_attributes=True)
270
+
271
+ label: str
272
+ engagement_score: float
273
+ dropout_risk: float
274
+
275
+
276
+ class DashboardCluster(BaseModel):
277
+ model_config = ConfigDict(from_attributes=True)
278
+
279
+ label: str
280
+ values: dict[str, float]
281
+
282
+
283
  class LearnerDashboard(BaseModel):
284
  model_config = ConfigDict(from_attributes=True)
285
 
 
288
  time_on_task_minutes_7d: float
289
  dropout_risk: float
290
  success_probability: float
291
+ activity_index: float
292
+ ai_engagement_score: float
293
+ cognitive_load_estimate: str
294
+ cognitive_load_share: float
295
+ trend: list[DashboardTrendPoint]
296
+ behavior_clusters: list[DashboardCluster]
297
  adaptive_feedback: list[str]
298
 
299
 
backend/app/services/bootstrap.py CHANGED
@@ -14,128 +14,153 @@ from app.services import repository
14
 
15
 
16
  async def seed_demo_data(session: AsyncSession) -> None:
17
- if await repository.has_modules(session):
18
- return
19
 
20
- module_one = ModuleDetail(
21
- id="11111111-1111-1111-1111-111111111111",
22
- code="AID-101",
23
- title="AI-Supported Self-Regulated Learning",
24
- description="Introduces learners to using AI for planning, monitoring, and reflecting on self-directed study.",
25
- difficulty_level="foundation",
26
- estimated_total_minutes=750,
27
- topics=["self-regulation", "ai literacy"],
28
- objectives=[
29
- ModuleObjective(
30
- text="Explain how AI tools can support planning and self-monitoring.",
31
- bloom_level="understand",
32
- competency_tag="self_regulated_learning",
33
- measurable_outcome="Learner explains planning and monitoring strategies.",
34
- ),
35
- ModuleObjective(
36
- text="Apply AI-supported reflection to improve study habits.",
37
- bloom_level="apply",
38
- competency_tag="reflective_practice",
39
- measurable_outcome="Learner produces an evidence-based reflection.",
40
- ),
41
- ],
42
- units=[
43
- ModuleUnit(
44
- title="Planning with AI",
45
- unit_type="video",
46
- chunk_minutes=12,
47
- content_ref="/content/modules/aid-101/planning-with-ai",
48
- ),
49
- ModuleUnit(
50
- title="Monitoring Progress",
51
- unit_type="reading",
52
- chunk_minutes=10,
53
- content_ref="/content/modules/aid-101/monitoring-progress",
54
- ),
55
- ],
56
- activities=[
57
- ModuleActivity(
58
- title="Study Strategy Reflection",
59
- activity_type="reflection",
60
- instructions="Reflect on how AI could improve your weekly study routine.",
61
- max_score=20,
62
- )
63
- ],
64
- completion_criteria=ModuleCompletionCriteria(
65
- min_unit_completion_ratio=0.85,
66
- min_formative_score=70,
67
- min_time_on_task_minutes=300,
68
  ),
69
- module_vector=[0.35, 0.7, 0.5],
70
- )
71
-
72
- module_two = ModuleDetail(
73
- id="22222222-2222-2222-2222-222222222222",
74
- code="AID-205",
75
- title="Scenario-Based Problem Solving with AI",
76
- description="Builds authentic problem-solving skills through case analysis and AI-supported reasoning.",
77
- difficulty_level="intermediate",
78
- estimated_total_minutes=780,
79
- topics=["problem solving", "scenario analysis"],
80
- objectives=[
81
- ModuleObjective(
82
- text="Analyze scenario constraints and choose suitable AI-supported strategies.",
83
- bloom_level="analyze",
84
- competency_tag="applied_ai_literacy",
85
- measurable_outcome="Learner compares alternative strategies in a case.",
86
- ),
87
- ModuleObjective(
88
- text="Evaluate outputs for reliability, bias, and usefulness.",
89
- bloom_level="evaluate",
90
- competency_tag="critical_evaluation",
91
- measurable_outcome="Learner critiques AI output quality.",
92
- ),
93
- ],
94
- units=[
95
- ModuleUnit(
96
- title="Interpreting Realistic Cases",
97
- unit_type="simulation",
98
- chunk_minutes=15,
99
- content_ref="/content/modules/aid-205/interpreting-cases",
100
- ),
101
- ModuleUnit(
102
- title="Evaluating AI Output",
103
- unit_type="interactive",
104
- chunk_minutes=14,
105
- content_ref="/content/modules/aid-205/evaluating-output",
106
- ),
107
- ],
108
- activities=[
109
- ModuleActivity(
110
- title="Case Analysis Quiz",
111
- activity_type="quiz",
112
- instructions="Select and justify the best response in each scenario.",
113
- max_score=30,
114
- )
115
- ],
116
- completion_criteria=ModuleCompletionCriteria(
117
- min_unit_completion_ratio=0.9,
118
- min_formative_score=75,
119
- min_time_on_task_minutes=320,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  ),
121
- prerequisites=["11111111-1111-1111-1111-111111111111"],
122
- module_vector=[0.65, 0.8, 0.6],
123
- )
124
 
125
- await repository.create_module(session, module_one)
126
- await repository.create_module(session, module_two)
 
 
 
127
 
128
- await repository.create_assessment(
129
- session,
130
  AssessmentDefinition(
131
- id="33333333-3333-3333-3333-333333333333",
132
- module_id=module_one.id,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  assessment_type="summative",
134
- title="AID-101 Summative Assessment",
135
- blueprint={
136
- "objectives": ["self_regulated_learning", "reflective_practice"],
137
- "time_per_item_seconds": 420,
138
- },
139
  time_limit_minutes=45,
140
- )
141
- )
 
 
 
 
 
 
 
 
 
14
 
15
 
16
  async def seed_demo_data(session: AsyncSession) -> None:
17
+ existing_modules = await repository.list_modules(session)
18
+ existing_codes = {module.code for module in existing_modules}
19
 
20
+ modules = [
21
+ ModuleDetail(
22
+ id="11111111-1111-1111-1111-111111111111",
23
+ code="AID-101",
24
+ title="AI Nedir?",
25
+ description="Yapay zekanin tanimini, kapsamini, alt alanlarini ve gundelik yasamdaki uygulamalarini ele alan 5 ECTS giris modulu.",
26
+ difficulty_level="foundation",
27
+ estimated_total_minutes=750,
28
+ topics=["ai foundations", "ai literacy"],
29
+ objectives=[
30
+ ModuleObjective(
31
+ text="Yapay zekanin ne oldugunu ve hangi problem turlerinde kullanildigini acikla.",
32
+ bloom_level="understand",
33
+ competency_tag="ai_foundations",
34
+ measurable_outcome="Ogrenen, AI kavramini ve temel kullanim alanlarini tanimlar.",
35
+ ),
36
+ ModuleObjective(
37
+ text="Yapay zekayi otomasyon, algoritma ve insan zekasi kavramlariyla iliskilendir.",
38
+ bloom_level="analyze",
39
+ competency_tag="ai_context",
40
+ measurable_outcome="Ogrenen, AI kavramini ilgili kavramlardan ayirt eder.",
41
+ ),
42
+ ],
43
+ units=[
44
+ ModuleUnit(title="Yapay Zekaya Giris", unit_type="video", chunk_minutes=12, content_ref="/content/modules/aid-101/intro"),
45
+ ModuleUnit(title="AI'nin Uygulama Alanlari", unit_type="reading", chunk_minutes=14, content_ref="/content/modules/aid-101/applications"),
46
+ ModuleUnit(title="AI Mitleri ve Gercekler", unit_type="interactive", chunk_minutes=10, content_ref="/content/modules/aid-101/myths"),
47
+ ],
48
+ activities=[
49
+ ModuleActivity(title="Kavram Esleme Quiz", activity_type="quiz", instructions="AI, otomasyon ve algoritma kavramlarini dogru tanimlarla eslestir.", max_score=20),
50
+ ModuleActivity(title="Kisa Yansitma", activity_type="reflection", instructions="Gundelik yasaminda AI kullanimina uc ornek ver ve neden AI oldugunu acikla.", max_score=20),
51
+ ],
52
+ completion_criteria=ModuleCompletionCriteria(min_unit_completion_ratio=0.85, min_formative_score=70, min_time_on_task_minutes=300),
53
+ module_vector=[0.25, 0.40, 0.15],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  ),
55
+ ModuleDetail(
56
+ id="22222222-2222-2222-2222-222222222222",
57
+ code="AID-102",
58
+ title="AI Nasil Calisir?",
59
+ description="Veri, ozellik, model, tahmin, hata ve degerlendirme mantigini aciklayan 5 ECTS temel modul.",
60
+ difficulty_level="foundation",
61
+ estimated_total_minutes=750,
62
+ topics=["machine learning", "data", "model inference"],
63
+ objectives=[
64
+ ModuleObjective(
65
+ text="Veri, ozellik ve model arasindaki iliskiyi acikla.",
66
+ bloom_level="understand",
67
+ competency_tag="model_logic",
68
+ measurable_outcome="Ogrenen, basit bir modelleme akisini tanimlar.",
69
+ ),
70
+ ModuleObjective(
71
+ text="Bir AI sisteminin tahmin ve degerlendirme surecini ornekle acikla.",
72
+ bloom_level="apply",
73
+ competency_tag="inference_evaluation",
74
+ measurable_outcome="Ogrenen, model ciktilarini dogruluk ve hata baglaminda yorumlar.",
75
+ ),
76
+ ],
77
+ units=[
78
+ ModuleUnit(title="Veri ve Ozellikler", unit_type="reading", chunk_minutes=12, content_ref="/content/modules/aid-102/data-features"),
79
+ ModuleUnit(title="Model ve Tahmin", unit_type="video", chunk_minutes=13, content_ref="/content/modules/aid-102/model-inference"),
80
+ ModuleUnit(title="Dogruluk ve Hata", unit_type="interactive", chunk_minutes=15, content_ref="/content/modules/aid-102/evaluation"),
81
+ ],
82
+ activities=[
83
+ ModuleActivity(title="Tahmin Simulasyonu", activity_type="simulation", instructions="Ornek veri uzerinden model girdisi ve ciktilarini yorumla.", max_score=25),
84
+ ModuleActivity(title="Mini Vaka Analizi", activity_type="quiz", instructions="Basit bir AI sisteminin nasil karar verdigini secenekler arasindan belirle.", max_score=20),
85
+ ],
86
+ completion_criteria=ModuleCompletionCriteria(min_unit_completion_ratio=0.85, min_formative_score=72, min_time_on_task_minutes=300),
87
+ prerequisites=["11111111-1111-1111-1111-111111111111"],
88
+ module_vector=[0.45, 0.58, 0.32],
89
+ ),
90
+ ModuleDetail(
91
+ id="33333333-3333-3333-3333-333333333333",
92
+ code="AID-103",
93
+ title="AI Nasil Egitilir?",
94
+ description="Model egitimi, veri bolme, dogrulama, test, overfitting ve etik riskleri ele alan 5 ECTS modul.",
95
+ difficulty_level="intermediate",
96
+ estimated_total_minutes=750,
97
+ topics=["model training", "validation", "ai ethics"],
98
+ objectives=[
99
+ ModuleObjective(
100
+ text="Bir modelin egitim, dogrulama ve test surecini acikla.",
101
+ bloom_level="analyze",
102
+ competency_tag="training_pipeline",
103
+ measurable_outcome="Ogrenen, model egitiminin asamalarini dogru sira ile aciklar.",
104
+ ),
105
+ ModuleObjective(
106
+ text="Bias, overfitting ve guvenilirlik risklerini degerlendir.",
107
+ bloom_level="evaluate",
108
+ competency_tag="responsible_ai",
109
+ measurable_outcome="Ogrenen, model kalitesi ve etik riskleri tartisir.",
110
+ ),
111
+ ],
112
+ units=[
113
+ ModuleUnit(title="Model Egitim Akisi", unit_type="video", chunk_minutes=14, content_ref="/content/modules/aid-103/training-pipeline"),
114
+ ModuleUnit(title="Validation ve Test", unit_type="reading", chunk_minutes=12, content_ref="/content/modules/aid-103/validation-test"),
115
+ ModuleUnit(title="Bias ve Overfitting", unit_type="interactive", chunk_minutes=15, content_ref="/content/modules/aid-103/bias-overfitting"),
116
+ ],
117
+ activities=[
118
+ ModuleActivity(title="Train-Validation-Test Gorevi", activity_type="simulation", instructions="Veri setini dogru asamalara dagit ve en uygun egitim akisini sec.", max_score=25),
119
+ ModuleActivity(title="Etik Risk Tartismasi", activity_type="reflection", instructions="Bir modelin yanli veya guvenilmez hale gelmesine yol acabilecek riskleri acikla.", max_score=20),
120
+ ],
121
+ completion_criteria=ModuleCompletionCriteria(min_unit_completion_ratio=0.90, min_formative_score=75, min_time_on_task_minutes=320),
122
+ prerequisites=["22222222-2222-2222-2222-222222222222"],
123
+ module_vector=[0.66, 0.72, 0.44],
124
  ),
125
+ ]
 
 
126
 
127
+ created_codes: set[str] = set()
128
+ for module in modules:
129
+ if module.code not in existing_codes:
130
+ await repository.create_module(session, module)
131
+ created_codes.add(module.code)
132
 
133
+ assessments = [
 
134
  AssessmentDefinition(
135
+ id="44444444-4444-4444-4444-444444444444",
136
+ module_id="11111111-1111-1111-1111-111111111111",
137
+ assessment_type="summative",
138
+ title="AI Nedir? Summative Assessment",
139
+ blueprint={"objectives": ["ai_foundations", "ai_context"], "time_per_item_seconds": 360},
140
+ time_limit_minutes=40,
141
+ ),
142
+ AssessmentDefinition(
143
+ id="55555555-5555-5555-5555-555555555555",
144
+ module_id="22222222-2222-2222-2222-222222222222",
145
+ assessment_type="summative",
146
+ title="AI Nasil Calisir? Summative Assessment",
147
+ blueprint={"objectives": ["model_logic", "inference_evaluation"], "time_per_item_seconds": 360},
148
+ time_limit_minutes=40,
149
+ ),
150
+ AssessmentDefinition(
151
+ id="66666666-6666-6666-6666-666666666666",
152
+ module_id="33333333-3333-3333-3333-333333333333",
153
  assessment_type="summative",
154
+ title="AI Nasil Egitilir? Summative Assessment",
155
+ blueprint={"objectives": ["training_pipeline", "responsible_ai"], "time_per_item_seconds": 420},
 
 
 
156
  time_limit_minutes=45,
157
+ ),
158
+ ]
159
+
160
+ assessment_by_code = {
161
+ "AID-101": assessments[0],
162
+ "AID-102": assessments[1],
163
+ "AID-103": assessments[2],
164
+ }
165
+ for code in created_codes:
166
+ await repository.create_assessment(session, assessment_by_code[code])
backend/app/services/engines.py CHANGED
@@ -10,6 +10,8 @@ from app.models.schemas import (
10
  AssessmentAttemptResult,
11
  AssessmentDefinition,
12
  AssessmentItem,
 
 
13
  DiagnosticSubmission,
14
  InteractionEvent,
15
  LearnerDashboard,
@@ -157,6 +159,47 @@ async def build_dashboard(session: AsyncSession, learner: LearnerRecord) -> Lear
157
  total_minutes = round(sum((item.duration_ms or 0) for item in interactions) / 60000, 2)
158
  dropout_risk = learner.risk_profile.get("dropout_risk", 0.3)
159
  success_probability = round(max(0.2, 1 - dropout_risk + 0.15), 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  return LearnerDashboard(
162
  learner_id=learner.id,
@@ -164,6 +207,12 @@ async def build_dashboard(session: AsyncSession, learner: LearnerRecord) -> Lear
164
  time_on_task_minutes_7d=total_minutes,
165
  dropout_risk=dropout_risk,
166
  success_probability=success_probability,
 
 
 
 
 
 
167
  adaptive_feedback=[
168
  "Continue with short focused sessions of 10 to 15 minutes.",
169
  "Prioritize scenario-based practice to improve transfer and assessment performance.",
 
10
  AssessmentAttemptResult,
11
  AssessmentDefinition,
12
  AssessmentItem,
13
+ DashboardCluster,
14
+ DashboardTrendPoint,
15
  DiagnosticSubmission,
16
  InteractionEvent,
17
  LearnerDashboard,
 
159
  total_minutes = round(sum((item.duration_ms or 0) for item in interactions) / 60000, 2)
160
  dropout_risk = learner.risk_profile.get("dropout_risk", 0.3)
161
  success_probability = round(max(0.2, 1 - dropout_risk + 0.15), 2)
162
+ activity_index = round(min(100.0, 48 + total_minutes * 1.2 + len(interactions) * 1.8), 1)
163
+ ai_engagement_score = round(min(100.0, 44 + learner.readiness_score * 0.28 + len(interactions) * 2.4), 1)
164
+
165
+ if learner.readiness_score >= 70:
166
+ cognitive_load_estimate = "Orta"
167
+ cognitive_load_share = 36.0
168
+ elif learner.readiness_score >= 45:
169
+ cognitive_load_estimate = "Yuksek"
170
+ cognitive_load_share = 42.0
171
+ else:
172
+ cognitive_load_estimate = "Cok Yuksek"
173
+ cognitive_load_share = 55.0
174
+
175
+ base_engagement = max(35.0, min(92.0, activity_index))
176
+ base_risk = round(dropout_risk * 100, 1)
177
+ trend = [
178
+ DashboardTrendPoint(label="1 Mar", engagement_score=round(base_engagement - 4, 1), dropout_risk=max(4.0, round(base_risk - 3, 1))),
179
+ DashboardTrendPoint(label="5 Mar", engagement_score=round(base_engagement - 1, 1), dropout_risk=max(4.0, round(base_risk - 4, 1))),
180
+ DashboardTrendPoint(label="10 Mar", engagement_score=round(base_engagement + 3, 1), dropout_risk=max(4.0, round(base_risk - 5, 1))),
181
+ DashboardTrendPoint(label="15 Mar", engagement_score=round(base_engagement - 7, 1), dropout_risk=round(base_risk + 3, 1)),
182
+ DashboardTrendPoint(label="20 Mar", engagement_score=round(base_engagement - 2, 1), dropout_risk=round(base_risk + 1, 1)),
183
+ DashboardTrendPoint(label="25 Mar", engagement_score=round(base_engagement - 3, 1), dropout_risk=round(base_risk + 2, 1)),
184
+ DashboardTrendPoint(label="30 Mar", engagement_score=round(base_engagement + 1, 1), dropout_risk=max(4.0, round(base_risk, 1))),
185
+ DashboardTrendPoint(label="5 Nis", engagement_score=round(base_engagement + 2, 1), dropout_risk=max(4.0, round(base_risk - 1, 1))),
186
+ DashboardTrendPoint(label="10 Nis", engagement_score=round(base_engagement - 1, 1), dropout_risk=round(base_risk + 3, 1)),
187
+ ]
188
+
189
+ behavior_clusters = [
190
+ DashboardCluster(
191
+ label="Yuksek Performanslilar",
192
+ values={"video_izleme": 88, "forum_katilimi": 72, "odev_teslimi": 92, "quiz_performansi": 84, "notlara_erisim": 86, "canli_ders": 70},
193
+ ),
194
+ DashboardCluster(
195
+ label="Orta Performanslilar",
196
+ values={"video_izleme": 68, "forum_katilimi": 54, "odev_teslimi": 74, "quiz_performansi": 58, "notlara_erisim": 76, "canli_ders": 48},
197
+ ),
198
+ DashboardCluster(
199
+ label="Dusuk Performanslilar",
200
+ values={"video_izleme": 42, "forum_katilimi": 24, "odev_teslimi": 46, "quiz_performansi": 18, "notlara_erisim": 52, "canli_ders": 14},
201
+ ),
202
+ ]
203
 
204
  return LearnerDashboard(
205
  learner_id=learner.id,
 
207
  time_on_task_minutes_7d=total_minutes,
208
  dropout_risk=dropout_risk,
209
  success_probability=success_probability,
210
+ activity_index=activity_index,
211
+ ai_engagement_score=ai_engagement_score,
212
+ cognitive_load_estimate=cognitive_load_estimate,
213
+ cognitive_load_share=cognitive_load_share,
214
+ trend=trend,
215
+ behavior_clusters=behavior_clusters,
216
  adaptive_feedback=[
217
  "Continue with short focused sessions of 10 to 15 minutes.",
218
  "Prioritize scenario-based practice to improve transfer and assessment performance.",
backend/app/services/repository.py CHANGED
@@ -4,6 +4,7 @@ from uuid import UUID
4
 
5
  from fastapi import HTTPException, status
6
  from sqlalchemy import select
 
7
  from sqlalchemy.ext.asyncio import AsyncSession
8
  from sqlalchemy.orm import selectinload
9
 
@@ -154,6 +155,10 @@ def _attempt_to_schema(model: AssessmentAttemptORM) -> AssessmentAttempt:
154
 
155
 
156
  async def create_learner(session: AsyncSession, record: LearnerRecord) -> LearnerRecord:
 
 
 
 
157
  learner = LearnerORM(
158
  id=record.id,
159
  email=str(record.email),
@@ -191,7 +196,11 @@ async def create_learner(session: AsyncSession, record: LearnerRecord) -> Learne
191
  ],
192
  )
193
  session.add(learner)
194
- await session.commit()
 
 
 
 
195
  await session.refresh(learner)
196
  return await get_learner(session, learner.id)
197
 
@@ -249,6 +258,18 @@ async def get_learner(session: AsyncSession, learner_id: UUID) -> LearnerRecord:
249
  return _learner_to_schema(learner)
250
 
251
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  async def list_modules(session: AsyncSession) -> list[ModuleDetail]:
253
  stmt = select(ModuleORM).options(
254
  selectinload(ModuleORM.objectives),
 
4
 
5
  from fastapi import HTTPException, status
6
  from sqlalchemy import select
7
+ from sqlalchemy.exc import IntegrityError
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
  from sqlalchemy.orm import selectinload
10
 
 
155
 
156
 
157
  async def create_learner(session: AsyncSession, record: LearnerRecord) -> LearnerRecord:
158
+ existing_stmt = select(LearnerORM.id).where(LearnerORM.email == str(record.email))
159
+ if (await session.execute(existing_stmt)).scalar_one_or_none() is not None:
160
+ raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Learner already exists with this email")
161
+
162
  learner = LearnerORM(
163
  id=record.id,
164
  email=str(record.email),
 
196
  ],
197
  )
198
  session.add(learner)
199
+ try:
200
+ await session.commit()
201
+ except IntegrityError as exc:
202
+ await session.rollback()
203
+ raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Learner already exists with this email") from exc
204
  await session.refresh(learner)
205
  return await get_learner(session, learner.id)
206
 
 
258
  return _learner_to_schema(learner)
259
 
260
 
261
+ async def get_learner_by_email(session: AsyncSession, email: str) -> LearnerRecord:
262
+ stmt = (
263
+ select(LearnerORM)
264
+ .where(LearnerORM.email == email)
265
+ .options(selectinload(LearnerORM.prior_learning_items), selectinload(LearnerORM.goals))
266
+ )
267
+ learner = (await session.execute(stmt)).scalar_one_or_none()
268
+ if learner is None:
269
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No learner found for this email")
270
+ return _learner_to_schema(learner)
271
+
272
+
273
  async def list_modules(session: AsyncSession) -> list[ModuleDetail]:
274
  stmt = select(ModuleORM).options(
275
  selectinload(ModuleORM.objectives),