cryogenic22 commited on
Commit
4060e18
·
verified ·
1 Parent(s): f9b0731

Update src/components/learning_paths.py

Browse files
Files changed (1) hide show
  1. src/components/learning_paths.py +167 -181
src/components/learning_paths.py CHANGED
@@ -1,4 +1,4 @@
1
- import streamlit as st
2
  import json
3
  from datetime import datetime
4
  import os
@@ -6,17 +6,18 @@ from huggingface_hub import HfApi, Repository
6
  from pathlib import Path
7
  import tempfile
8
  import anthropic
 
9
 
10
  class LearningPaths:
11
  def __init__(self):
12
  self.initialize_storage()
13
  self.initialize_llm()
14
  self.load_curriculum()
 
15
 
16
  def initialize_storage(self):
17
  """Initialize HuggingFace storage connection and create repository if needed"""
18
  try:
19
- # Try to get token from Streamlit secrets first, then environment variables
20
  self.hf_token = st.secrets.get("HF_TOKEN") or os.getenv('HF_TOKEN')
21
  if not self.hf_token:
22
  st.error("HF_TOKEN not found in secrets or environment variables. Using local storage instead.")
@@ -24,25 +25,22 @@ class LearningPaths:
24
  return
25
 
26
  self.api = HfApi(token=self.hf_token)
27
- # Try to get repo_id from secrets first, then environment variables, then default
28
- self.repo_id = (st.secrets.get("HF_REPO_ID") or
29
- os.getenv('HF_REPO_ID') or
30
  f'{self.api.whoami()["name"]}/eduai-content')
31
-
32
- # Check if repository exists
33
  try:
34
  self.api.repo_info(repo_id=self.repo_id, repo_type="dataset")
35
  except Exception:
36
- # Repository doesn't exist, create it
37
  st.info(f"Creating new HuggingFace dataset repository: {self.repo_id}")
38
  self.api.create_repo(
39
  repo_id=self.repo_id,
40
  repo_type="dataset",
41
  private=True
42
  )
43
-
44
  self.use_local_storage = False
45
-
46
  except Exception as e:
47
  st.error(f"Error initializing HuggingFace storage: {e}")
48
  st.info("Falling back to local storage")
@@ -51,76 +49,20 @@ class LearningPaths:
51
  def initialize_llm(self):
52
  """Initialize Anthropic/Claude client"""
53
  try:
54
- # Try to get API key from secrets first, then environment variables
55
  api_key = st.secrets.get("ANTHROPIC_API_KEY") or os.getenv('ANTHROPIC_API_KEY')
56
  if not api_key:
57
  st.error("ANTHROPIC_API_KEY not found in secrets or environment variables")
58
  return
59
-
60
  self.client = anthropic.Anthropic(api_key=api_key)
61
  except Exception as e:
62
  st.error(f"Error initializing LLM: {e}")
63
 
64
- def load_curriculum(self):
65
- """Load or initialize curriculum structure"""
66
- # Try loading from HuggingFace
67
- curriculum = self.load_from_storage("curriculum.json")
68
- if curriculum:
69
- self.curriculum = curriculum
70
- return
71
 
72
- # Initialize default curriculum
73
- self.curriculum = {
74
- 'python_basics': {
75
- 'name': 'Python Programming Basics',
76
- 'description': 'Master the fundamental concepts of Python programming.',
77
- 'prerequisites': [],
78
- 'modules': [
79
- {
80
- 'id': 'intro_python',
81
- 'name': 'Introduction to Python',
82
- 'concepts': ['programming_basics', 'python_environment', 'basic_syntax'],
83
- 'difficulty': 'beginner',
84
- 'estimated_hours': 2
85
- },
86
- {
87
- 'id': 'variables_types',
88
- 'name': 'Variables and Data Types',
89
- 'concepts': ['variables', 'numbers', 'strings', 'type_conversion'],
90
- 'difficulty': 'beginner',
91
- 'estimated_hours': 3
92
- }
93
- ]
94
- },
95
- 'data_structures': {
96
- 'name': 'Data Structures',
97
- 'description': 'Learn essential Python data structures and their operations.',
98
- 'prerequisites': ['python_basics'],
99
- 'modules': [
100
- {
101
- 'id': 'lists_tuples',
102
- 'name': 'Lists and Tuples',
103
- 'concepts': ['list_operations', 'tuple_basics', 'sequence_types'],
104
- 'difficulty': 'intermediate',
105
- 'estimated_hours': 4
106
- },
107
- {
108
- 'id': 'dictionaries',
109
- 'name': 'Dictionaries',
110
- 'concepts': ['dict_operations', 'key_value_pairs', 'dict_methods'],
111
- 'difficulty': 'intermediate',
112
- 'estimated_hours': 3
113
- }
114
- ]
115
- }
116
- }
117
-
118
- # Save initial curriculum
119
- self.save_to_storage(self.curriculum, "curriculum.json")
120
-
121
- def generate_module_content(self, module_id: str) -> dict:
122
- """Generate content for a module using Claude"""
123
- # Find module details
124
  module = None
