Harshal Vhatkar commited on
Commit
b8a1cb6
·
1 Parent(s): f39b63e

update pre-class-analytics

Browse files
Files changed (5) hide show
  1. .gitignore +5 -1
  2. app.py +5 -4
  3. pre_class_analytics.py +0 -850
  4. pre_class_analytics2.py +677 -0
  5. session_page.py +467 -106
.gitignore CHANGED
@@ -14,4 +14,8 @@ all_chat_histories2.json
14
  analytics.ipynb
15
  chat_history.csv
16
  harshal.py
17
- course_creation.py
 
 
 
 
 
14
  analytics.ipynb
15
  chat_history.csv
16
  harshal.py
17
+ course_creation.py
18
+ topics.json
19
+ new_analytics.json
20
+ new_analytics2.json
21
+ pre_class_analytics.py
app.py CHANGED
@@ -135,10 +135,11 @@ def login_form():
135
  col1, col2 = st.columns(2)
136
 
137
  with col1:
138
- user_type = st.selectbox(
139
  "Please select your Role",
140
- ["student", "faculty", "research_assistant", "analyst"]
141
  )
 
142
  username = st.text_input("Username or Email")
143
 
144
  with col2:
@@ -159,7 +160,7 @@ def login_form():
159
 
160
  def get_courses(username, user_type):
161
  if user_type == "student":
162
- student = students_collection.find_one({"full_name": username})
163
  if student:
164
  enrolled_course_ids = [
165
  course["course_id"] for course in student.get("enrolled_courses", [])
@@ -855,7 +856,7 @@ def enroll_in_course(course_id, course_title, student):
855
  {"$set": {"enrolled_courses": courses}},
856
  )
857
  st.success(f"Enrolled in course {course_title}")
858
- st.experimental_rerun()
859
  else:
860
  st.error("Course not found")
861
  else:
 
135
  col1, col2 = st.columns(2)
136
 
137
  with col1:
138
+ user_option = st.selectbox(
139
  "Please select your Role",
140
+ ["Student", "Faculty", "Research Assistant", "Analyst"]
141
  )
142
+ user_type = user_option.lower()
143
  username = st.text_input("Username or Email")
144
 
145
  with col2:
 
160
 
161
  def get_courses(username, user_type):
162
  if user_type == "student":
163
+ student = students_collection.find_one({"$or": [{"full_name": username}, {"username": username}]})
164
  if student:
165
  enrolled_course_ids = [
166
  course["course_id"] for course in student.get("enrolled_courses", [])
 
856
  {"$set": {"enrolled_courses": courses}},
857
  )
858
  st.success(f"Enrolled in course {course_title}")
859
+ # st.experimental_rerun()
860
  else:
861
  st.error("Course not found")
862
  else:
