Spaces:
Build error
Build error
Harshal Vhatkar
commited on
Commit
·
b8a1cb6
1
Parent(s):
f39b63e
update pre-class-analytics
Browse files- .gitignore +5 -1
- app.py +5 -4
- pre_class_analytics.py +0 -850
- pre_class_analytics2.py +677 -0
- 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 |
-
|
139 |
"Please select your Role",
|
140 |
-
["
|
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
|
253 |
-
2. If the answer to the user's question is not explicitly in the context
|
254 |
-
|
255 |
-
|
256 |
-
|
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 |
-
|
1181 |
-
|
1182 |
-
|
1183 |
-
|
1184 |
-
|
1185 |
-
|
1186 |
-
|
1187 |
-
|
1188 |
-
|
1189 |
-
|
1190 |
-
|
1191 |
-
|
1192 |
-
|
1193 |
-
|
1194 |
-
|
1195 |
-
|
1196 |
-
|
1197 |
-
|
1198 |
-
|
1199 |
-
|
1200 |
-
|
1201 |
-
|
1202 |
-
|
1203 |
-
|
1204 |
-
|
1205 |
-
|
1206 |
-
|
1207 |
-
|
1208 |
-
|
1209 |
-
|
1210 |
-
.glossary-card {
|
1211 |
-
padding: 15px;
|
1212 |
-
margin-top: 40px;
|
1213 |
-
}
|
1214 |
-
</style>
|
1215 |
-
""", unsafe_allow_html=True)
|
1216 |
|
1217 |
-
|
1218 |
-
|
1219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1220 |
|
1221 |
-
|
1222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1223 |
|
1224 |
-
|
1225 |
-
|
1226 |
-
|
1227 |
-
|
1228 |
-
|
1229 |
-
|
1230 |
-
|
1231 |
-
|
1232 |
-
|
1233 |
-
|
1234 |
-
|
1235 |
-
|
1236 |
-
|
1237 |
-
|
1238 |
-
|
1239 |
-
#
|
1240 |
-
|
1241 |
-
|
1242 |
-
|
1243 |
-
|
1244 |
-
|
1245 |
-
|
1246 |
-
|
1247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1248 |
|
1249 |
-
|
1250 |
-
|
1251 |
-
|
1252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
1260 |
-
|
1261 |
-
|
|
|
1262 |
|
1263 |
-
|
1264 |
-
|
1265 |
-
|
1266 |
-
|
1267 |
-
|
|
|
|
|
|
|
|
|
|
|
1268 |
|
1269 |
-
|
1270 |
-
|
1271 |
-
|
1272 |
-
|
1273 |
-
|
1274 |
-
|
1275 |
|
1276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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)
|