125
  for path in self.curriculum.values():
126
  for m in path['modules']:
@@ -133,8 +75,10 @@ class LearningPaths:
133
  if not module:
134
  return None
135
 
 
 
 
136
  try:
137
- # Generate content using Claude
138
  message = self.client.messages.create(
139
  model="claude-3-opus-20240229",
140
  max_tokens=1024,
@@ -171,100 +115,35 @@ class LearningPaths:
171
  )
172
 
173
  content = json.loads(message.content[0].text)
174
-
175
- # Save generated content
176
  content_path = f"content/{module_id}.json"
177
  self.save_to_storage(content, content_path)
178
 
 
 
179
  return content
180
 
181
  except Exception as e:
182
- st.error(f"Error generating content: {e}")
183
  return None
184
 
185
  def get_module_content(self, module_id: str) -> dict:
186
  """Get or generate module content"""
187
- # Try loading existing content
 
 
188
  content_path = f"content/{module_id}.json"
189
  content = self.load_from_storage(content_path)
190
-
191
- if not content:
192
- # Generate if doesn't exist
193
- content = self.generate_module_content(module_id)
194
-
195
- return content
196
-
197
- def save_to_storage(self, content: dict, path: str):
198
- """Save content to storage (HuggingFace or local)"""
199
- if self.use_local_storage:
200
- return self.save_locally(content, path)
201
-
202
- try:
203
- with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
204
- json.dump(content, tmp, indent=2)
205
- tmp.flush()
206
- self.api.upload_file(
207
- path_or_fileobj=tmp.name,
208
- path_in_repo=path,
209
- repo_id=self.repo_id,
210
- repo_type="dataset"
211
- )
212
- except Exception as e:
213
- st.error(f"Error saving to HuggingFace: {e}")
214
- # Fallback to local storage
215
- return self.save_locally(content, path)
216
 
217
- def load_from_storage(self, path: str) -> dict:
218
- """Load content from storage (HuggingFace or local)"""
219
- if self.use_local_storage:
220
- return self.load_locally(path)
221
-
222
- try:
223
- content = self.api.download_file(
224
- repo_id=self.repo_id,
225
- filename=path,
226
- repo_type="dataset"
227
- )
228
- return json.loads(content)
229
- except Exception:
230
- # Try loading from local storage as fallback
231
- return self.load_locally(path)
232
-
233
- def save_locally(self, content: dict, path: str):
234
- """Save content to local storage"""
235
- try:
236
- # Create data directory if it doesn't exist
237
- local_dir = Path("data")
238
- local_dir.mkdir(exist_ok=True)
239
-
240
- # Create subdirectories if needed
241
- file_path = local_dir / path
242
- file_path.parent.mkdir(parents=True, exist_ok=True)
243
-
244
- # Save the content
245
- with open(file_path, 'w') as f:
246
- json.dump(content, f, indent=2)
247
-
248
- except Exception as e:
249
- st.error(f"Error saving locally: {e}")
250
-
251
- def load_locally(self, path: str) -> dict:
252
- """Load content from local storage"""
253
- try:
254
- file_path = Path("data") / path
255
- if not file_path.exists():
256
- return None
257
-
258
- with open(file_path, 'r') as f:
259
- return json.load(f)
260
- except Exception:
261
  return None
262
 
 
 
 
263
  def display(self):
264
  """Display learning paths interface"""
265
  st.header("Learning Paths")