pre_class_analytics.py DELETED
@@ -1,850 +0,0 @@
1
- import re
2
- from bson import ObjectId
3
- from pymongo import MongoClient
4
- import pandas as pd
5
- import numpy as np
6
- from datetime import datetime
7
- from dotenv import load_dotenv
8
- import os
9
- from typing import List, Dict, Any
10
- from transformers import pipeline
11
- from textstat import flesch_reading_ease
12
- from collections import Counter
13
- import logging
14
- import spacy
15
- import json
16
-
17
- # Load chat histories from JSON file
18
- # all_chat_histories = []
19
- # with open(r'all_chat_histories2.json', 'r') as file:
20
- # all_chat_histories = json.load(file)
21
-
22
- load_dotenv()
23
- MONGO_URI = os.getenv("MONGO_URI")
24
- client = MongoClient(MONGO_URI)
25
- db = client['novascholar_db']
26
-
27
- chat_history_collection = db['chat_history']
28
-
29
- # def get_chat_history(user_id, session_id):
30
- # query = {
31
- # "user_id": ObjectId(user_id),
32
- # "session_id": session_id,
33
- # "timestamp": {"$lte": datetime.utcnow()}
34
- # }
35
- # result = chat_history_collection.find(query)
36
- # return list(result)
37
-
38
- # if __name__ == "__main__":
39
- # user_ids = ["6738b70cc97dffb641c7d158", "6738b7b33f648a9224f7aa69"]
40
- # session_ids = ["S104"]
41
- # for user_id in user_ids:
42
- # for session_id in session_ids:
43
- # result = get_chat_history(user_id, session_id)
44
- # print(result)
45
-
46
- # Configure logging
47
- logging.basicConfig(level=logging.INFO)
48
- logger = logging.getLogger(__name__)
49
-
50
- class NovaScholarAnalytics:
51
- def __init__(self):
52
- # Initialize NLP components
53
- self.nlp = spacy.load("en_core_web_sm")
54
- self.sentiment_analyzer = pipeline("sentiment-analysis", model="finiteautomata/bertweet-base-sentiment-analysis", top_k=None)
55
-
56
- # Define question words for detecting questions
57
- self.question_words = {"what", "why", "how", "when", "where", "which", "who", "whose", "whom"}
58
-
59
- # Define question categories
60
- self.question_categories = {
61
- 'conceptual': {'what', 'define', 'describe', 'explain'},
62
- 'procedural': {'how', 'steps', 'procedure', 'process'},
63
- 'reasoning': {'why', 'reason', 'cause', 'effect'},
64
- 'clarification': {'clarify', 'mean', 'difference', 'between'}
65
- }
66
-
67
- def _categorize_questions(self, questions_df: pd.DataFrame) -> Dict[str, int]:
68
- """
69
- Categorize questions into different types based on their content.
70
-
71
- Args:
72
- questions_df: DataFrame containing questions
73
-
74
- Returns:
75
- Dictionary with question categories and their counts
76
- """
77
- categories_count = {
78
- 'conceptual': 0,
79
- 'procedural': 0,
80
- 'reasoning': 0,
81
- 'clarification': 0,
82
- 'other': 0
83
- }
84
-
85
- for _, row in questions_df.iterrows():
86
- prompt_lower = row['prompt'].lower()
87
- categorized = False
88
-
89
- for category, keywords in self.question_categories.items():
90
- if any(keyword in prompt_lower for keyword in keywords):
91
- categories_count[category] += 1
92
- categorized = True
93
- break
94
-
95
- if not categorized:
96
- categories_count['other'] += 1
97
-
98
- return categories_count
99
-
100
-
101
- def _identify_frustration(self, df: pd.DataFrame) -> List[str]:
102
- """
103
- Identify signs of frustration in student messages.
104
-
105
- Args:
106
- df: DataFrame containing messages
107
-
108
- Returns:
109
- List of topics/areas where frustration was detected
110
- """
111
- frustration_indicators = [
112
- "don't understand", "confused", "difficult", "hard to",
113
- "not clear", "stuck", "help", "can't figure"
114
- ]
115
-
116
- frustrated_messages = df[
117
- df['prompt'].str.lower().str.contains('|'.join(frustration_indicators), na=False)
118
- ]
119
-
120
- if len(frustrated_messages) == 0:
121
- return []
122
-
123
- # Extract topics from frustrated messages
124
- frustrated_topics = self._extract_topics(frustrated_messages)
125
- return list(set(frustrated_topics)) # Unique topic
126
-
127
- def _calculate_resolution_times(self, df: pd.DataFrame) -> Dict[str, float]:
128
- """
129
- Calculate average time taken to resolve questions for different topics.
130
-
131
- Args:
132
- df: DataFrame containing messages
133
-
134
- Returns:
135
- Dictionary with topics and their average resolution times in minutes
136
- """
137
- resolution_times = {}
138
-
139
- # Group messages by topic
140
- topics = self._extract_topics(df)
141
- for topic in set(topics):
142
- escaped_topic = re.escape(topic)
143
- topic_msgs = df[df['prompt'].str.contains(escaped_topic, case=False)]
144
- if len(topic_msgs) >= 2:
145
- # Calculate time difference between first and last message
146
- start_time = pd.to_datetime(topic_msgs['timestamp'].iloc[0])
147
- end_time = pd.to_datetime(topic_msgs['timestamp'].iloc[-1])
148
- duration = (end_time - start_time).total_seconds() / 60 # Convert to minutes
149
- resolution_times[topic] = duration
150
-
151
- return resolution_times
152
-
153
- def _calculate_completion_rates(self, df: pd.DataFrame) -> Dict[str, float]:
154
- """
155
- Calculate completion rates for different topics.
156
-
157
- Args:
158
- df: DataFrame containing messages
159
-
160
- Returns:
161
- Dictionary with topics and their completion rates
162
- """
163
- completion_rates = {}
164
- topics = self._extract_topics(df)
165
-
166
- for topic in set(topics):
167
- escaped_topic = re.escape(topic)
168
- topic_msgs = df[df['prompt'].str.contains(escaped_topic, case=False)]
169
- if len(topic_msgs) > 0:
170
- # Consider a topic completed if there are no frustrated messages in the last 2 messages
171
- last_msgs = topic_msgs.tail(2)
172
- frustrated = self._identify_frustration(last_msgs)
173
- completion_rates[topic] = 0.0 if frustrated else 1.0
174
-
175
- return completion_rates
176
-
177
- def _analyze_time_distribution(self, df: pd.DataFrame) -> Dict[str, Dict[str, float]]:
178
- """
179
- Analyze time spent on different topics.
180
-
181
- Args:
182
- df: DataFrame containing messages
183
-
184
- Returns:
185
- Dictionary with time distribution statistics per topic
186
- """
187
- time_stats = {}
188
- topics = self._extract_topics(df)
189
-
190
- for topic in set(topics):
191
- escaped_topic = re.escape(topic)
192
- topic_msgs = df[df['prompt'].str.contains(escaped_topic, case=False)]
193
- if len(topic_msgs) >= 2:
194
- times = pd.to_datetime(topic_msgs['timestamp'])
195
- duration = (times.max() - times.min()).total_seconds() / 60
196
-
197
- time_stats[topic] = {
198
- 'total_minutes': duration,
199
- 'avg_minutes_per_message': duration / len(topic_msgs),
200
- 'message_count': len(topic_msgs)
201
- }
202
-
203
- return time_stats
204
-
205
- def _identify_coverage_gaps(self, df: pd.DataFrame) -> List[str]:
206
- """
207
- Identify topics with potential coverage gaps.
208
-
209
- Args:
210
- df: DataFrame containing messages
211
-
212
- Returns:
213
- List of topics with coverage gaps
214
- """
215
- gaps = []
216
- topics = self._extract_topics(df)
217
- topic_stats = self._analyze_time_distribution(df)
218
-
219
- for topic in set(topics):
220
- if topic in topic_stats:
221
- stats = topic_stats[topic]
222
- # Flag topics with very short interaction times or few messages
223
- if stats['total_minutes'] < 5 or stats['message_count'] < 3:
224
- gaps.append(topic)
225
-
226
- return gaps
227
-
228
- def _calculate_student_metrics(self, df: pd.DataFrame) -> Dict[str, Dict[str, float]]:
229
- """
230
- Calculate various metrics for each student.
231
-
232
- Args:
233
- df: DataFrame containing messages
234
-
235
- Returns:
236
- Dictionary with student metrics
237
- """
238
- student_metrics = {}
239
-
240
- for user_id in df['user_id'].unique():
241
- user_msgs = df[df['user_id'] == user_id]
242
-
243
- metrics = {
244
- 'message_count': len(user_msgs),
245
- 'question_count': len(user_msgs[user_msgs['prompt'].str.contains('|'.join(self.question_words), case=False)]),
246
- 'avg_response_length': user_msgs['response'].str.len().mean(),
247
- 'unique_topics': len(set(self._extract_topics(user_msgs))),
248
- 'frustration_count': len(self._identify_frustration(user_msgs))
249
- }
250
-
251
- student_metrics[user_id] = metrics
252
-
253
- return student_metrics
254
-
255
- def _determine_student_cluster(self, metrics: Dict[str, float]) -> str:
256
- """
257
- Determine which cluster a student belongs to based on their metrics.
258
-
259
- Args:
260
- metrics: Dictionary containing student metrics
261
-
262
- Returns:
263
- Cluster label ('confident', 'engaged', or 'struggling')
264
- """
265
- # Simple rule-based clustering
266
- if metrics['frustration_count'] > 2 or metrics['question_count'] / metrics['message_count'] > 0.7:
267
- return 'struggling'
268
- elif metrics['message_count'] > 10 and metrics['unique_topics'] > 3:
269
- return 'engaged'
270
- else:
271
- return 'confident'
272
-
273
- def _identify_abandon_points(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
274
- """
275
- Identify points where students abandoned topics.
276
-
277
- Args:
278
- df: DataFrame containing messages
279
-
280
- Returns:
281
- List of dictionaries containing abandon point information
282
- """
283
- abandon_points = []
284
- topics = self._extract_topics(df)
285
-
286
- for topic in set(topics):
287
- escaped_topic = re.escape(topic)
288
- topic_msgs = df[df['prompt'].str.contains(escaped_topic, case=False)]
289
- if len(topic_msgs) >= 2:
290
- # Check for large time gaps between messages
291
- times = pd.to_datetime(topic_msgs['timestamp'])
292
- time_gaps = times.diff()
293
-
294
- for idx, gap in enumerate(time_gaps):
295
- if gap and gap.total_seconds() > 600: # 10 minutes threshold
296
- abandon_points.append({
297
- 'topic': topic,
298
- 'message_before': topic_msgs.iloc[idx-1]['prompt'],
299
- 'time_gap': gap.total_seconds() / 60, # Convert to minutes
300
- 'resumed': idx < len(topic_msgs) - 1
301
- })
302
-
303
- return abandon_points
304
-
305
- def process_chat_history(self, chat_history: List[Dict[Any, Any]]) -> Dict[str, Any]:
306
- """
307
- Process chat history data and generate comprehensive analytics.
308
-
309
- Args:
310
- chat_history: List of chat history documents
311
- session_info: Dictionary containing session metadata (topic, duration, etc.)
312
-
313
- Returns:
314
- Dictionary containing all analytics results
315
- """
316
- try:
317
- # Convert chat history to DataFrame for easier processing
318
- messages_data = []
319
- for chat in chat_history:
320
- for msg in chat['messages']:
321
- messages_data.append({
322
- 'user_id': chat['user_id'],
323
- 'session_id': chat['session_id'],
324
- 'timestamp': msg['timestamp'],
325
- 'prompt': msg['prompt'],
326
- 'response': msg['response']
327
- })
328
-
329
- df = pd.DataFrame(messages_data)
330
-
331
- # Generate all analytics
332
- analytics_results = {
333
- 'topic_interaction': self._analyze_topic_interaction(df),
334
- 'question_patterns': self._analyze_question_patterns(df),
335
- 'sentiment_analysis': self._analyze_sentiment(df),
336
- 'completion_trends': self._analyze_completion_trends(df),
337
- 'student_clustering': self._cluster_students(df),
338
- 'abandoned_conversations': self._analyze_abandoned_conversations(df)
339
- }
340
-
341
- return analytics_results
342
-
343
- except Exception as e:
344
- logger.error(f"Error processing chat history: {str(e)}")
345
- raise
346
-
347
- def _analyze_topic_interaction(self, df: pd.DataFrame) -> Dict[str, Any]:
348
- """Analyze topic interaction frequency and patterns."""
349
- topics = self._extract_topics(df)
350
-
351
- topic_stats = {
352
- 'interaction_counts': Counter(topics),
353
- 'revisit_patterns': self._calculate_topic_revisits(df, topics),
354
- 'avg_time_per_topic': self._calculate_avg_time_per_topic(df, topics)
355
- }
356
-
357
- return topic_stats
358
-
359
- def _analyze_question_patterns(self, df: pd.DataFrame) -> Dict[str, Any]:
360
- """Analyze question patterns and identify difficult topics."""
361
- questions = df[df['prompt'].str.lower().str.split().apply(
362
- lambda x: any(word.lower() in self.question_words for word in x)
363
- )]
364
-
365
- question_stats = {
366
- 'total_questions': len(questions),
367
- 'question_types': self._categorize_questions(questions),
368
- 'complex_chains': self._identify_complex_chains(df)
369
- }
370
-
371
- return question_stats
372
-
373
- def _analyze_sentiment(self, df: pd.DataFrame) -> Dict[str, Any]:
374
- """Perform sentiment analysis on messages."""
375
- sentiments = []
376
- for prompt in df['prompt']:
377
- try:
378
- sentiment = self.sentiment_analyzer(prompt)[0]
379
- sentiments.append(sentiment['label'])
380
- except Exception as e:
381
- logger.warning(f"Error in sentiment analysis: {str(e)}")
382
- sentiments.append('NEUTRAL')
383
-
384
- sentiment_stats = {
385
- 'overall_sentiment': Counter(sentiments),
386
- 'frustration_indicators': self._identify_frustration(df),
387
- 'resolution_times': self._calculate_resolution_times(df)
388
- }
389
-
390
- return sentiment_stats
391
-
392
- def _analyze_completion_trends(self, df: pd.DataFrame) -> Dict[str, Any]:
393
- """Analyze topic completion trends and coverage."""
394
- completion_stats = {
395
- 'completion_rates': self._calculate_completion_rates(df),
396
- 'time_distribution': self._analyze_time_distribution(df),
397
- 'coverage_gaps': self._identify_coverage_gaps(df)
398
- }
399
-
400
- return completion_stats
401
-
402
- def _cluster_students(self, df: pd.DataFrame) -> Dict[str, Any]:
403
- """Cluster students based on interaction patterns."""
404
- student_metrics = self._calculate_student_metrics(df)
405
-
406
- clusters = {
407
- 'confident': [],
408
- 'engaged': [],
409
- 'struggling': []
410
- }
411
-
412
- for student_id, metrics in student_metrics.items():
413
- cluster = self._determine_student_cluster(metrics)
414
- clusters[cluster].append(student_id)
415
-
416
- return clusters
417
-
418
- def _analyze_abandoned_conversations(self, df: pd.DataFrame) -> Dict[str, Any]:
419
- """Identify and analyze abandoned conversations."""
420
- abandoned_stats = {
421
- 'abandon_points': self._identify_abandon_points(df),
422
- 'incomplete_topics': self._identify_incomplete_topics(df),
423
- 'dropout_patterns': self._analyze_dropout_patterns(df)
424
- }
425
-
426
- return abandoned_stats
427
-
428
- def _identify_incomplete_topics(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
429
- """
430
- Identify topics that were started but not completed by students.
431
-
432
- Args:
433
- df: DataFrame containing messages
434
-
435
- Returns:
436
- List of dictionaries containing incomplete topic information
437
- """
438
- incomplete_topics = []
439
- topics = self._extract_topics(df)
440
-
441
- for topic in set(topics):
442
- escaped_topic = re.escape(topic)
443
- topic_msgs = df[df['prompt'].str.contains(escaped_topic, case=False)]
444
-
445
- if len(topic_msgs) > 0:
446
- # Check for completion indicators
447
- last_msgs = topic_msgs.tail(3) # Look at last 3 messages
448
-
449
- # Consider a topic incomplete if:
450
- # 1. There are unanswered questions
451
- # 2. Contains frustration indicators
452
- # 3. No positive confirmation/understanding indicators
453
- has_questions = last_msgs['prompt'].str.contains('|'.join(self.question_words), case=False).any()
454
- has_frustration = bool(self._identify_frustration(last_msgs))
455
-
456
- completion_indicators = ['understand', 'got it', 'makes sense', 'thank you', 'clear now']
457
- has_completion = last_msgs['prompt'].str.contains('|'.join(completion_indicators), case=False).any()
458
-
459
- if (has_questions or has_frustration) and not has_completion:
460
- incomplete_topics.append({
461
- 'topic': topic,
462
- 'last_interaction': topic_msgs.iloc[-1]['timestamp'],
463
- 'message_count': len(topic_msgs),
464
- 'has_pending_questions': has_questions,
465
- 'shows_frustration': has_frustration
466
- })
467
-
468
- return incomplete_topics
469
-
470
- def _analyze_dropout_patterns(self, df: pd.DataFrame) -> Dict[str, Any]:
471
- """
472
- Analyze patterns in where and why students tend to drop out of conversations.
473
-
474
- Args:
475
- df: DataFrame containing messages
476
-
477
- Returns:
478
- Dictionary containing dropout pattern analysis
479
- """
480
- dropout_analysis = {
481
- 'timing_patterns': {},
482
- 'topic_patterns': {},
483
- 'complexity_indicators': {},
484
- 'engagement_metrics': {}
485
- }
486
-
487
- # Analyze timing of dropouts
488
- timestamps = pd.to_datetime(df['timestamp'])
489
- time_gaps = timestamps.diff()
490
- dropout_points = time_gaps[time_gaps > pd.Timedelta(minutes=30)].index
491
-
492
- for point in dropout_points:
493
- # Get context before dropout
494
- context_msgs = df.loc[max(0, point-5):point]
495
-
496
- # Analyze timing
497
- time_of_day = timestamps[point].hour
498
- dropout_analysis['timing_patterns'][time_of_day] = \
499
- dropout_analysis['timing_patterns'].get(time_of_day, 0) + 1
500
-
501
- # Analyze topics at dropout points
502
- dropout_topics = self._extract_topics(context_msgs)
503
- for topic in dropout_topics:
504
- dropout_analysis['topic_patterns'][topic] = \
505
- dropout_analysis['topic_patterns'].get(topic, 0) + 1
506
-
507
- # Analyze complexity
508
- msg_lengths = context_msgs['prompt'].str.len().mean()
509
- question_density = len(context_msgs[context_msgs['prompt'].str.contains(
510
- '|'.join(self.question_words), case=False)]) / len(context_msgs)
511
-
512
- dropout_analysis['complexity_indicators'][point] = {
513
- 'message_length': msg_lengths,
514
- 'question_density': question_density
515
- }
516
-
517
- # Analyze engagement
518
- dropout_analysis['engagement_metrics'][point] = {
519
- 'messages_before_dropout': len(context_msgs),
520
- 'response_times': time_gaps[max(0, point-5):point].mean().total_seconds() / 60
521
- }
522
-
523
- return dropout_analysis
524
-
525
- def _rank_topics_by_difficulty(self, analytics_results: Dict[str, Any]) -> List[Dict[str, Any]]:
526
- """
527
- Rank topics by their difficulty based on various metrics from analytics results.
528
-
529
- Args:
530
- analytics_results: Dictionary containing all analytics data
531
-
532
- Returns:
533
- List of dictionaries containing topic difficulty rankings and scores
534
- """
535
- topic_difficulty = []
536
-
537
- # Extract relevant metrics for each topic
538
- topics = set()
539
- for topic in analytics_results['topic_interaction']['interaction_counts'].keys():
540
-
541
- # Calculate difficulty score based on multiple factors
542
- difficulty_score = 0
543
-
544
- # Factor 1: Question frequency
545
- question_count = sum(1 for chain in analytics_results['question_patterns']['complex_chains']
546
- if chain['topic'] == topic)
547
- difficulty_score += question_count * 0.3
548
-
549
- # Factor 2: Frustration indicators
550
- frustration_count = sum(1 for indicator in analytics_results['sentiment_analysis']['frustration_indicators']
551
- if topic.lower() in indicator.lower())
552
- difficulty_score += frustration_count * 0.25
553
-
554
- # Factor 3: Completion rate (inverse relationship)
555
- completion_rate = analytics_results['completion_trends']['completion_rates'].get(topic, 1.0)
556
- difficulty_score += (1 - completion_rate) * 0.25
557
-
558
- # Factor 4: Time spent (normalized)
559
- avg_time = analytics_results['topic_interaction']['avg_time_per_topic'].get(topic, 0)
560
- max_time = max(analytics_results['topic_interaction']['avg_time_per_topic'].values())
561
- normalized_time = avg_time / max_time if max_time > 0 else 0
562
- difficulty_score += normalized_time * 0.2
563
-
564
- topic_difficulty.append({
565
- 'topic': topic,
566
- 'difficulty_score': round(difficulty_score, 2),
567
- 'metrics': {
568
- 'question_frequency': question_count,
569
- 'frustration_indicators': frustration_count,
570
- 'completion_rate': completion_rate,
571
- 'avg_time_spent': avg_time
572
- }
573
- })
574
-
575
- # Sort topics by difficulty score
576
- return sorted(topic_difficulty, key=lambda x: x['difficulty_score'], reverse=True)
577
-
578
- def _identify_support_needs(self, analytics_results: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
579
- """
580
- Identify specific support needs for students based on analytics results.
581
-
582
- Args:
583
- analytics_results: Dictionary containing all analytics data
584
-
585
- Returns:
586
- Dictionary containing support needs categorized by urgency
587
- """
588
- support_needs = {
589
- 'immediate_attention': [],
590
- 'monitoring_needed': [],
591
- 'general_support': []
592
- }
593
-
594
- # Analyze struggling students
595
- for student_id in analytics_results['student_clustering']['struggling']:
596
- # Get student-specific metrics
597
- student_msgs = analytics_results.get('sentiment_analysis', {}).get('messages', [])
598
- frustration_topics = [topic for topic in analytics_results['sentiment_analysis']['frustration_indicators']
599
- if any(msg['user_id'] == student_id for msg in student_msgs)]
600
-
601
- # Calculate engagement metrics
602
- engagement_level = len([chain for chain in analytics_results['question_patterns']['complex_chains']
603
- if any(msg['user_id'] == student_id for msg in chain['messages'])])
604
-
605
- # Identify immediate attention needs
606
- if len(frustration_topics) >= 3 or engagement_level < 2:
607
- support_needs['immediate_attention'].append({
608
- 'student_id': student_id,
609
- 'issues': frustration_topics,
610
- 'engagement_level': engagement_level,
611
- 'recommended_actions': [
612
- 'Schedule one-on-one session',
613
- 'Review difficult topics',
614
- 'Provide additional resources'
615
- ]
616
- })
617
-
618
- # Identify monitoring needs
619
- elif len(frustration_topics) >= 1 or engagement_level < 4:
620
- support_needs['monitoring_needed'].append({
621
- 'student_id': student_id,
622
- 'areas_of_concern': frustration_topics,
623
- 'engagement_level': engagement_level,
624
- 'recommended_actions': [
625
- 'Regular progress checks',
626
- 'Provide supplementary materials'
627
- ]
628
- })
629
-
630
- # General support needs
631
- else:
632
- support_needs['general_support'].append({
633
- 'student_id': student_id,
634
- 'areas_for_improvement': frustration_topics,
635
- 'engagement_level': engagement_level,
636
- 'recommended_actions': [
637
- 'Maintain regular communication',
638
- 'Encourage participation'
639
- ]
640
- })
641
-
642
- return support_needs
643
-
644
-
645
- def _extract_topics(self, df: pd.DataFrame) -> List[str]:
646
- """Extract topics from messages using spaCy."""
647
- topics = []
648
- for doc in self.nlp.pipe(df['prompt']):
649
- # Extract noun phrases as potential topics
650
- noun_phrases = [chunk.text for chunk in doc.noun_chunks]
651
- topics.extend(noun_phrases)
652
- return topics
653
-
654
- def _calculate_topic_revisits(self, df: pd.DataFrame, topics: List[str]) -> Dict[str, int]:
655
- """Calculate how often topics are revisited."""
656
- topic_visits = Counter(topics)
657
- return {topic: count for topic, count in topic_visits.items() if count > 1}
658
-
659
- def _calculate_avg_time_per_topic(self, df: pd.DataFrame, topics: List[str]) -> Dict[str, float]:
660
- """Calculate average time spent per topic."""
661
- topic_times = {}
662
- for topic in set(topics):
663
- escaped_topic = re.escape(topic)
664
- topic_msgs = df[df['prompt'].str.contains(escaped_topic, case=False)]
665
- if len(topic_msgs) > 1:
666
- time_diffs = pd.to_datetime(topic_msgs['timestamp']).diff()
667
- avg_time = time_diffs.mean().total_seconds() / 60 # Convert to minutes
668
- topic_times[topic] = avg_time
669
- return topic_times
670
-
671
- def _identify_complex_chains(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
672
- """Identify complex conversation chains."""
673
- chains = []
674
- current_chain = []
675
-
676
- for idx, row in df.iterrows():
677
- if self._is_followup_question(row['prompt']):
678
- current_chain.append(row)
679
- else:
680
- if len(current_chain) >= 3: # Consider 3+ related questions as complex chain
681
- chains.append({
682
- 'messages': current_chain,
683
- 'topic': self._extract_topics([current_chain[0]['prompt']])[0],
684
- 'length': len(current_chain)
685
- })
686
- current_chain = []
687
-
688
- return chains
689
-
690
- def _generate_topic_priority_list(self, analytics_results: Dict[str, Any]) -> List[Dict[str, Any]]:
691
- """
692
- Generate a prioritized list of topics for the upcoming session.
693
-
694
- Args:
695
- analytics_results: Dictionary containing all analytics data
696
-
697
- Returns:
698
- List of dictionaries containing topics and their priority scores
699
- """
700
- topic_priorities = []
701
-
702
- # Get difficulty rankings
703
- difficulty_ranking = self._rank_topics_by_difficulty(analytics_results)
704
-
705
- for topic_data in difficulty_ranking:
706
- topic = topic_data['topic']
707
-
708
- # Calculate priority score based on multiple factors
709
- priority_score = 0
710
-
711
- # Factor 1: Difficulty score (40% weight)
712
- priority_score += topic_data['difficulty_score'] * 0.4
713
-
714
- # Factor 2: Student frustration (25% weight)
715
- frustration_count = sum(1 for indicator in
716
- analytics_results['sentiment_analysis']['frustration_indicators']
717
- if topic.lower() in indicator.lower())
718
- normalized_frustration = min(frustration_count / 5, 1) # Cap at 5 frustrations
719
- priority_score += normalized_frustration * 0.25
720
-
721
- # Factor 3: Incomplete understanding (20% weight)
722
- incomplete_topics = analytics_results.get('abandoned_conversations', {}).get('incomplete_topics', [])
723
- if any(t['topic'] == topic for t in incomplete_topics):
724
- priority_score += 0.2
725
-
726
- # Factor 4: Coverage gaps (15% weight)
727
- if topic in analytics_results['completion_trends']['coverage_gaps']:
728
- priority_score += 0.15
729
-
730
- topic_priorities.append({
731
- 'topic': topic,
732
- 'priority_score': round(priority_score, 2),
733
- 'reasons': {
734
- 'difficulty_level': topic_data['difficulty_score'],
735
- 'frustration_indicators': frustration_count,
736
- 'has_incomplete_understanding': any(t['topic'] == topic for t in incomplete_topics),
737
- 'has_coverage_gaps': topic in analytics_results['completion_trends']['coverage_gaps']
738
- },
739
- 'recommended_focus_areas': self._generate_focus_recommendations(topic_data, analytics_results)
740
- })
741
-
742
- # Sort by priority score
743
- return sorted(topic_priorities, key=lambda x: x['priority_score'], reverse=True)
744
-
745
- def _generate_focus_recommendations(self, topic_data: Dict[str, Any],
746
- analytics_results: Dict[str, Any]) -> List[str]:
747
- """Generate specific focus recommendations for a topic."""
748
- recommendations = []
749
-
750
- if topic_data['metrics']['question_frequency'] > 3:
751
- recommendations.append("Provide more detailed explanations and examples")
752
-
753
- if topic_data['metrics']['completion_rate'] < 0.7:
754
- recommendations.append("Break down complex concepts into smaller segments")
755
-
756
- if topic_data['metrics']['frustration_indicators'] > 2:
757
- recommendations.append("Review prerequisite concepts and provide additional context")
758
-
759
- return recommendations
760
-
761
- def _is_followup_question(self, prompt: str) -> bool:
762
- """Determine if a prompt is a follow-up question."""
763
- followup_indicators = {'also', 'then', 'additionally', 'furthermore', 'related to that'}
764
- return any(indicator in prompt.lower() for indicator in followup_indicators)
765
-
766
- def generate_faculty_report(self, analytics_results: Dict[str, Any]) -> Dict[str, Any]:
767
- """Generate a comprehensive report for faculty."""
768
- report = {
769
- 'key_findings': self._generate_key_findings(analytics_results),
770
- 'recommended_actions': self._generate_recommendations(analytics_results),
771
- 'topic_difficulty_ranking': self._rank_topics_by_difficulty(analytics_results),
772
- 'student_support_needs': self._identify_support_needs(analytics_results),
773
- 'topic_priorities': self._generate_topic_priority_list(analytics_results)
774
- }
775
-
776
- return report
777
-
778
- def _generate_key_findings(self, analytics_results: Dict[str, Any]) -> List[str]:
779
- """Generate key findings from analytics results."""
780
- findings = []
781
-
782
- # Analyze topic interaction patterns
783
- topic_stats = analytics_results['topic_interaction']
784
- low_interaction_topics = [topic for topic, count in topic_stats['interaction_counts'].items()
785
- if count < 3] # Arbitrary threshold
786
- if low_interaction_topics:
787
- findings.append(f"Low engagement detected in topics: {', '.join(low_interaction_topics)}")
788
-
789
- # Analyze sentiment patterns
790
- sentiment_stats = analytics_results['sentiment_analysis']
791
- if sentiment_stats['frustration_indicators']:
792
- findings.append("Significant frustration detected in the following areas: " +
793
- ', '.join(sentiment_stats['frustration_indicators']))
794
-
795
- # Analyze student clustering
796
- student_clusters = analytics_results['student_clustering']
797
- if len(student_clusters['struggling']) > 0:
798
- findings.append(f"{len(student_clusters['struggling'])} students showing signs of difficulty")
799
-
800
- return findings
801
-
802
- def _generate_recommendations(self, analytics_results: Dict[str, Any]) -> List[str]:
803
- """Generate actionable recommendations for faculty."""
804
- recommendations = []
805
-
806
- # Analyze complex chains
807
- question_patterns = analytics_results['question_patterns']
808
- if question_patterns['complex_chains']:
809
- topics_needing_clarity = set(chain['topic'] for chain in question_patterns['complex_chains'])
810
- recommendations.append(f"Consider providing additional examples for: {', '.join(topics_needing_clarity)}")
811
-
812
- # Analyze completion trends
813
- completion_trends = analytics_results['completion_trends']
814
- low_completion_topics = [topic for topic, rate in completion_trends['completion_rates'].items()
815
- if rate < 0.7] # 70% threshold
816
- if low_completion_topics:
817
- recommendations.append(f"Review and possibly simplify material for: {', '.join(low_completion_topics)}")
818
-
819
- return recommendations
820
-
821
- # Example usage
822
- if __name__ == "__main__":
823
- # Initialize analytics engine
824
- analytics_engine = NovaScholarAnalytics()
825
-
826
- # Sample usage with dummy data
827
- sample_chat_history = [
828
- {
829
- "user_id": "123",
830
- "session_id": "S101",
831
- "messages": [
832
- {
833
- "prompt": "What is DevOps?",
834
- "response": "DevOps is a software engineering practice...",
835
- "timestamp": datetime.now()
836
- }
837
- ]
838
- }
839
- ]
840
-
841
- # Process analytics
842
- #results = analytics_engine.process_chat_history(all_chat_histories)
843
-
844
- # Generate faculty report
845
- #faculty_report = analytics_engine.generate_faculty_report(results)
846
- #print(faculty_report)
847
- # Print results
848
- # logger.info("Analytics processing completed")
849
- # logger.info(f"Key findings: {faculty_report['key_findings']}")
850
- # logger.info(f"Recommendations: {faculty_report['recommended_actions']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pre_class_analytics2.py ADDED
@@ -0,0 +1,677 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import typing_extensions as typing
3
+ import google.generativeai as genai
4
+ from typing import List, Dict, Any
5
+ import numpy as np
6
+ from collections import defaultdict
7
+
8
+ from dotenv import load_dotenv
9
+ import os
10
+ import pymongo
11
+ from pymongo import MongoClient
12
+
13
+ load_dotenv()
14
+ GEMINI_API_KEY = os.getenv('GEMINI_KEY')
15
+
16
+ class EngagementMetrics(typing.TypedDict):
17
+ participation_level: str # "high" | "medium" | "low"
18
+ question_quality: str # "advanced" | "intermediate" | "basic"
19
+ concept_understanding: str # "strong" | "moderate" | "needs_improvement"
20
+
21
+ class StudentInsight(typing.TypedDict):
22
+ student_id: str
23
+ performance_level: str # "high_performer" | "average" | "at_risk"
24
+ struggling_topics: list[str]
25
+ engagement_metrics: EngagementMetrics
26
+
27
+ class TopicInsight(typing.TypedDict):
28
+ topic: str
29
+ difficulty_level: float # 0 to 1
30
+ student_count: int
31
+ common_issues: list[str]
32
+ key_misconceptions: list[str]
33
+
34
+ class RecommendedAction(typing.TypedDict):
35
+ action: str
36
+ priority: str # "high" | "medium" | "low"
37
+ target_group: str # "all_students" | "specific_students" | "faculty"
38
+ reasoning: str
39
+ expected_impact: str
40
+
41
+ class ClassDistribution(typing.TypedDict):
42
+ high_performers: float
43
+ average_performers: float
44
+ at_risk: float
45
+
46
+ class CourseHealth(typing.TypedDict):
47
+ overall_engagement: float # 0 to 1
48
+ critical_topics: list[str]
49
+ class_distribution: ClassDistribution
50
+
51
+ class InterventionMetrics(typing.TypedDict):
52
+ immediate_attention_needed: list[str] # student_ids
53
+ monitoring_required: list[str] # student_ids
54
+
55
+ class AnalyticsResponse(typing.TypedDict):
56
+ topic_insights: list[TopicInsight]
57
+ student_insights: list[StudentInsight]
58
+ recommended_actions: list[RecommendedAction]
59
+ course_health: CourseHealth
60
+ intervention_metrics: InterventionMetrics
61
+
62
+
63
+
64
+ class NovaScholarAnalytics:
65
+ def __init__(self, model_name: str = "gemini-1.5-flash"):
66
+ genai.configure(api_key=GEMINI_API_KEY)
67
+ self.model = genai.GenerativeModel(model_name)
68
+
69
+ def _create_analytics_prompt(self, chat_histories: List[Dict], all_topics: List[str]) -> str:
70
+ """Creates a structured prompt for Gemini to analyze chat histories."""
71
+ # Prompt 1:
72
+ # return f"""Analyze these student chat histories for a university course and provide detailed analytics.
73
+
74
+ # Context:
75
+ # - These are pre-class chat interactions between students and an AI tutor
76
+ # - Topics covered: {', '.join(all_topics)}
77
+
78
+ # Chat histories: {json.dumps(chat_histories, indent=2)}
79
+
80
+ # Return the analysis in JSON format matching this exact schema:
81
+ # {AnalyticsResponse.__annotations__}
82
+
83
+ # Ensure all numeric values are between 0 and 1 (accuracy upto 3 decimal places) where applicable.
84
+
85
+ # Important analysis guidelines:
86
+ # 1. Identify topics where students show confusion or ask multiple follow-up questions
87
+ # 2. Look for patterns in question types and complexity
88
+ # 3. Analyze response understanding based on follow-up questions
89
+ # 4. Consider both explicit and implicit signs of difficulty
90
+ # 5. Focus on concept relationships and prerequisite understanding"""
91
+
92
+ # Prompt 2:
93
+ # return f"""Analyze the provided student chat histories for a university course and generate concise, actionable analytics.
94
+
95
+ # Context:
96
+ # - Chat histories: {json.dumps(chat_histories, indent=2)}
97
+ # - These are pre-class interactions between students and an AI tutor aimed at identifying learning difficulties and improving course delivery.
98
+ # - Topics covered: {', '.join(all_topics)}.
99
+
100
+ # Your task is to extract key insights that will help faculty address challenges effectively and enhance learning outcomes.
101
+
102
+ # Output Format:
103
+ # 1. Topics where students face significant difficulties:
104
+ # - Provide a ranked list of topics where the majority of students are struggling, based on the frequency and nature of their questions or misconceptions.
105
+ # - Include the percentage of students who found each topic challenging.
106
+
107
+ # 2. AI-recommended actions for faculty:
108
+ # - Suggest actionable steps to address the difficulties identified in each critical topic.
109
+ # - Specify the priority of each action (high, medium, low) based on the urgency and impact.
110
+ # - Explain the reasoning behind each recommendation and its expected impact on student outcomes.
111
+
112
+ # 3. Student-specific analytics (focusing on at-risk students):
113
+ # - Identify students categorized as "at-risk" based on their engagement levels, question complexity, and recurring struggles.
114
+ # - For each at-risk student, list their top 3 struggling topics and their engagement metrics (participation level, concept understanding).
115
+ # - Provide personalized recommendations for improving their understanding.
116
+
117
+ # Guidelines for Analysis:
118
+ # - Focus on actionable and concise insights rather than exhaustive details.
119
+ # - Use both explicit (e.g., direct questions) and implicit (e.g., repeated follow-ups) cues to identify areas of difficulty.
120
+ # - Prioritize topics with higher difficulty scores or more students struggling.
121
+ # - Ensure numerical values (e.g., difficulty levels, percentages) are between 0 and 1 where applicable.
122
+
123
+ # The response must be well-structured, concise, and highly actionable for faculty to implement improvements effectively."""
124
+
125
+ # Prompt 3:
126
+ return f"""Analyze the provided student chat histories for a university course and generate concise, actionable analytics.
127
+ Context:
128
+ - Chat histories: {json.dumps(chat_histories, indent=2)}
129
+ - These are pre-class interactions between students and an AI tutor aimed at identifying learning difficulties and improving course delivery.
130
+ - Topics covered: {', '.join(all_topics)}.
131
+
132
+ Your task is to provide detailed analytics that will help faculty address challenges effectively and enhance learning outcomes.
133
+
134
+ Output Format (strictly follow this JSON structure):
135
+ {{
136
+ "topic_wise_insights": [
137
+ {{
138
+ "topic": "<string>",
139
+ "struggling_percentage": <number between 0 and 1>,
140
+ "key_issues": ["<string>", "<string>", ...],
141
+ "key_misconceptions": ["<string>", "<string>", ...],
142
+ "recommended_actions": {{
143
+ "description": "<string>",
144
+ "priority": "high|medium|low",
145
+ "expected_outcome": "<string>"
146
+ }}
147
+ }}
148
+ ],
149
+ "ai_recommended_actions": [
150
+ {{
151
+ "action": "<string>",
152
+ "priority": "high|medium|low",
153
+ "reasoning": "<string>",
154
+ "expected_outcome": "<string>",
155
+ "pedagogy_recommendations": {{
156
+ "methods": ["<string>", "<string>", ...],
157
+ "resources": ["<string>", "<string>", ...],
158
+ "expected_impact": "<string>"
159
+ }}
160
+ }}
161
+ ],
162
+ "student_analytics": [
163
+ {{
164
+ "student_id": "<string>",
165
+ "engagement_metrics": {{
166
+ "participation_level": <number between 0 and 1>,
167
+ "concept_understanding": "strong|moderate|needs_improvement",
168
+ "question_quality": "advanced|intermediate|basic"
169
+ }},
170
+ "struggling_topics": ["<string>", "<string>", ...],
171
+ "personalized_recommendation": "<string>"
172
+ }}
173
+ ]
174
+ }}
175
+
176
+ Guidelines for Analysis:
177
+ - Focus on actionable and concise insights rather than exhaustive details.
178
+ - Use both explicit (e.g., direct questions) and implicit (e.g., repeated follow-ups) cues to identify areas of difficulty.
179
+ - Prioritize topics with higher difficulty scores or more students struggling.
180
+ - Ensure numerical values (e.g., difficulty levels, percentages) are between 0 and 1 where applicable.
181
+ - Make sure to include All** students in the analysis, not just a subset.
182
+ - for the ai_recommended_actions:
183
+ - Prioritize pedagogy recommendations for critical topics with the high difficulty scores or struggling percentages.
184
+ - For each action:
185
+ - Include specific teaching methods (e.g., interactive discussions or quizzes, problem-based learning, practical examples etc).
186
+ - Recommend supporting resources (e.g., videos, handouts, simulations).
187
+ - Provide reasoning for the recommendation and the expected outcomes for student learning.
188
+ - Example:
189
+ - **Action:** Conduct an interactive problem-solving session on "<Topic Name>".
190
+ - **Reasoning:** Students showed difficulty in applying concepts to practical problems.
191
+ - **Expected Outcome:** Improved practical understanding and application of the topic.
192
+ - **Pedagogy Recommendations:**
193
+ - **Methods:** Group discussions, real-world case studies.
194
+ - **Resources:** Online interactive tools, relevant case studies, video walkthroughs.
195
+ - **Expected Impact:** Enhance conceptual clarity by 40% and practical application by 30%.
196
+
197
+ The response must adhere strictly to the above JSON structure, with all fields populated appropriately."""
198
+
199
+
200
+ def _calculate_class_distribution(self, analytics: Dict) -> Dict:
201
+ """Calculate the distribution of students across performance levels."""
202
+ try:
203
+ total_students = len(analytics.get("student_insights", []))
204
+ if total_students == 0:
205
+ return {
206
+ "high_performers": 0,
207
+ "average_performers": 0,
208
+ "at_risk": 0
209
+ }
210
+
211
+ distribution = defaultdict(int)
212
+
213
+ for student in analytics.get("student_insights", []):
214
+ performance_level = student.get("performance_level", "average")
215
+ # Map performance levels to our three categories
216
+ if performance_level in ["excellent", "high", "high_performer"]:
217
+ distribution["high_performers"] += 1
218
+ elif performance_level in ["struggling", "low", "at_risk"]:
219
+ distribution["at_risk"] += 1
220
+ else:
221
+ distribution["average_performers"] += 1
222
+
223
+ # Convert to percentages
224
+ return {
225
+ level: count/total_students
226
+ for level, count in distribution.items()
227
+ }
228
+ except Exception as e:
229
+ print(f"Error calculating class distribution: {str(e)}")
230
+ return {
231
+ "high_performers": 0,
232
+ "average_performers": 0,
233
+ "at_risk": 0
234
+ }
235
+
236
+ def _identify_urgent_cases(self, analytics: Dict) -> List[str]:
237
+ """Identify students needing immediate attention."""
238
+ try:
239
+ urgent_cases = []
240
+ for student in analytics.get("student_insights", []):
241
+ student_id = student.get("student_id")
242
+ if not student_id:
243
+ continue
244
+
245
+ # Check multiple risk factors
246
+ risk_factors = 0
247
+
248
+ # Factor 1: Performance level
249
+ if student.get("performance_level") in ["struggling", "at_risk", "low"]:
250
+ risk_factors += 1
251
+
252
+ # Factor 2: Number of struggling topics
253
+ if len(student.get("struggling_topics", [])) >= 2:
254
+ risk_factors += 1
255
+
256
+ # Factor 3: Engagement metrics
257
+ engagement = student.get("engagement_metrics", {})
258
+ if (engagement.get("participation_level") == "low" or
259
+ engagement.get("concept_understanding") == "needs_improvement"):
260
+ risk_factors += 1
261
+
262
+ # If student has multiple risk factors, add to urgent cases
263
+ if risk_factors >= 2:
264
+ urgent_cases.append(student_id)
265
+
266
+ return urgent_cases
267
+ except Exception as e:
268
+ print(f"Error identifying urgent cases: {str(e)}")
269
+ return []
270
+
271
+ def _identify_monitoring_cases(self, analytics: Dict) -> List[str]:
272
+ """Identify students who need monitoring but aren't urgent cases."""
273
+ try:
274
+ monitoring_cases = []
275
+ urgent_cases = set(self._identify_urgent_cases(analytics))
276
+
277
+ for student in analytics.get("student_insights", []):
278
+ student_id = student.get("student_id")
279
+ if not student_id or student_id in urgent_cases:
280
+ continue
281
+
282
+ # Check monitoring criteria
283
+ monitoring_needed = False
284
+
285
+ # Criterion 1: Has some struggling topics but not enough for urgent
286
+ if len(student.get("struggling_topics", [])) == 1:
287
+ monitoring_needed = True
288
+
289
+ # Criterion 2: Medium-low engagement
290
+ engagement = student.get("engagement_metrics", {})
291
+ if engagement.get("participation_level") == "medium":
292
+ monitoring_needed = True
293
+
294
+ # Criterion 3: Recent performance decline
295
+ if student.get("performance_level") == "average":
296
+ monitoring_needed = True
297
+
298
+ if monitoring_needed:
299
+ monitoring_cases.append(student_id)
300
+
301
+ return monitoring_cases
302
+ except Exception as e:
303
+ print(f"Error identifying monitoring cases: {str(e)}")
304
+ return []
305
+
306
+ def _identify_critical_topics(self, analytics: Dict) -> List[str]:
307
+ """
308
+ Identify critical topics that need attention based on multiple factors.
309
+ Returns a list of topic names that are considered critical.
310
+ """
311
+ try:
312
+ critical_topics = []
313
+ topics = analytics.get("topic_insights", [])
314
+
315
+ for topic in topics:
316
+ if not isinstance(topic, dict):
317
+ continue
318
+
319
+ # Initialize score for topic criticality
320
+ critical_score = 0
321
+
322
+ # Factor 1: High difficulty level
323
+ difficulty_level = topic.get("difficulty_level", 0)
324
+ if difficulty_level > 0.7:
325
+ critical_score += 2
326
+ elif difficulty_level > 0.5:
327
+ critical_score += 1
328
+
329
+ # Factor 2: Number of students struggling
330
+ student_count = topic.get("student_count", 0)
331
+ total_students = len(analytics.get("student_insights", []))
332
+ if total_students > 0:
333
+ struggle_ratio = student_count / total_students
334
+ if struggle_ratio > 0.5:
335
+ critical_score += 2
336
+ elif struggle_ratio > 0.3:
337
+ critical_score += 1
338
+
339
+ # Factor 3: Number of common issues
340
+ if len(topic.get("common_issues", [])) > 2:
341
+ critical_score += 1
342
+
343
+ # Factor 4: Number of key misconceptions
344
+ if len(topic.get("key_misconceptions", [])) > 1:
345
+ critical_score += 1
346
+
347
+ # If topic exceeds threshold, mark as critical
348
+ if critical_score >= 3:
349
+ critical_topics.append(topic.get("topic", "Unknown Topic"))
350
+
351
+ return critical_topics
352
+
353
+ except Exception as e:
354
+ print(f"Error identifying critical topics: {str(e)}")
355
+ return []
356
+
357
+ def _calculate_engagement(self, analytics: Dict) -> Dict:
358
+ """
359
+ Calculate detailed engagement metrics across all students.
360
+ Returns a dictionary with engagement statistics.
361
+ """
362
+ try:
363
+ total_students = len(analytics.get("student_insights", []))
364
+ if total_students == 0:
365
+ return {
366
+ "total_students": 0,
367
+ "overall_score": 0,
368
+ "engagement_distribution": {
369
+ "high": 0,
370
+ "medium": 0,
371
+ "low": 0
372
+ },
373
+ "participation_metrics": {
374
+ "average_topics_per_student": 0,
375
+ "active_participants": 0
376
+ }
377
+ }
378
+
379
+ engagement_levels = defaultdict(int)
380
+ total_topics_engaged = 0
381
+ active_participants = 0
382
+
383
+ for student in analytics.get("student_insights", []):
384
+ # Get engagement metrics
385
+ metrics = student.get("engagement_metrics", {})
386
+
387
+ # Calculate participation level
388
+ participation = metrics.get("participation_level", "low").lower()
389
+ engagement_levels[participation] += 1
390
+
391
+ # Count topics student is engaged with
392
+ topics_count = len(student.get("struggling_topics", []))
393
+ total_topics_engaged += topics_count
394
+
395
+ # Count active participants (students engaging with any topics)
396
+ if topics_count > 0:
397
+ active_participants += 1
398
+
399
+ # Calculate overall engagement score (0-1)
400
+ weighted_score = (
401
+ (engagement_levels["high"] * 1.0 +
402
+ engagement_levels["medium"] * 0.6 +
403
+ engagement_levels["low"] * 0.2) / total_students
404
+ )
405
+
406
+ return {
407
+ "total_students": total_students,
408
+ "overall_score": round(weighted_score, 2),
409
+ "engagement_distribution": {
410
+ level: count/total_students
411
+ for level, count in engagement_levels.items()
412
+ },
413
+ "participation_metrics": {
414
+ "average_topics_per_student": round(total_topics_engaged / total_students, 2),
415
+ "active_participants_ratio": round(active_participants / total_students, 2)
416
+ }
417
+ }
418
+
419
+ except Exception as e:
420
+ print(f"Error calculating engagement: {str(e)}")
421
+ return {
422
+ "total_students": 0,
423
+ "overall_score": 0,
424
+ "engagement_distribution": {
425
+ "high": 0,
426
+ "medium": 0,
427
+ "low": 0
428
+ },
429
+ "participation_metrics": {
430
+ "average_topics_per_student": 0,
431
+ "active_participants_ratio": 0
432
+ }
433
+ }
434
+
435
+ def _process_gemini_response(self, response: str) -> Dict:
436
+ """Process and validate Gemini's response."""
437
+ # try:
438
+ # analytics = json.loads(response)
439
+ # return self._enrich_analytics(analytics)
440
+ # except json.JSONDecodeError as e:
441
+ # print(f"Error decoding Gemini response: {e}")
442
+ # return self._fallback_analytics()
443
+ try:
444
+ # Parse JSON response
445
+ analytics = json.loads(response)
446
+
447
+ # Validate required fields exist
448
+ required_fields = {
449
+ "topic_insights": [],
450
+ "student_insights": [],
451
+ "recommended_actions": []
452
+ }
453
+
454
+ # Ensure all required fields exist with default values
455
+ for field, default_value in required_fields.items():
456
+ if field not in analytics or not analytics[field]:
457
+ analytics[field] = default_value
458
+
459
+ # Now enrich the validated analytics
460
+ return self._enrich_analytics(analytics)
461
+
462
+ except (json.JSONDecodeError, KeyError, TypeError) as e:
463
+ print(f"Error processing Gemini response: {str(e)}")
464
+ print(f"Raw response: {response}")
465
+ return self._fallback_analytics()
466
+
467
+ def _enrich_analytics(self, analytics: Dict) -> Dict:
468
+ """Add derived insights and metrics to the analytics."""
469
+ # Add overall course health metrics
470
+ analytics["course_health"] = {
471
+ "overall_engagement": self._calculate_engagement(analytics),
472
+ "critical_topics": self._identify_critical_topics(analytics),
473
+ "class_distribution": self._calculate_class_distribution(analytics)
474
+ }
475
+
476
+ # Add intervention urgency scores
477
+ analytics["intervention_metrics"] = {
478
+ "immediate_attention_needed": self._identify_urgent_cases(analytics),
479
+ "monitoring_required": self._identify_monitoring_cases(analytics)
480
+ }
481
+
482
+ return analytics
483
+
484
+ def _calculate_engagement(self, analytics: Dict) -> Dict:
485
+ # """Calculate overall engagement metrics."""
486
+ # total_students = len(analytics["student_insights"])
487
+ # engagement_levels = defaultdict(int)
488
+
489
+ # for student in analytics["student_insights"]:
490
+ # engagement_levels[student["engagement_metrics"]["participation_level"]] += 1
491
+
492
+ # return {
493
+ # "total_students": total_students,
494
+ # "engagement_distribution": {
495
+ # level: count/total_students
496
+ # for level, count in engagement_levels.items()
497
+ # }
498
+ # }
499
+ """Calculate overall engagement metrics with defensive programming."""
500
+ try:
501
+ total_students = len(analytics.get("student_insights", []))
502
+ if total_students == 0:
503
+ return {
504
+ "total_students": 0,
505
+ "engagement_distribution": {
506
+ "high": 0,
507
+ "medium": 0,
508
+ "low": 0
509
+ }
510
+ }
511
+
512
+ engagement_levels = defaultdict(int)
513
+
514
+ for student in analytics.get("student_insights", []):
515
+ metrics = student.get("engagement_metrics", {})
516
+ level = metrics.get("participation_level", "low")
517
+ engagement_levels[level] += 1
518
+
519
+ return {
520
+ "total_students": total_students,
521
+ "engagement_distribution": {
522
+ level: count/total_students
523
+ for level, count in engagement_levels.items()
524
+ }
525
+ }
526
+ except Exception as e:
527
+ print(f"Error calculating engagement: {str(e)}")
528
+ return {
529
+ "total_students": 0,
530
+ "engagement_distribution": {
531
+ "high": 0,
532
+ "medium": 0,
533
+ "low": 0
534
+ }
535
+ }
536
+
537
+ def _identify_critical_topics(self, analytics: Dict) -> List[Dict]:
538
+ # """Identify topics needing immediate attention."""
539
+ # return [
540
+ # topic for topic in analytics["topic_insights"]
541
+ # if topic["difficulty_level"] > 0.7 or
542
+ # len(topic["common_issues"]) > 2
543
+ # ]
544
+ """Identify topics needing immediate attention with defensive programming."""
545
+ try:
546
+ return [
547
+ topic for topic in analytics.get("topic_insights", [])
548
+ if topic.get("difficulty_level", 0) > 0.7 or
549
+ len(topic.get("common_issues", [])) > 2
550
+ ]
551
+ except Exception as e:
552
+ print(f"Error identifying critical topics: {str(e)}")
553
+ return []
554
+
555
+ def generate_analytics(self, chat_histories: List[Dict], all_topics: List[str]) -> Dict:
556
+ # Method 1: (caused key 'student_insights' error):
557
+ # """Main method to generate analytics from chat histories."""
558
+ # # Preprocess chat histories
559
+ # processed_histories = self._preprocess_chat_histories(chat_histories)
560
+
561
+ # # Create and send prompt to Gemini
562
+ # prompt = self._create_analytics_prompt(processed_histories, all_topics)
563
+ # response = self.model.generate_content(
564
+ # prompt,
565
+ # generation_config=genai.GenerationConfig(
566
+ # response_mime_type="application/json",
567
+ # response_schema=AnalyticsResponse
568
+ # )
569
+ # )
570
+
571
+ # # # Process and enrich analytics
572
+ # # analytics = self._process_gemini_response(response.text)
573
+ # # return analytics
574
+ # # Process, validate, and enrich the response
575
+ # analytics = self._process_gemini_response(response.text)
576
+
577
+ # # Then cast it to satisfy the type checker
578
+ # return typing.cast(AnalyticsResponse, analytics)
579
+
580
+ # Method 2 (possible fix):
581
+ """Main method to generate analytics with better error handling."""
582
+ try:
583
+ processed_histories = self._preprocess_chat_histories(chat_histories)
584
+ prompt = self._create_analytics_prompt(processed_histories, all_topics)
585
+
586
+ response = self.model.generate_content(
587
+ prompt,
588
+ generation_config=genai.GenerationConfig(
589
+ response_mime_type="application/json"
590
+ # response_schema=AnalyticsResponse
591
+ )
592
+ )
593
+
594
+ if not response.text:
595
+ print("Empty response from Gemini")
596
+ return self._fallback_analytics()
597
+
598
+ # analytics = self._process_gemini_response(response.text)
599
+ # return typing.cast(AnalyticsResponse, analytics)
600
+ # return response.text;
601
+ analytics = json.loads(response.text)
602
+ return analytics
603
+
604
+ except Exception as e:
605
+ print(f"Error generating analytics: {str(e)}")
606
+ return self._fallback_analytics()
607
+
608
+ def _preprocess_chat_histories(self, chat_histories: List[Dict]) -> List[Dict]:
609
+ """Preprocess chat histories to focus on relevant information."""
610
+ processed = []
611
+
612
+ for chat in chat_histories:
613
+ print(str(chat["user_id"]))
614
+ processed_chat = {
615
+ "user_id": str(chat["user_id"]),
616
+ "messages": [
617
+ {
618
+ "prompt": msg["prompt"],
619
+ "response": msg["response"]
620
+ }
621
+ for msg in chat["messages"]
622
+ ]
623
+ }
624
+ processed.append(processed_chat)
625
+
626
+ return processed
627
+
628
+ def _fallback_analytics(self) -> Dict:
629
+ # """Provide basic analytics in case of LLM processing failure."""
630
+ # return {
631
+ # "topic_insights": [],
632
+ # "student_insights": [],
633
+ # "recommended_actions": [
634
+ # {
635
+ # "action": "Review analytics generation process",
636
+ # "priority": "high",
637
+ # "target_group": "system_administrators",
638
+ # "reasoning": "Analytics generation failed",
639
+ # "expected_impact": "Restore analytics functionality"
640
+ # }
641
+ # ]
642
+ # }
643
+ """Provide comprehensive fallback analytics that match our schema."""
644
+ return {
645
+ "topic_insights": [],
646
+ "student_insights": [],
647
+ "recommended_actions": [
648
+ {
649
+ "action": "Review analytics generation process",
650
+ "priority": "high",
651
+ "target_group": "system_administrators",
652
+ "reasoning": "Analytics generation failed",
653
+ "expected_impact": "Restore analytics functionality"
654
+ }
655
+ ],
656
+ "course_health": {
657
+ "overall_engagement": 0,
658
+ "critical_topics": [],
659
+ "class_distribution": {
660
+ "high_performers": 0,
661
+ "average_performers": 0,
662
+ "at_risk": 0
663
+ }
664
+ },
665
+ "intervention_metrics": {
666
+ "immediate_attention_needed": [],
667
+ "monitoring_required": []
668
+ }
669
+ }
670
+
671
+ # if __name__ == "__main__":
672
+ # # Example usage
673
+
674
+
675
+ # analytics_generator = NovaScholarAnalytics()
676
+ # analytics = analytics_generator.generate_analytics(chat_histories, all_topics)
677
+ # print(json.dumps(analytics, indent=2))
session_page.py CHANGED
@@ -16,11 +16,12 @@ import os
16
  from pymongo import MongoClient
17
  from gen_mcqs import generate_mcqs, save_quiz, quizzes_collection, get_student_quiz_score, submit_quiz_answers
18
  from create_course import courses_collection
19
- from pre_class_analytics import NovaScholarAnalytics
 
20
  import openai
21
  from openai import OpenAI
22
 
23
-
24
  from goals2 import GoalAnalyzer
25
  from openai import OpenAI
26
  import asyncio
@@ -242,19 +243,37 @@ def display_preclass_content(session, student_id, course_id):
242
 
243
  # Please provide a clear and concise answer based only on the information provided in the context.
244
  # """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  context_prompt = f"""
246
- You are a highly intelligent and resourceful assistant capable of synthesizing information from the provided context.
247
 
248
  Context:
249
  {context}
250
 
251
  Instructions:
252
- 1. Base your answers primarily on the given context.
253
- 2. If the answer to the user's question is not explicitly in the context but can be inferred or synthesized from the information provided, do so thoughtfully.
254
- 3. Only use external knowledge or web assistance when:
255
- - The context lacks sufficient information, and
256
- - The question requires knowledge beyond what can be reasonably inferred from the context.
257
- 4. Clearly state if you are relying on web assistance for any part of your answer.
258
 
259
  Question: {prompt}
260
 
@@ -1147,6 +1166,88 @@ def get_response_from_llm(raw_data):
1147
  st.error(f"Error generating response: {str(e)}")
1148
  return None
1149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1150
  def get_preclass_analytics(session):
1151
  """Get all user_ids from chat_history collection where session_id matches"""
1152
  user_ids = chat_history_collection.distinct("user_id", {"session_id": session['session_id']})
@@ -1168,120 +1269,380 @@ def get_preclass_analytics(session):
1168
  else:
1169
  st.warning("No chat history found for this session.")
1170
 
1171
- # Use the analytics engine
1172
- analytics_engine = NovaScholarAnalytics()
1173
- results = analytics_engine.process_chat_history(all_chat_histories)
1174
- faculty_report = analytics_engine.generate_faculty_report(results)
1175
-
1176
- # Pass this Faculty Report to an LLM model for refinements and clarity
1177
- refined_report = get_response_from_llm(faculty_report)
1178
- return refined_report
1179
 
1180
- def display_preclass_analytics2(session, course_id):
1181
- refined_report = get_preclass_analytics(session)
1182
- st.subheader("Pre-class Analytics")
1183
- if refined_report:
1184
- # Custom CSS to improve the look and feel
1185
- st.markdown("""
1186
- <style>
1187
- .metric-card {
1188
- background-color: #f8f9fa;
1189
- border-radius: 10px;
1190
- padding: 20px;
1191
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1192
- }
1193
- .header-text {
1194
- color: #1f77b4;
1195
- font-size: 24px;
1196
- font-weight: bold;
1197
- margin-bottom: 20px;
1198
- }
1199
- .subheader {
1200
- color: #2c3e50;
1201
- font-size: 17px;
1202
- font-weight: 500;
1203
- margin-bottom: 10px;
1204
- }
1205
- .insight-text {
1206
- color: #34495e;
1207
- font-size: 16px;
1208
- line-height: 1.6;
1209
- }
1210
- .glossary-card {
1211
- padding: 15px;
1212
- margin-top: 40px;
1213
- }
1214
- </style>
1215
- """, unsafe_allow_html=True)
1216
 
1217
- # Header
1218
- # st.markdown("<h1 style='text-align: center; color: #2c3e50;'>Pre-Class Analytics Dashboard</h1>", unsafe_allow_html=True)
1219
- # st.markdown("<p style='text-align: center; color: #7f8c8d;'>Insights from Student Interactions</p>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1220
 
1221
- # Create three columns for metrics
1222
- col1, col2, col3 = st.columns(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1223
 
1224
- with col1:
1225
- st.markdown("<p class='header-text'>🎯 Low Engagement Topics</p>", unsafe_allow_html=True)
1226
-
1227
- # Group topics by category
1228
- topics = refined_report["Low Engagement Topics"]
1229
- # categories = defaultdict(list)
1230
- for i, topic in enumerate(topics):
1231
- st.markdown(f"{i + 1}. <p class='subheader'>{topic}</p>", unsafe_allow_html=True)
1232
-
1233
- # # Categorize topics (you can modify these categories based on your needs)
1234
- # for topic in topics:
1235
- # if "Data" in topic and ("Type" in topic or "Structure" in topic):
1236
- # categories["Data Types"].append(topic)
1237
- # elif "Analytics" in topic:
1238
- # categories["Analytics Concepts"].append(topic)
1239
- # else:
1240
- # categories["General Concepts"].append(topic)
1241
-
1242
- # Display categorized topics
1243
- # for category, items in categories.items():
1244
- # st.markdown(f"<p class='subheader'>{category}</p>", unsafe_allow_html=True)
1245
- # i = 0
1246
- # for i, item in items:
1247
- # st.markdown(f"{i + 1} {item}", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1248
 
1249
- with col2:
1250
- st.markdown("<p class='header-text'>⚠️ Frustration Areas</p>", unsafe_allow_html=True)
1251
- for i, area in enumerate(refined_report["Frustration Areas"]):
1252
- st.markdown(f"{i + 1}. <p class='subheader'>{area}</p>", unsafe_allow_html=True)
 
 
 
 
 
 
1253
 
1254
- with col3:
1255
- st.markdown("<p class='header-text'>💡 Recommendations</p>", unsafe_allow_html=True)
1256
- for i, rec in enumerate(refined_report["Recommendations"]):
1257
- st.markdown(f"{i + 1}. <p class='subheader'>{rec}</p>", unsafe_allow_html=True)
1258
 
1259
- # Glossary section
1260
- st.markdown("<div class='glossary-card'>", unsafe_allow_html=True)
1261
- # st.markdown("<h3 style='color: #2c3e50;'>Understanding the Metrics</h3>", unsafe_allow_html=True)
 
1262
 
1263
- explanations = {
1264
- "Low Engagement Topics": "Topics where students showed minimal interaction or understanding during their chat sessions. These areas may require additional focus during classroom instruction.",
1265
- "Frustration Areas": "Specific concepts or topics where students expressed difficulty or confusion during their interactions with the chatbot. These areas may need immediate attention or alternative teaching approaches.",
1266
- "Recommendations": "AI-generated suggestions for improving student engagement and understanding, based on the analyzed chat interactions and identified patterns."
1267
- }
 
 
 
 
 
1268
 
1269
- st.subheader("Understanding the Metrics")
1270
-
1271
- for metric, explanation in explanations.items():
1272
- # st.markdown(f"<p class='subheader'>{metric}</p>", unsafe_allow_html=True)
1273
- # st.markdown(f"<p class='insight-text'>{explanation}</p>", unsafe_allow_html=True)
1274
- st.markdown(f"<span class='subheader'>**{metric}**</span>: <span class='subheader'>{explanation}</span>", unsafe_allow_html=True)
1275
 
1276
- st.markdown("</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1277
 
 
 
 
 
 
 
 
 
 
 
 
1278
 
1279
  def display_session_analytics(session, course_id):
1280
  """Display session analytics for faculty"""
1281
  st.header("Session Analytics")
1282
 
1283
  # Display Pre-class Analytics
1284
- display_preclass_analytics2(session, course_id)
1285
 
1286
  # Display In-class Analytics
1287
  display_inclass_analytics(session, course_id)
 
16
  from pymongo import MongoClient
17
  from gen_mcqs import generate_mcqs, save_quiz, quizzes_collection, get_student_quiz_score, submit_quiz_answers
18
  from create_course import courses_collection
19
+ # from pre_class_analytics import NovaScholarAnalytics
20
+ from pre_class_analytics2 import NovaScholarAnalytics
21
  import openai
22
  from openai import OpenAI
23
 
24
+ import google.generativeai as genai
25
  from goals2 import GoalAnalyzer
26
  from openai import OpenAI
27
  import asyncio
 
243
 
244
  # Please provide a clear and concise answer based only on the information provided in the context.
245
  # """
246
+ # context_prompt = f"""
247
+ # You are a highly intelligent and resourceful assistant capable of synthesizing information from the provided context.
248
+
249
+ # Context:
250
+ # {context}
251
+
252
+ # Instructions:
253
+ # 1. Base your answers primarily on the given context.
254
+ # 2. If the answer to the user's question is not explicitly in the context but can be inferred or synthesized from the information provided, do so thoughtfully.
255
+ # 3. Only use external knowledge or web assistance when:
256
+ # - The context lacks sufficient information, and
257
+ # - The question requires knowledge beyond what can be reasonably inferred from the context.
258
+ # 4. Clearly state if you are relying on web assistance for any part of your answer.
259
+ # 5. Do not respond with a negative. If the answer is not in the context, provide a thoughtful response based on the information available on the web about it.
260
+
261
+ # Question: {prompt}
262
+
263
+ # Please provide a clear and comprehensive answer based on the above instructions.
264
+ # """
265
  context_prompt = f"""
266
+ You are a highly intelligent and resourceful assistant capable of synthesizing information from the provided context and external sources.
267
 
268
  Context:
269
  {context}
270
 
271
  Instructions:
272
+ 1. Base your answers on the provided context wherever possible.
273
+ 2. If the answer to the user's question is not explicitly in the context:
274
+ - Use external knowledge or web assistance to provide a clear and accurate response.
275
+ 3. Do not respond negatively. If the answer is not in the context, use web assistance or your knowledge to generate a thoughtful response.
276
+ 4. Clearly state if part of your response relies on web assistance.
 
277
 
278
  Question: {prompt}
279
 
 
1166
  st.error(f"Error generating response: {str(e)}")
1167
  return None
1168
 
1169
+ import typing_extensions as typing
1170
+ from typing import Union, List, Dict
1171
+
1172
+ # class Topics(typing.TypedDict):
1173
+ # overarching_theme: List[Dict[str, Union[str, List[Dict[str, Union[str, List[str]]]]]]]
1174
+ # indirect_topics: List[Dict[str, str]]
1175
+
1176
+ def extract_topics_from_materials(session):
1177
+ """Extract topics from pre-class materials"""
1178
+ materials = resources_collection.find({"session_id": session['session_id']})
1179
+ texts = ""
1180
+ if materials:
1181
+ for material in materials:
1182
+ if 'text_content' in material:
1183
+ text = material['text_content']
1184
+ texts += text + "\n"
1185
+ else:
1186
+ st.warning("No text content found in the material.")
1187
+ return
1188
+ else:
1189
+ st.error("No pre-class materials found for this session.")
1190
+ return
1191
+
1192
+ if texts:
1193
+ context_prompt = f"""
1194
+ Task: Extract Comprehensive Topics in a List Format
1195
+ You are tasked with analyzing the provided text content and extracting a detailed, flat list of topics.
1196
+
1197
+ Instructions:
1198
+ Identify All Topics: Extract a comprehensive list of all topics, subtopics, and indirect topics present in the provided text content. This list should include:
1199
+
1200
+ Overarching themes
1201
+ Main topics
1202
+ Subtopics and their sub-subtopics
1203
+ Indirectly related topics
1204
+ Flat List Format: Provide a flat list where each item is a topic. Ensure topics at all levels (overarching, main, sub, sub-sub, indirect) are represented as individual entries in the list.
1205
+
1206
+ Be Exhaustive: Ensure the response captures every topic, subtopic, and indirectly related concept comprehensively.
1207
+
1208
+ Output Requirements:
1209
+ Use this structure:
1210
+ {{
1211
+ "topics": [
1212
+ "Topic 1",
1213
+ "Topic 2",
1214
+ "Topic 3",
1215
+ ...
1216
+ ]
1217
+ }}
1218
+ Do Not Include: Do not include backticks, hierarchical structures, or the word 'json' in your response.
1219
+
1220
+ Content to Analyze:
1221
+ {texts}
1222
+ """
1223
+ try:
1224
+ # response = model.generate_content(context_prompt, generation_config=genai.GenerationConfig(response_mime_type="application/json", response_schema=list[Topics]))
1225
+ response = model.generate_content(context_prompt)
1226
+ if not response or not response.text:
1227
+ st.error("Error extracting topics from materials.")
1228
+ return
1229
+
1230
+ topics = response.text
1231
+ return topics
1232
+ except Exception as e:
1233
+ st.error(f"Error extracting topics: {str(e)}")
1234
+ return None
1235
+ else:
1236
+ st.error("No text content found in the pre-class materials.")
1237
+ return None
1238
+
1239
+ def convert_json_to_dict(json_str):
1240
+ try:
1241
+ return json.loads(json_str)
1242
+ except Exception as e:
1243
+ st.error(f"Error converting JSON to dictionary. {str(e)}")
1244
+ return None
1245
+
1246
+ # Load topics from a JSON file
1247
+ topics = []
1248
+ with open(r'topics.json', 'r') as file:
1249
+ topics = json.load(file)
1250
+
1251
  def get_preclass_analytics(session):
1252
  """Get all user_ids from chat_history collection where session_id matches"""
1253
  user_ids = chat_history_collection.distinct("user_id", {"session_id": session['session_id']})
 
1269
  else:
1270
  st.warning("No chat history found for this session.")
1271
 
 
 
 
 
 
 
 
 
1272
 
1273
+ # Pass the pre-class materials content to the analytics engine
1274
+ # topics = extract_topics_from_materials(session)
1275
+ # dict_topics = convert_json_to_dict(topics)
1276
+ print(topics)
1277
+
1278
+ # # Use the 1st analytics engine
1279
+ # analytics_engine = NovaScholarAnalytics(all_topics_list=topics)
1280
+ # # extracted_topics = analytics_engine._extract_topics(None, topics)
1281
+ # # print(extracted_topics)
1282
+
1283
+ # results = analytics_engine.process_chat_history(all_chat_histories)
1284
+ # faculty_report = analytics_engine.generate_faculty_report(results)
1285
+ # print(faculty_report)
1286
+ # # Pass this Faculty Report to an LLM model for refinements and clarity
1287
+ # refined_report = get_response_from_llm(faculty_report)
1288
+ # return refined_report
1289
+
1290
+ # Use the 2nd analytice engine (using LLM):
1291
+ analytics_generator = NovaScholarAnalytics()
1292
+ analytics2 = analytics_generator.generate_analytics(all_chat_histories, topics)
1293
+ # enriched_analytics = analytics_generator._enrich_analytics(analytics2)
1294
+ print("Analytics is: ", analytics2)
1295
+ return analytics2
1296
+ # print(json.dumps(analytics, indent=2))
1297
+
1298
+
1299
+ # Load Analytics from a JSON file
1300
+ # analytics = []
1301
+ # with open(r'new_analytics2.json', 'r') as file:
1302
+ # analytics = json.load(file)
 
 
 
 
 
 
1303
 
1304
+ def display_preclass_analytics(session, course_id):
1305
+ # Initialize or get analytics data from session state
1306
+ if 'analytics_data' not in st.session_state:
1307
+ st.session_state.analytics_data = get_preclass_analytics(session)
1308
+
1309
+ analytics = st.session_state.analytics_data
1310
+
1311
+ # Enhanced CSS for better styling and interactivity
1312
+ st.markdown("""
1313
+ <style>
1314
+ /* General styles */
1315
+ .section-title {
1316
+ color: #1a237e;
1317
+ font-size: 1.5rem;
1318
+ font-weight: 600;
1319
+ margin-top: 1rem 0 1rem 0;
1320
+ }
1321
 
1322
+ /* Topic list styles */
1323
+ .topic-list {
1324
+ max-width: 800px;
1325
+ margin: 0 auto;
1326
+ }
1327
+ .topic-header {
1328
+ background-color: #ffffff;
1329
+ border: 1px solid #e0e0e0;
1330
+ border-radius: 8px;
1331
+ padding: 1rem 1.25rem;
1332
+ margin: 0.5rem 0;
1333
+ cursor: pointer;
1334
+ display: flex;
1335
+ align-items: center;
1336
+ justify-content: space-between;
1337
+ transition: all 0.2s ease;
1338
+ }
1339
+ .topic-header:hover {
1340
+ background-color: #f8fafc;
1341
+ transform: translateX(5px);
1342
+ }
1343
+ .topic-header h3 {
1344
+ color: #1e3a8a;
1345
+ font-size: 1.1rem;
1346
+ font-weight: 500;
1347
+ margin: 0;
1348
+ }
1349
+ .topic-struggling-rate {
1350
+ background-color: #dbeafe;
1351
+ padding: 0.25rem 0.75rem;
1352
+ border-radius: 16px;
1353
+ font-size: 0.85rem;
1354
+ color: #1e40af;
1355
+ }
1356
+ .topic-content {
1357
+ background-color: #ffffff;
1358
+ border: 1px solid #e0e0e0;
1359
+ border-top: none;
1360
+ border-radius: 0 0 8px 8px;
1361
+ padding: 1.25rem;
1362
+ margin-top: -0.5rem;
1363
+ margin-bottom: 1rem;
1364
+ }
1365
+ .topic-content .section-heading {
1366
+ color: #2c5282;
1367
+ font-size: 1rem;
1368
+ font-weight: 600;
1369
+ margin: 1rem 0 0.5rem 0;
1370
+ }
1371
+ .topic-content ul {
1372
+ margin: 0;
1373
+ padding-left: 1.25rem;
1374
+ font-size: 0.85rem;
1375
+ color: #4a5568;
1376
+ }
1377
 
1378
+ /* Recommendation card styles */
1379
+ .recommendation-grid {
1380
+ display: grid;
1381
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1382
+ gap: 1rem;
1383
+ margin: 1rem 0;
1384
+ }
1385
+ .recommendation-card {
1386
+ background-color: #f8fafc;
1387
+ border-radius: 8px;
1388
+ padding: 1.25rem;
1389
+ border-left: 4px solid #3b82f6;
1390
+ margin-bottom: 1rem;
1391
+ }
1392
+ .recommendation-card h4 {
1393
+ color: #1e40af;
1394
+ font-size: 1rem;
1395
+ font-weight: 600;
1396
+ margin-bottom: 0;
1397
+ display: flex;
1398
+ align-items: center;
1399
+ gap: 0.5rem;
1400
+ }
1401
+ .recommendation-card .priority-badge {
1402
+ font-size: 0.75rem;
1403
+ padding: 0.25rem 0.5rem;
1404
+ border-radius: 4px;
1405
+ background-color: #dbeafe;
1406
+ color: #1e40af;
1407
+ text-transform: uppercase;
1408
+ }
1409
+
1410
+ /* Student analytics styles */
1411
+ .student-filters {
1412
+ background-color: #f8fafc;
1413
+ padding: 1rem;
1414
+ border-radius: 8px;
1415
+ margin-bottom: 1rem;
1416
+ }
1417
+ .analytics-grid {
1418
+ display: grid;
1419
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1420
+ gap: 1rem;
1421
+ margin-top: 1rem;
1422
+ }
1423
+ .student-metrics-card {
1424
+ background-color: #ffffff;
1425
+ border-radius: 8px;
1426
+ padding: 1rem;
1427
+ border: 1px solid #e5e7eb;
1428
+ margin-bottom: 1rem;
1429
+ }
1430
+ .student-metrics-card .header {
1431
+ display: flex;
1432
+ justify-content: space-between;
1433
+ align-items: center;
1434
+ margin-bottom: 0.75rem;
1435
+ }
1436
+ .student-metrics-card .student-id {
1437
+ color: #1e40af;
1438
+ font-size: 1rem;
1439
+ font-weight: 600;
1440
+ }
1441
+ .student-metrics-card .metrics-grid {
1442
+ display: grid;
1443
+ grid-template-columns: repeat(2, 1fr);
1444
+ gap: 0.75rem;
1445
+ }
1446
+ .metric-box {
1447
+ background-color: #f8fafc;
1448
+ padding: 0.75rem;
1449
+ border-radius: 6px;
1450
+ }
1451
+ .metric-box .label {
1452
+ font-size: 0.9rem;
1453
+ color: #6b7280;
1454
+ margin-bottom: 0.25rem;
1455
+ font-weight: 500;
1456
+ }
1457
+ .metric-box .value {
1458
+ font-size: 0.9rem;
1459
+ color: #1f2937;
1460
+ font-weight: 600;
1461
+ }
1462
+ .struggling-topics {
1463
+ grid-column: span 2;
1464
+ margin-top: 0.5rem;
1465
+ }
1466
+ .struggling-topics .label{
1467
+ font-size: 0.9rem;
1468
+ font-weight: 600;
1469
+ }
1470
+ .struggling-topics .value{
1471
+ font-size: 0.9rem;
1472
+ font-weight: 500;
1473
+ }
1474
+ .recommendation-text {
1475
+ grid-column: span 2;
1476
+ font-size: 0.95rem;
1477
+ color: #4b5563;
1478
+ margin-top: 0.75rem;
1479
+ padding-top: 0.75rem;
1480
+ border-top: 1px solid #e5e7eb;
1481
+ }
1482
+ .reason{
1483
+ font-size: 1rem;
1484
+ font-weight: 600;
1485
+ }
1486
+ </style>
1487
+ """, unsafe_allow_html=True)
1488
 
1489
+ # Topic-wise Analytics Section
1490
+ st.markdown('<h2 class="section-title">Topic-wise Analytics</h2>', unsafe_allow_html=True)
1491
+
1492
+ # Initialize session state for topic expansion
1493
+ if 'expanded_topic' not in st.session_state:
1494
+ st.session_state.expanded_topic = None
1495
+
1496
+ # Store topic indices in session state if not already done
1497
+ if 'topic_indices' not in st.session_state:
1498
+ st.session_state.topic_indices = list(range(len(analytics["topic_wise_insights"])))
1499
 
 
 
 
 
1500
 
1501
+ st.markdown('<div class="topic-list">', unsafe_allow_html=True)
1502
+ for idx in st.session_state.topic_indices:
1503
+ topic = analytics["topic_wise_insights"][idx]
1504
+ topic_id = f"topic_{idx}"
1505
 
1506
+ # Create clickable header
1507
+ col1, col2 = st.columns([3, 1])
1508
+ with col1:
1509
+ if st.button(
1510
+ topic["topic"],
1511
+ key=f"topic_button_{idx}",
1512
+ use_container_width=True,
1513
+ type="secondary"
1514
+ ):
1515
+ st.session_state.expanded_topic = topic_id if st.session_state.expanded_topic != topic_id else None
1516
 
1517
+ with col2:
1518
+ st.markdown(f"""
1519
+ <div style="text-align: right;">
1520
+ <span class="topic-struggling-rate">{topic["struggling_percentage"]*100:.1f}% Struggling</span>
1521
+ </div>
1522
+ """, unsafe_allow_html=True)
1523
 
1524
+ # Show content if topic is expanded
1525
+ if st.session_state.expanded_topic == topic_id:
1526
+ st.markdown(f"""
1527
+ <div class="topic-content">
1528
+ <div class="section-heading">Key Issues</div>
1529
+ <ul>
1530
+ {"".join([f"<li>{issue}</li>" for issue in topic["key_issues"]])}
1531
+ </ul>
1532
+ <div class="section-heading">Key Misconceptions</div>
1533
+ <ul>
1534
+ {"".join([f"<li>{misc}</li>" for misc in topic["key_misconceptions"]])}
1535
+ </ul>
1536
+ </div>
1537
+ """, unsafe_allow_html=True)
1538
+ st.markdown('</div>', unsafe_allow_html=True)
1539
+
1540
+ # AI Recommendations Section
1541
+ st.markdown('<h2 class="section-title">AI-Powered Recommendations</h2>', unsafe_allow_html=True)
1542
+ st.markdown('<div class="recommendation-grid">', unsafe_allow_html=True)
1543
+ for idx, rec in enumerate(analytics["ai_recommended_actions"]):
1544
+ st.markdown(f"""
1545
+ <div class="recommendation-card">
1546
+ <h4>
1547
+ <span>Recommendation {idx + 1}</span>
1548
+ <span class="priority-badge">{rec["priority"]}</span>
1549
+ </h4>
1550
+ <p>{rec["action"]}</p>
1551
+ <p><span class="reason">Reason:</span> {rec["reasoning"]}</p>
1552
+ <p><span class="reason">Expected Outcome:</span> {rec["expected_outcome"]}</p>
1553
+ </div>
1554
+ """, unsafe_allow_html=True)
1555
+ st.markdown('</div>', unsafe_allow_html=True)
1556
+
1557
+ # Student Analytics Section
1558
+ st.markdown('<h2 class="section-title">Student Analytics</h2>', unsafe_allow_html=True)
1559
+
1560
+ # Filters
1561
+ with st.container():
1562
+ # st.markdown('<div class="student-filters">', unsafe_allow_html=True)
1563
+ col1, col2, col3 = st.columns(3)
1564
+ with col1:
1565
+ concept_understanding = st.selectbox(
1566
+ "Filter by Understanding",
1567
+ ["All", "Strong", "Moderate", "Needs Improvement"]
1568
+ )
1569
+ with col2:
1570
+ participation_level = st.selectbox(
1571
+ "Filter by Participation",
1572
+ ["All", "High (>80%)", "Medium (50-80%)", "Low (<50%)"]
1573
+ )
1574
+ with col3:
1575
+ struggling_topic = st.selectbox(
1576
+ "Filter by Struggling Topic",
1577
+ ["All"] + list(set([topic for student in analytics["student_analytics"]
1578
+ for topic in student["struggling_topics"]]))
1579
+ )
1580
+ # st.markdown('</div>', unsafe_allow_html=True)
1581
+
1582
+ # Display student metrics in a grid
1583
+ st.markdown('<div class="analytics-grid">', unsafe_allow_html=True)
1584
+ for student in analytics["student_analytics"]:
1585
+ # Apply filters
1586
+ if (concept_understanding != "All" and
1587
+ student["engagement_metrics"]["concept_understanding"].replace("_", " ").title() != concept_understanding):
1588
+ continue
1589
+
1590
+ participation = student["engagement_metrics"]["participation_level"] * 100
1591
+ if participation_level != "All":
1592
+ if participation_level == "High (>80%)" and participation <= 80:
1593
+ continue
1594
+ elif participation_level == "Medium (50-80%)" and (participation < 50 or participation > 80):
1595
+ continue
1596
+ elif participation_level == "Low (<50%)" and participation >= 50:
1597
+ continue
1598
+
1599
+ if struggling_topic != "All" and struggling_topic not in student["struggling_topics"]:
1600
+ continue
1601
+
1602
+ st.markdown(f"""
1603
+ <div class="student-metrics-card">
1604
+ <div class="header">
1605
+ <span class="student-id">Student {student["student_id"][-6:]}</span>
1606
+ </div>
1607
+ <div class="metrics-grid">
1608
+ <div class="metric-box">
1609
+ <div class="label">Participation</div>
1610
+ <div class="value">{student["engagement_metrics"]["participation_level"]*100:.1f}%</div>
1611
+ </div>
1612
+ <div class="metric-box">
1613
+ <div class="label">Understanding</div>
1614
+ <div class="value">{student["engagement_metrics"]["concept_understanding"].replace('_', ' ').title()}</div>
1615
+ </div>
1616
+ <div class="struggling-topics">
1617
+ <div class="label">Struggling Topics: </div>
1618
+ <div class="value">{", ".join(student["struggling_topics"]) if student["struggling_topics"] else "None"}</div>
1619
+ </div>
1620
+ <div class="recommendation-text">
1621
+ {student["personalized_recommendation"]}
1622
+ </div>
1623
+ </div>
1624
+ </div>
1625
+ """, unsafe_allow_html=True)
1626
+ st.markdown('</div>', unsafe_allow_html=True)
1627
 
1628
+ def reset_analytics_state():
1629
+ """
1630
+ Helper function to reset the analytics state when needed
1631
+ (e.g., when loading a new session or when data needs to be refreshed)
1632
+ """
1633
+ if 'analytics_data' in st.session_state:
1634
+ del st.session_state.analytics_data
1635
+ if 'expanded_topic' in st.session_state:
1636
+ del st.session_state.expanded_topic
1637
+ if 'topic_indices' in st.session_state:
1638
+ del st.session_state.topic_indice
1639
 
1640
  def display_session_analytics(session, course_id):
1641
  """Display session analytics for faculty"""
1642
  st.header("Session Analytics")
1643
 
1644
  # Display Pre-class Analytics
1645
+ display_preclass_analytics(session, course_id)
1646
 
1647
  # Display In-class Analytics
1648
  display_inclass_analytics(session, course_id)