266
-
267
- # Topic selection
268
  selected_path = st.selectbox(
269
  "Select Learning Path",
270
  options=list(self.curriculum.keys()),
@@ -277,59 +156,54 @@ class LearningPaths:
277
  def display_path_content(self, path_id: str):
278
  """Display content for selected path"""
279
  path = self.curriculum[path_id]
280
-
281
- # Path header
282
  st.subheader(path['name'])
283
  st.write(path['description'])
284
-
285
- # Check prerequisites
286
  if path['prerequisites']:
287
  prereqs_met = self.check_prerequisites(path['prerequisites'])
288
  if not prereqs_met:
289
- st.warning("⚠️ Please complete the prerequisite paths first: " +
290
  ", ".join([self.curriculum[p]['name'] for p in path['prerequisites']]))
291
  return
292
 
293
- # Progress overview
294
  progress = self.get_path_progress(path_id)
295
  st.progress(progress, f"Progress: {int(progress * 100)}%")
296
 
297
- # Display modules
298
  for module in path['modules']:
299
  with st.expander(f"📚 {module['name']} ({module['difficulty']})"):
300
  completed = self.is_module_complete(module['id'])
301
-
302
  if completed:
303
  st.success("✅ Module completed!")
304
-
305
- # Get or generate content
 
 
 
306
  content = self.get_module_content(module['id'])
307
  if content:
308
- # Display introduction
309
  st.write(content['introduction'])
310
-
311
- # Display sections
312
  for section in content['sections']:
313
  st.markdown(f"### {section['title']}")
314
  st.write(section['content'])
315
  if 'examples' in section:
316
  for example in section['examples']:
317
  st.code(example)
318
-
319
- # Display exercises
320
  st.markdown("### Practice Exercises")
321
  for exercise in content['exercises']:
322
  with st.expander(exercise['title']):
323
  st.write(exercise['description'])
324
  if st.button("Show Solution", key=f"sol_{module['id']}_{exercise['title']}"):
325
  st.code(exercise['solution'])
326
-
327
- # Display quiz
328
  if not completed:
329
  st.markdown("### Quiz")
330
  correct_answers = 0
331
  total_questions = len(content['quiz']['questions'])
332
-
333
  for i, q in enumerate(content['quiz']['questions']):
334
  st.write(f"\n**Question {i+1}:** {q['question']}")
335
  answer = st.radio(
@@ -339,7 +213,7 @@ class LearningPaths:
339
  )
340
  if answer == q['options'][q['correct']]:
341
  correct_answers += 1
342
-
343
  if st.button("Submit Quiz", key=f"submit_{module['id']}"):
344
  score = correct_answers / total_questions
345
  if score >= 0.8:
@@ -348,13 +222,127 @@ class LearningPaths:
348
  else:
349
  st.warning(f"Score: {score*100:.0f}%. You need 80% to complete the module.")
350
  else:
351
- st.error("Error loading module content")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  def check_prerequisites(self, prerequisites: list) -> bool:
354
  """Check if prerequisites are met"""
355
  if 'completed_modules' not in st.session_state:
356
  st.session_state.completed_modules = []
357
-
358
  for prereq in prerequisites:
359
  prereq_modules = {m['id'] for m in self.curriculum[prereq]['modules']}
360
  if not prereq_modules.issubset(set(st.session_state.completed_modules)):
@@ -365,7 +353,7 @@ class LearningPaths:
365
  """Calculate progress percentage for a path"""
366
  if 'completed_modules' not in st.session_state:
367
  st.session_state.completed_modules = []
368
-
369
  path_modules = {m['id'] for m in self.curriculum[path_id]['modules']}
370
  completed = path_modules.intersection(set(st.session_state.completed_modules))
371
  return len(completed) / len(path_modules)
@@ -374,25 +362,23 @@ class LearningPaths:
374
  """Check if a module is completed"""
375
  if 'completed_modules' not in st.session_state:
376
  st.session_state.completed_modules = []
377
-
378
  return module_id in st.session_state.completed_modules
379
 
380
  def complete_module(self, module_id: str, path_id: str):
381
  """Mark a module as complete and update progress"""
382
  if 'completed_modules' not in st.session_state:
383
  st.session_state.completed_modules = []
384
-
385
  if module_id not in st.session_state.completed_modules:
386
  st.session_state.completed_modules.append(module_id)
387
-
388
- # Save progress
389
- if 'username' in st.session_state:
390
- progress = {
391
- 'completed_modules': st.session_state.completed_modules,
392
- 'last_active': datetime.now().isoformat()
393
- }
394
- progress_path = f"progress/{st.session_state.username}.json"
395
- self.save_to_storage(progress, progress_path)
396
 
397
  def load_user_progress(self, username: str):
398
  """Load user progress from storage"""
 
1
+ import streamlit as st
2
  import json
3
  from datetime import datetime
4
  import os
 
6
  from pathlib import Path
7
  import tempfile
8
  import anthropic
9
+ import time
10
 
11
  class LearningPaths:
12
  def __init__(self):
13
  self.initialize_storage()
14
  self.initialize_llm()
15
  self.load_curriculum()
16
+ self.content_generation_cache = {}
17
 
18
  def initialize_storage(self):
19
  """Initialize HuggingFace storage connection and create repository if needed"""
20
  try:
 
21
  self.hf_token = st.secrets.get("HF_TOKEN") or os.getenv('HF_TOKEN')
22
  if not self.hf_token:
23
  st.error("HF_TOKEN not found in secrets or environment variables. Using local storage instead.")
 
25
  return
26
 
27
  self.api = HfApi(token=self.hf_token)
28
+ self.repo_id = (st.secrets.get("HF_REPO_ID") or
29
+ os.getenv('HF_REPO_ID') or
 
30
  f'{self.api.whoami()["name"]}/eduai-content')
31
+
 
32
  try:
33
  self.api.repo_info(repo_id=self.repo_id, repo_type="dataset")
34
  except Exception:
 
35
  st.info(f"Creating new HuggingFace dataset repository: {self.repo_id}")
36
  self.api.create_repo(
37
  repo_id=self.repo_id,
38
  repo_type="dataset",
39
  private=True
40
  )
41
+
42
  self.use_local_storage = False
43
+
44
  except Exception as e:
45
  st.error(f"Error initializing HuggingFace storage: {e}")
46
  st.info("Falling back to local storage")
 
49
  def initialize_llm(self):
50
  """Initialize Anthropic/Claude client"""
51
  try:
 
52
  api_key = st.secrets.get("ANTHROPIC_API_KEY") or os.getenv('ANTHROPIC_API_KEY')
53
  if not api_key:
54
  st.error("ANTHROPIC_API_KEY not found in secrets or environment variables")
55
  return
56
+
57
  self.client = anthropic.Anthropic(api_key=api_key)
58
  except Exception as e:
59
  st.error(f"Error initializing LLM: {e}")
60
 
61
+ def generate_module_content_async(self, module_id: str):
62
+ """Asynchronous content generation with caching"""
63
+ if module_id in self.content_generation_cache:
64
+ return self.content_generation_cache[module_id]
 
 
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  module = None
67
  for path in self.curriculum.values():
68
  for m in path['modules']:
 
75
  if not module:
76
  return None
77
 
78
+ placeholder = st.empty()
79
+ placeholder.info("🔄 Generating learning content...")
80
+
81
  try:
 
82
  message = self.client.messages.create(
83
  model="claude-3-opus-20240229",
84
  max_tokens=1024,
 
115
  )
116
 
117
  content = json.loads(message.content[0].text)
 
 
118
  content_path = f"content/{module_id}.json"
119
  self.save_to_storage(content, content_path)
120
 
121
+ placeholder.empty()
122
+ self.content_generation_cache[module_id] = content
123
  return content
124
 
125
  except Exception as e:
126
+ placeholder.error(f"Error generating content: {e}")
127
  return None
128
 
129
  def get_module_content(self, module_id: str) -> dict:
130
  """Get or generate module content"""
131
+ if module_id in self.content_generation_cache:
132
+ return self.content_generation_cache[module_id]
133
+
134
  content_path = f"content/{module_id}.json"
135
  content = self.load_from_storage(content_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ if not content:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  return None
139
 
140
+ self.content_generation_cache[module_id] = content
141
+ return content
142
+
143
  def display(self):
144
  """Display learning paths interface"""
145
  st.header("Learning Paths")
146
+
 
147
  selected_path = st.selectbox(
148
  "Select Learning Path",
149
  options=list(self.curriculum.keys()),
 
156
  def display_path_content(self, path_id: str):
157
  """Display content for selected path"""
158
  path = self.curriculum[path_id]
159
+
 
160
  st.subheader(path['name'])
161
  st.write(path['description'])
162
+
 
163
  if path['prerequisites']:
164
  prereqs_met = self.check_prerequisites(path['prerequisites'])
165
  if not prereqs_met:
166
+ st.warning("⚠️ Please complete the prerequisite paths first: " +
167
  ", ".join([self.curriculum[p]['name'] for p in path['prerequisites']]))
168
  return
169
 
 
170
  progress = self.get_path_progress(path_id)
171
  st.progress(progress, f"Progress: {int(progress * 100)}%")
172
 
 
173
  for module in path['modules']:
174
  with st.expander(f"📚 {module['name']} ({module['difficulty']})"):
175
  completed = self.is_module_complete(module['id'])
176
+
177
  if completed:
178
  st.success("✅ Module completed!")
179
+
180
+ if st.button(f"Generate Content for {module['name']}", key=f"generate_{module['id']}"):
181
+ content = self.generate_module_content_async(module['id'])
182
+ st.experimental_rerun()
183
+
184
  content = self.get_module_content(module['id'])
185
  if content:
 
186
  st.write(content['introduction'])
187
+
 
188
  for section in content['sections']:
189
  st.markdown(f"### {section['title']}")
190
  st.write(section['content'])
191
  if 'examples' in section:
192
  for example in section['examples']:
193
  st.code(example)
194
+
 
195
  st.markdown("### Practice Exercises")
196
  for exercise in content['exercises']:
197
  with st.expander(exercise['title']):
198
  st.write(exercise['description'])
199
  if st.button("Show Solution", key=f"sol_{module['id']}_{exercise['title']}"):
200
  st.code(exercise['solution'])
201
+
 
202
  if not completed:
203
  st.markdown("### Quiz")
204
  correct_answers = 0
205
  total_questions = len(content['quiz']['questions'])
206
+
207
  for i, q in enumerate(content['quiz']['questions']):
208
  st.write(f"\n**Question {i+1}:** {q['question']}")
209
  answer = st.radio(
 
213
  )
214
  if answer == q['options'][q['correct']]:
215
  correct_answers += 1
216
+
217
  if st.button("Submit Quiz", key=f"submit_{module['id']}"):
218
  score = correct_answers / total_questions
219
  if score >= 0.8:
 
222
  else:
223
  st.warning(f"Score: {score*100:.0f}%. You need 80% to complete the module.")
224
  else:
225
+ st.info("Content not generated. Click 'Generate Content' to create learning materials.")
226
+
227
+ def load_curriculum(self):
228
+ """Load or initialize curriculum structure"""
229
+ curriculum = self.load_from_storage("curriculum.json")
230
+ if curriculum:
231
+ self.curriculum = curriculum
232
+ return
233
+
234
+ self.curriculum = {
235
+ 'python_basics': {
236
+ 'name': 'Python Programming Basics',
237
+ 'description': 'Master the fundamental concepts of Python programming.',
238
+ 'prerequisites': [],
239
+ 'modules': [
240
+ {
241
+ 'id': 'intro_python',
242
+ 'name': 'Introduction to Python',
243
+ 'concepts': ['programming_basics', 'python_environment', 'basic_syntax'],
244
+ 'difficulty': 'beginner',
245
+ 'estimated_hours': 2
246
+ },
247
+ {
248
+ 'id': 'variables_types',
249
+ 'name': 'Variables and Data Types',
250
+ 'concepts': ['variables', 'numbers', 'strings', 'type_conversion'],
251
+ 'difficulty': 'beginner',
252
+ 'estimated_hours': 3
253
+ }
254
+ ]
255
+ },
256
+ 'data_structures': {
257
+ 'name': 'Data Structures',
258
+ 'description': 'Learn essential Python data structures and their operations.',
259
+ 'prerequisites': ['python_basics'],
260
+ 'modules': [
261
+ {
262
+ 'id': 'lists_tuples',
263
+ 'name': 'Lists and Tuples',
264
+ 'concepts': ['list_operations', 'tuple_basics', 'sequence_types'],
265
+ 'difficulty': 'intermediate',
266
+ 'estimated_hours': 4
267
+ },
268
+ {
269
+ 'id': 'dictionaries',
270
+ 'name': 'Dictionaries',
271
+ 'concepts': ['dict_operations', 'key_value_pairs', 'dict_methods'],
272
+ 'difficulty': 'intermediate',
273
+ 'estimated_hours': 3
274
+ }
275
+ ]
276
+ }
277
+ }
278
+
279
+ self.save_to_storage(self.curriculum, "curriculum.json")
280
+
281
+ def save_to_storage(self, content: dict, path: str):
282
+ """Save content to storage (HuggingFace or local)"""
283
+ if self.use_local_storage:
284
+ return self.save_locally(content, path)
285
+
286
+ try:
287
+ with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
288
+ json.dump(content, tmp, indent=2)
289
+ tmp.flush()
290
+ self.api.upload_file(
291
+ path_or_fileobj=tmp.name,
292
+ path_in_repo=path,
293
+ repo_id=self.repo_id,
294
+ repo_type="dataset"
295
+ )
296
+ except Exception as e:
297
+ st.error(f"Error saving to HuggingFace: {e}")
298
+ return self.save_locally(content, path)
299
+
300
+ def load_from_storage(self, path: str) -> dict:
301
+ """Load content from storage (HuggingFace or local)"""
302
+ if self.use_local_storage:
303
+ return self.load_locally(path)
304
+
305
+ try:
306
+ content = self.api.download_file(
307
+ repo_id=self.repo_id,
308
+ filename=path,
309
+ repo_type="dataset"
310
+ )
311
+ return json.loads(content)
312
+ except Exception:
313
+ return self.load_locally(path)
314
+
315
+ def save_locally(self, content: dict, path: str):
316
+ """Save content to local storage"""
317
+ try:
318
+ local_dir = Path("data")
319
+ local_dir.mkdir(exist_ok=True)
320
+
321
+ file_path = local_dir / path
322
+ file_path.parent.mkdir(parents=True, exist_ok=True)
323
+
324
+ with open(file_path, 'w') as f:
325
+ json.dump(content, f, indent=2)
326
+ except Exception as e:
327
+ st.error(f"Error saving locally: {e}")
328
+
329
+ def load_locally(self, path: str) -> dict:
330
+ """Load content from local storage"""
331
+ try:
332
+ file_path = Path("data") / path
333
+ if not file_path.exists():
334
+ return None
335
+
336
+ with open(file_path, 'r') as f:
337
+ return json.load(f)
338
+ except Exception:
339
+ return None
340
 
341
  def check_prerequisites(self, prerequisites: list) -> bool:
342
  """Check if prerequisites are met"""
343
  if 'completed_modules' not in st.session_state:
344
  st.session_state.completed_modules = []
345
+
346
  for prereq in prerequisites:
347
  prereq_modules = {m['id'] for m in self.curriculum[prereq]['modules']}
348
  if not prereq_modules.issubset(set(st.session_state.completed_modules)):
 
353
  """Calculate progress percentage for a path"""
354
  if 'completed_modules' not in st.session_state:
355
  st.session_state.completed_modules = []
356
+
357
  path_modules = {m['id'] for m in self.curriculum[path_id]['modules']}
358
  completed = path_modules.intersection(set(st.session_state.completed_modules))
359
  return len(completed) / len(path_modules)
 
362
  """Check if a module is completed"""
363
  if 'completed_modules' not in st.session_state:
364
  st.session_state.completed_modules = []
365
+
366
  return module_id in st.session_state.completed_modules
367
 
368
  def complete_module(self, module_id: str, path_id: str):
369
  """Mark a module as complete and update progress"""
370
  if 'completed_modules' not in st.session_state:
371
  st.session_state.completed_modules = []
372
+
373
  if module_id not in st.session_state.completed_modules:
374
  st.session_state.completed_modules.append(module_id)
375
+
376
+ progress = {
377
+ 'completed_modules': st.session_state.completed_modules,
378
+ 'last_active': datetime.now().isoformat()
379
+ }
380
+ progress_path = f"progress/{st.session_state.username}.json"
381
+ self.save_to_storage(progress, progress_path)
 
 
382
 
383
  def load_user_progress(self, username: str):
384
  """Load user progress from storage"""