Spaces:
Sleeping
Sleeping
Commit ·
f4552a1
1
Parent(s): 8098153
Changes before Firebase Studio auto-run
Browse files- .idx/dev.nix +7 -2
- 25.2 +0 -0
- [25 +0 -0
- __pycache__/dashboard_analyzer.cpython-311.pyc +0 -0
- __pycache__/job_scraper.cpython-311.pyc +0 -0
- __pycache__/prompts.cpython-311.pyc +0 -0
- __pycache__/rag_system.cpython-311.pyc +0 -0
- __pycache__/resume_parser.cpython-311.pyc +0 -0
- __pycache__/youtube_search_tool.cpython-311.pyc +0 -0
- agg.py +106 -23
- app copy.py +0 -58
- app_copy.py +203 -0
- dashboard_analyzer.py +222 -0
- final_cleaned_student_data.json +44 -0
- job_scraper.py +203 -0
- prompts.py +39 -10
- rag_system.py +337 -33
- requirements.txt +99 -10
- resume.pdf +3 -0
- resume_parser.py +53 -0
- static/final.css +449 -0
- static/final.js +817 -0
- templates/final.html +173 -386
- templates/script.js +463 -0
- templates/style.css +378 -0
- youtube_search_tool.py +478 -0
.idx/dev.nix
CHANGED
|
@@ -22,11 +22,16 @@
|
|
| 22 |
enable = true;
|
| 23 |
previews = {
|
| 24 |
web = {
|
| 25 |
-
command = [ "./
|
| 26 |
-
env = {
|
|
|
|
|
|
|
|
|
|
| 27 |
manager = "web";
|
| 28 |
};
|
| 29 |
};
|
| 30 |
};
|
| 31 |
};
|
|
|
|
| 32 |
}
|
|
|
|
|
|
| 22 |
enable = true;
|
| 23 |
previews = {
|
| 24 |
web = {
|
| 25 |
+
command = [ "sh" "-c" "source .venv/bin/activate && python -u 'app copy.py'" ];
|
| 26 |
+
env = {
|
| 27 |
+
PORT = "$PORT";
|
| 28 |
+
GOOGLE_API_KEY = "AIzaSyCc8ggBCJY0XimSrE1NU8z9Ax4qAPMXO_w";
|
| 29 |
+
};
|
| 30 |
manager = "web";
|
| 31 |
};
|
| 32 |
};
|
| 33 |
};
|
| 34 |
};
|
| 35 |
+
|
| 36 |
}
|
| 37 |
+
|
25.2
DELETED
|
File without changes
|
[25
DELETED
|
File without changes
|
__pycache__/dashboard_analyzer.cpython-311.pyc
ADDED
|
Binary file (17.9 kB). View file
|
|
|
__pycache__/job_scraper.cpython-311.pyc
ADDED
|
Binary file (14.2 kB). View file
|
|
|
__pycache__/prompts.cpython-311.pyc
CHANGED
|
Binary files a/__pycache__/prompts.cpython-311.pyc and b/__pycache__/prompts.cpython-311.pyc differ
|
|
|
__pycache__/rag_system.cpython-311.pyc
CHANGED
|
Binary files a/__pycache__/rag_system.cpython-311.pyc and b/__pycache__/rag_system.cpython-311.pyc differ
|
|
|
__pycache__/resume_parser.cpython-311.pyc
ADDED
|
Binary file (2.84 kB). View file
|
|
|
__pycache__/youtube_search_tool.cpython-311.pyc
ADDED
|
Binary file (20 kB). View file
|
|
|
agg.py
CHANGED
|
@@ -1,25 +1,36 @@
|
|
| 1 |
-
# data_aggregator.py (
|
| 2 |
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
import time
|
| 7 |
from datetime import datetime
|
|
|
|
| 8 |
|
| 9 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
from github_scraper import get_github_profile
|
| 11 |
from codeforces_scraper import get_codeforces_profile
|
| 12 |
from leetcode_scraper import get_leetcode_profile
|
| 13 |
from ipu_scraper import StudentScraper
|
|
|
|
|
|
|
| 14 |
|
| 15 |
# --- Configuration ---
|
| 16 |
-
# Define the list of students
|
| 17 |
STUDENTS_TO_FETCH = [
|
| 18 |
{
|
| 19 |
"enrollment_no": "35214811922",
|
| 20 |
"leetcode_user": "akshitsharma7093",
|
| 21 |
"github_user": "akshit7093",
|
| 22 |
-
"codeforces_user": "akshit7093"
|
|
|
|
| 23 |
}
|
| 24 |
# Add more student dictionaries here
|
| 25 |
]
|
|
@@ -31,6 +42,7 @@ OUTPUT_FILE = 'final_cleaned_student_data.json'
|
|
| 31 |
def clean_ipu_data(raw_data):
|
| 32 |
"""Transforms raw IPU academic data into a final, clean format."""
|
| 33 |
if not raw_data or raw_data.get("status") != "success":
|
|
|
|
| 34 |
return None
|
| 35 |
|
| 36 |
overall = raw_data["academic_summary"]["overall_performance"]
|
|
@@ -67,6 +79,7 @@ def clean_ipu_data(raw_data):
|
|
| 67 |
def clean_leetcode_data(raw_data):
|
| 68 |
"""Cleans and filters LeetCode data, summarizing top skills."""
|
| 69 |
if not raw_data:
|
|
|
|
| 70 |
return None
|
| 71 |
|
| 72 |
# Flatten all skills into a single list to find the absolute top skills
|
|
@@ -103,6 +116,7 @@ def clean_leetcode_data(raw_data):
|
|
| 103 |
def clean_github_data(raw_data):
|
| 104 |
"""Summarizes GitHub data, cleans README, and fixes pinned repo logic."""
|
| 105 |
if not raw_data:
|
|
|
|
| 106 |
return None
|
| 107 |
|
| 108 |
def summarize_repo(repo):
|
|
@@ -156,6 +170,7 @@ def clean_github_data(raw_data):
|
|
| 156 |
def clean_codeforces_data(raw_data):
|
| 157 |
"""Cleans Codeforces data, focusing on performance and simplifying contest history."""
|
| 158 |
if not raw_data:
|
|
|
|
| 159 |
return None
|
| 160 |
|
| 161 |
profile = raw_data.get("profile", {})
|
|
@@ -190,6 +205,57 @@ def clean_codeforces_data(raw_data):
|
|
| 190 |
]
|
| 191 |
}
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
# --- Main Execution Logic ---
|
| 194 |
|
| 195 |
def main():
|
|
@@ -197,15 +263,15 @@ def main():
|
|
| 197 |
ipu_scraper = StudentScraper(encryption_key="Qm9sRG9OYVphcmEK")
|
| 198 |
all_student_data = {}
|
| 199 |
|
| 200 |
-
|
| 201 |
|
| 202 |
for student in STUDENTS_TO_FETCH:
|
| 203 |
enrollment_no = student.get("enrollment_no")
|
| 204 |
if not enrollment_no:
|
| 205 |
-
|
| 206 |
continue
|
| 207 |
|
| 208 |
-
|
| 209 |
|
| 210 |
student_record = {
|
| 211 |
"name": None,
|
|
@@ -216,72 +282,89 @@ def main():
|
|
| 216 |
"github": None,
|
| 217 |
"codeforces": None,
|
| 218 |
},
|
|
|
|
| 219 |
"errors": {}
|
| 220 |
}
|
| 221 |
|
| 222 |
# Fetch, Clean, and Assign Data
|
| 223 |
try:
|
| 224 |
-
|
| 225 |
raw_ipu_data = ipu_scraper.get_student_data(enrollment_no)
|
| 226 |
student_record["academic_profile"] = clean_ipu_data(raw_ipu_data)
|
| 227 |
if student_record["academic_profile"]:
|
| 228 |
student_record["name"] = raw_ipu_data.get("student_info", {}).get("name")
|
| 229 |
-
|
| 230 |
else:
|
| 231 |
-
|
| 232 |
except Exception as e:
|
| 233 |
student_record["errors"]["ipu"] = str(e)
|
| 234 |
-
|
| 235 |
|
| 236 |
if student.get("leetcode_user"):
|
| 237 |
try:
|
| 238 |
-
|
| 239 |
raw_leetcode_result = get_leetcode_profile(student["leetcode_user"])
|
| 240 |
if raw_leetcode_result.get("success"):
|
| 241 |
student_record["coding_profiles"]["leetcode"] = clean_leetcode_data(raw_leetcode_result["data"])
|
| 242 |
-
|
| 243 |
else:
|
| 244 |
raise Exception(raw_leetcode_result.get("error", "Unknown error"))
|
| 245 |
except Exception as e:
|
| 246 |
student_record["errors"]["leetcode"] = str(e)
|
| 247 |
-
|
| 248 |
|
| 249 |
if student.get("github_user"):
|
| 250 |
try:
|
| 251 |
-
|
| 252 |
raw_github_result = get_github_profile(student["github_user"])
|
| 253 |
if raw_github_result.get("success"):
|
| 254 |
student_record["coding_profiles"]["github"] = clean_github_data(raw_github_result["data"])
|
| 255 |
-
|
| 256 |
else:
|
| 257 |
raise Exception(raw_github_result.get("error", "Unknown error"))
|
| 258 |
except Exception as e:
|
| 259 |
student_record["errors"]["github"] = str(e)
|
| 260 |
-
|
| 261 |
|
| 262 |
if student.get("codeforces_user"):
|
| 263 |
try:
|
| 264 |
-
|
| 265 |
raw_codeforces_result = get_codeforces_profile(student["codeforces_user"])
|
| 266 |
if raw_codeforces_result.get("success"):
|
| 267 |
student_record["coding_profiles"]["codeforces"] = clean_codeforces_data(raw_codeforces_result["data"])
|
| 268 |
-
|
| 269 |
else:
|
| 270 |
raise Exception(raw_codeforces_result.get("error", "Unknown error"))
|
| 271 |
except Exception as e:
|
| 272 |
student_record["errors"]["codeforces"] = str(e)
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
all_student_data[enrollment_no] = student_record
|
| 276 |
-
time.sleep(1)
|
| 277 |
|
| 278 |
# Save the final cleaned & aggregated data
|
| 279 |
try:
|
| 280 |
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
| 281 |
json.dump(all_student_data, f, indent=4, ensure_ascii=False)
|
| 282 |
-
|
| 283 |
except Exception as e:
|
| 284 |
-
|
| 285 |
|
| 286 |
if __name__ == "__main__":
|
| 287 |
main()
|
|
|
|
| 1 |
+
# data_aggregator.py (Complete Version with Resume Parsing)
|
| 2 |
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
import time
|
| 7 |
from datetime import datetime
|
| 8 |
+
import logging
|
| 9 |
|
| 10 |
+
# Configure logging
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=logging.INFO,
|
| 13 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 14 |
+
)
|
| 15 |
+
logger = logging.getLogger('data_aggregator')
|
| 16 |
+
|
| 17 |
+
# Import the scraper functions and classes
|
| 18 |
from github_scraper import get_github_profile
|
| 19 |
from codeforces_scraper import get_codeforces_profile
|
| 20 |
from leetcode_scraper import get_leetcode_profile
|
| 21 |
from ipu_scraper import StudentScraper
|
| 22 |
+
# Import our resume parser
|
| 23 |
+
from resume_parser import parse_resume
|
| 24 |
|
| 25 |
# --- Configuration ---
|
| 26 |
+
# Define the list of students with resume paths
|
| 27 |
STUDENTS_TO_FETCH = [
|
| 28 |
{
|
| 29 |
"enrollment_no": "35214811922",
|
| 30 |
"leetcode_user": "akshitsharma7093",
|
| 31 |
"github_user": "akshit7093",
|
| 32 |
+
"codeforces_user": "akshit7093",
|
| 33 |
+
"resume_path": "resume.pdf" # REQUIRED FIELD
|
| 34 |
}
|
| 35 |
# Add more student dictionaries here
|
| 36 |
]
|
|
|
|
| 42 |
def clean_ipu_data(raw_data):
|
| 43 |
"""Transforms raw IPU academic data into a final, clean format."""
|
| 44 |
if not raw_data or raw_data.get("status") != "success":
|
| 45 |
+
logger.warning("IPU data is empty or failed")
|
| 46 |
return None
|
| 47 |
|
| 48 |
overall = raw_data["academic_summary"]["overall_performance"]
|
|
|
|
| 79 |
def clean_leetcode_data(raw_data):
|
| 80 |
"""Cleans and filters LeetCode data, summarizing top skills."""
|
| 81 |
if not raw_data:
|
| 82 |
+
logger.warning("LeetCode data is empty")
|
| 83 |
return None
|
| 84 |
|
| 85 |
# Flatten all skills into a single list to find the absolute top skills
|
|
|
|
| 116 |
def clean_github_data(raw_data):
|
| 117 |
"""Summarizes GitHub data, cleans README, and fixes pinned repo logic."""
|
| 118 |
if not raw_data:
|
| 119 |
+
logger.warning("GitHub data is empty")
|
| 120 |
return None
|
| 121 |
|
| 122 |
def summarize_repo(repo):
|
|
|
|
| 170 |
def clean_codeforces_data(raw_data):
|
| 171 |
"""Cleans Codeforces data, focusing on performance and simplifying contest history."""
|
| 172 |
if not raw_data:
|
| 173 |
+
logger.warning("Codeforces data is empty")
|
| 174 |
return None
|
| 175 |
|
| 176 |
profile = raw_data.get("profile", {})
|
|
|
|
| 205 |
]
|
| 206 |
}
|
| 207 |
|
| 208 |
+
def clean_resume_data(raw_resume_data):
|
| 209 |
+
"""Processes raw resume data into final structured format"""
|
| 210 |
+
if not raw_resume_data:
|
| 211 |
+
logger.warning("Resume data is empty")
|
| 212 |
+
return None
|
| 213 |
+
|
| 214 |
+
# Extract only professional hyperlinks (filter out common non-professional links)
|
| 215 |
+
professional_links = [
|
| 216 |
+
url for url in raw_resume_data["hyperlinks"]
|
| 217 |
+
if not re.search(r'(facebook|instagram|twitter|linkedin\.com\/in\/[^\/]+\/(detail|overlay)|youtube)', url, re.I)
|
| 218 |
+
]
|
| 219 |
+
|
| 220 |
+
# Extract skills from resume text (simplified approach)
|
| 221 |
+
skills = []
|
| 222 |
+
skill_keywords = ['python', 'java', 'javascript', 'react', 'node', 'angular', 'vue', 'sql',
|
| 223 |
+
'mongodb', 'aws', 'docker', 'kubernetes', 'git', 'c++', 'c#', 'typescript',
|
| 224 |
+
'html', 'css', 'spring', 'django', 'flask', 'tensorflow', 'pytorch', 'dsa',
|
| 225 |
+
'data structures', 'algorithms', 'problem solving', 'full stack', 'backend',
|
| 226 |
+
'frontend', 'mobile', 'android', 'ios', 'flutter', 'react native']
|
| 227 |
+
|
| 228 |
+
resume_text = raw_resume_data["full_text"].lower()
|
| 229 |
+
for keyword in skill_keywords:
|
| 230 |
+
if keyword in resume_text and keyword not in skills:
|
| 231 |
+
skills.append(keyword.capitalize())
|
| 232 |
+
|
| 233 |
+
# Identify missing elements (simplified approach)
|
| 234 |
+
missing_elements = []
|
| 235 |
+
if 'projects' not in resume_text and 'project' not in resume_text:
|
| 236 |
+
missing_elements.append("Projects section")
|
| 237 |
+
if 'internship' not in resume_text and 'experience' not in resume_text and 'work' not in resume_text:
|
| 238 |
+
missing_elements.append("Work experience")
|
| 239 |
+
if 'education' not in resume_text and 'degree' not in resume_text:
|
| 240 |
+
missing_elements.append("Education details")
|
| 241 |
+
if len(skills) < 3:
|
| 242 |
+
missing_elements.append("Technical skills listing")
|
| 243 |
+
|
| 244 |
+
# Clean summary text (remove excessive whitespace and special characters)
|
| 245 |
+
cleaned_summary = re.sub(r'\s{2,}', ' ', raw_resume_data["summary"])
|
| 246 |
+
cleaned_summary = re.sub(r'[^\w\s.,;:!?()\-]', '', cleaned_summary)
|
| 247 |
+
|
| 248 |
+
return {
|
| 249 |
+
"full_text": raw_resume_data["full_text"],
|
| 250 |
+
"full_text_preview": raw_resume_data["full_text"][:500] + "..." if len(raw_resume_data["full_text"]) > 500 else raw_resume_data["full_text"],
|
| 251 |
+
"professional_links": professional_links,
|
| 252 |
+
"skills_summary": cleaned_summary,
|
| 253 |
+
"key_skills": skills,
|
| 254 |
+
"total_hyperlinks": len(raw_resume_data["hyperlinks"]),
|
| 255 |
+
"professional_link_count": len(professional_links),
|
| 256 |
+
"missing_elements": missing_elements
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
# --- Main Execution Logic ---
|
| 260 |
|
| 261 |
def main():
|
|
|
|
| 263 |
ipu_scraper = StudentScraper(encryption_key="Qm9sRG9OYVphcmEK")
|
| 264 |
all_student_data = {}
|
| 265 |
|
| 266 |
+
logger.info(f"Starting data aggregation for {len(STUDENTS_TO_FETCH)} student(s)...")
|
| 267 |
|
| 268 |
for student in STUDENTS_TO_FETCH:
|
| 269 |
enrollment_no = student.get("enrollment_no")
|
| 270 |
if not enrollment_no:
|
| 271 |
+
logger.warning("Skipping entry due to missing enrollment number.")
|
| 272 |
continue
|
| 273 |
|
| 274 |
+
logger.info(f"\nProcessing data for Enrollment No: {enrollment_no}")
|
| 275 |
|
| 276 |
student_record = {
|
| 277 |
"name": None,
|
|
|
|
| 282 |
"github": None,
|
| 283 |
"codeforces": None,
|
| 284 |
},
|
| 285 |
+
"resume": None, # NEW FIELD
|
| 286 |
"errors": {}
|
| 287 |
}
|
| 288 |
|
| 289 |
# Fetch, Clean, and Assign Data
|
| 290 |
try:
|
| 291 |
+
logger.info(" - Processing IPU data...")
|
| 292 |
raw_ipu_data = ipu_scraper.get_student_data(enrollment_no)
|
| 293 |
student_record["academic_profile"] = clean_ipu_data(raw_ipu_data)
|
| 294 |
if student_record["academic_profile"]:
|
| 295 |
student_record["name"] = raw_ipu_data.get("student_info", {}).get("name")
|
| 296 |
+
logger.info(" > IPU data processed successfully.")
|
| 297 |
else:
|
| 298 |
+
raise Exception("Failed to process IPU data.")
|
| 299 |
except Exception as e:
|
| 300 |
student_record["errors"]["ipu"] = str(e)
|
| 301 |
+
logger.error(f" > IPU processing FAILED: {e}")
|
| 302 |
|
| 303 |
if student.get("leetcode_user"):
|
| 304 |
try:
|
| 305 |
+
logger.info(f" - Processing LeetCode data for '{student['leetcode_user']}'...")
|
| 306 |
raw_leetcode_result = get_leetcode_profile(student["leetcode_user"])
|
| 307 |
if raw_leetcode_result.get("success"):
|
| 308 |
student_record["coding_profiles"]["leetcode"] = clean_leetcode_data(raw_leetcode_result["data"])
|
| 309 |
+
logger.info(" > LeetCode data processed successfully.")
|
| 310 |
else:
|
| 311 |
raise Exception(raw_leetcode_result.get("error", "Unknown error"))
|
| 312 |
except Exception as e:
|
| 313 |
student_record["errors"]["leetcode"] = str(e)
|
| 314 |
+
logger.error(f" > LeetCode processing FAILED: {e}")
|
| 315 |
|
| 316 |
if student.get("github_user"):
|
| 317 |
try:
|
| 318 |
+
logger.info(f" - Processing GitHub data for '{student['github_user']}'...")
|
| 319 |
raw_github_result = get_github_profile(student["github_user"])
|
| 320 |
if raw_github_result.get("success"):
|
| 321 |
student_record["coding_profiles"]["github"] = clean_github_data(raw_github_result["data"])
|
| 322 |
+
logger.info(" > GitHub data processed successfully.")
|
| 323 |
else:
|
| 324 |
raise Exception(raw_github_result.get("error", "Unknown error"))
|
| 325 |
except Exception as e:
|
| 326 |
student_record["errors"]["github"] = str(e)
|
| 327 |
+
logger.error(f" > GitHub processing FAILED: {e}")
|
| 328 |
|
| 329 |
if student.get("codeforces_user"):
|
| 330 |
try:
|
| 331 |
+
logger.info(f" - Processing Codeforces data for '{student['codeforces_user']}'...")
|
| 332 |
raw_codeforces_result = get_codeforces_profile(student["codeforces_user"])
|
| 333 |
if raw_codeforces_result.get("success"):
|
| 334 |
student_record["coding_profiles"]["codeforces"] = clean_codeforces_data(raw_codeforces_result["data"])
|
| 335 |
+
logger.info(" > Codeforces data processed successfully.")
|
| 336 |
else:
|
| 337 |
raise Exception(raw_codeforces_result.get("error", "Unknown error"))
|
| 338 |
except Exception as e:
|
| 339 |
student_record["errors"]["codeforces"] = str(e)
|
| 340 |
+
logger.error(f" > Codeforces processing FAILED: {e}")
|
| 341 |
+
|
| 342 |
+
# Process resume data
|
| 343 |
+
if student.get("resume_path"):
|
| 344 |
+
try:
|
| 345 |
+
logger.info(f" - Processing resume from '{student['resume_path']}'...")
|
| 346 |
+
|
| 347 |
+
# Check if file exists
|
| 348 |
+
if not os.path.exists(student["resume_path"]):
|
| 349 |
+
raise FileNotFoundError(f"Resume file not found at {student['resume_path']}")
|
| 350 |
+
|
| 351 |
+
raw_resume_data = parse_resume(student["resume_path"])
|
| 352 |
+
student_record["resume"] = clean_resume_data(raw_resume_data)
|
| 353 |
+
logger.info(" > Resume data processed successfully.")
|
| 354 |
+
except Exception as e:
|
| 355 |
+
student_record["errors"]["resume"] = str(e)
|
| 356 |
+
logger.error(f" > Resume processing FAILED: {e}")
|
| 357 |
|
| 358 |
all_student_data[enrollment_no] = student_record
|
| 359 |
+
time.sleep(1) # Respectful delay between requests
|
| 360 |
|
| 361 |
# Save the final cleaned & aggregated data
|
| 362 |
try:
|
| 363 |
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
| 364 |
json.dump(all_student_data, f, indent=4, ensure_ascii=False)
|
| 365 |
+
logger.info(f"\n✅ Final cleaning complete. Data saved to '{OUTPUT_FILE}'.")
|
| 366 |
except Exception as e:
|
| 367 |
+
logger.error(f"\n❌ Error saving final JSON file: {e}")
|
| 368 |
|
| 369 |
if __name__ == "__main__":
|
| 370 |
main()
|
app copy.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
# app.py
|
| 2 |
-
from flask import Flask, render_template, jsonify, request
|
| 3 |
-
from rag_system import StudentApiRAG
|
| 4 |
-
import json
|
| 5 |
-
|
| 6 |
-
app = Flask(__name__)
|
| 7 |
-
|
| 8 |
-
# Initialize the RAG system once when the application starts.
|
| 9 |
-
# This is efficient as the data is loaded into memory only once.
|
| 10 |
-
print("Initializing Student Analysis System...")
|
| 11 |
-
rag_system = StudentApiRAG()
|
| 12 |
-
print("System Initialized Successfully.")
|
| 13 |
-
|
| 14 |
-
# --- API Endpoints ---
|
| 15 |
-
|
| 16 |
-
@app.route('/')
|
| 17 |
-
def index():
|
| 18 |
-
"""Serves the main HTML page."""
|
| 19 |
-
return render_template('final.html')
|
| 20 |
-
|
| 21 |
-
@app.route('/api/students', methods=['GET'])
|
| 22 |
-
def get_students():
|
| 23 |
-
"""Returns a list of all available students to populate the dropdown."""
|
| 24 |
-
student_list = [
|
| 25 |
-
{"enrollment_no": eno, "name": data.get("name", "Unknown")}
|
| 26 |
-
for eno, data in rag_system.student_data.items()
|
| 27 |
-
]
|
| 28 |
-
return jsonify(student_list)
|
| 29 |
-
|
| 30 |
-
@app.route('/api/report/<enrollment_no>', methods=['GET'])
|
| 31 |
-
def get_report(enrollment_no):
|
| 32 |
-
"""Generates and returns the full structured report for a student."""
|
| 33 |
-
if not enrollment_no:
|
| 34 |
-
return jsonify({"error": "Enrollment number is required."}), 400
|
| 35 |
-
|
| 36 |
-
report = rag_system.generate_structured_report(enrollment_no)
|
| 37 |
-
|
| 38 |
-
if "error" in report:
|
| 39 |
-
return jsonify(report), 500
|
| 40 |
-
|
| 41 |
-
return jsonify(report)
|
| 42 |
-
|
| 43 |
-
@app.route('/api/ask', methods=['POST'])
|
| 44 |
-
def ask_question():
|
| 45 |
-
"""Handles a chatbot question and returns the AI's answer."""
|
| 46 |
-
data = request.json
|
| 47 |
-
enrollment_no = data.get('enrollment_no')
|
| 48 |
-
question = data.get('question')
|
| 49 |
-
|
| 50 |
-
if not all([enrollment_no, question]):
|
| 51 |
-
return jsonify({"error": "Enrollment number and question are required."}), 400
|
| 52 |
-
|
| 53 |
-
answer = rag_system.answer_question(question, enrollment_no)
|
| 54 |
-
return jsonify({"answer": answer})
|
| 55 |
-
|
| 56 |
-
if __name__ == '__main__':
|
| 57 |
-
# Use host='0.0.0.0' to make it accessible from your local network
|
| 58 |
-
app.run(host='0.0.0.0', port=5000, debug=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app_copy.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app_copy.py (or whatever your main Flask app file is named)
|
| 2 |
+
|
| 3 |
+
from flask import Flask, jsonify, request, render_template, redirect, url_for
|
| 4 |
+
import os
|
| 5 |
+
# Import the RAG system class
|
| 6 |
+
from rag_system import StudentApiRAG
|
| 7 |
+
import logging # Import logging module
|
| 8 |
+
|
| 9 |
+
# --- Configure logging ---
|
| 10 |
+
# It's generally good practice to configure logging early
|
| 11 |
+
logging.basicConfig(level=logging.INFO) # Adjust level as needed (DEBUG, INFO, WARNING, ERROR)
|
| 12 |
+
logger = logging.getLogger(__name__) # Create a logger for this module
|
| 13 |
+
|
| 14 |
+
# --- Initialize Flask App ---
|
| 15 |
+
app = Flask(__name__,
|
| 16 |
+
static_folder='static', # Directory for CSS/JS files
|
| 17 |
+
template_folder='templates') # Directory for HTML files
|
| 18 |
+
|
| 19 |
+
# Ensure static and templates directories exist
|
| 20 |
+
os.makedirs('static', exist_ok=True)
|
| 21 |
+
os.makedirs('templates', exist_ok=True)
|
| 22 |
+
|
| 23 |
+
# --- Initialize the RAG System ---
|
| 24 |
+
# This is the crucial part: create the instance and attach it to the app object
|
| 25 |
+
# Make sure the GOOGLE_API_KEY environment variable is set
|
| 26 |
+
try:
|
| 27 |
+
app.rag_system = StudentApiRAG() # <-- This line is key
|
| 28 |
+
logger.info("RAG System initialized successfully.")
|
| 29 |
+
except Exception as e:
|
| 30 |
+
logger.error(f"Failed to initialize RAG System: {e}")
|
| 31 |
+
# Depending on your needs, you might want to exit here or disable related features
|
| 32 |
+
app.rag_system = None # Or handle the error appropriately
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# --- Define Routes ---
|
| 36 |
+
|
| 37 |
+
@app.route('/')
|
| 38 |
+
def home():
|
| 39 |
+
return redirect(url_for('test_frontend'))
|
| 40 |
+
|
| 41 |
+
@app.route('/test') # Assuming this is your frontend route
|
| 42 |
+
def test_frontend():
|
| 43 |
+
return render_template('final.html') # Make sure index.html exists in templates/
|
| 44 |
+
|
| 45 |
+
# --- Example route for students (adjust as needed) ---
|
| 46 |
+
@app.route('/api/students', methods=['GET'])
|
| 47 |
+
def get_students_list():
|
| 48 |
+
# Dummy implementation or integrate with your student data
|
| 49 |
+
# This is just an example, replace with actual logic
|
| 50 |
+
return jsonify([{"enrollment_no": "35214811922", "name": "Akshit Sharma"}])
|
| 51 |
+
|
| 52 |
+
# --- Job Analysis Route (Corrected) ---
|
| 53 |
+
@app.route('/api/job-analysis', methods=['POST'])
|
| 54 |
+
def analyze_job_application_route():
|
| 55 |
+
"""Endpoint for job application analysis"""
|
| 56 |
+
# Check if RAG system was initialized successfully
|
| 57 |
+
if not app.rag_system:
|
| 58 |
+
logger.error("RAG System not initialized. Cannot perform job analysis.")
|
| 59 |
+
return jsonify({
|
| 60 |
+
'success': False,
|
| 61 |
+
'error': 'Internal server error: Analysis system not available.'
|
| 62 |
+
}), 500
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
data = request.get_json()
|
| 66 |
+
if not data:
|
| 67 |
+
logger.warning("No JSON data received in job analysis request.")
|
| 68 |
+
return jsonify({
|
| 69 |
+
'success': False,
|
| 70 |
+
'error': 'Invalid request: No JSON data provided.'
|
| 71 |
+
}), 400
|
| 72 |
+
|
| 73 |
+
job_application_link = data.get('job_application_link')
|
| 74 |
+
# Get the enrollment number from the request data
|
| 75 |
+
enrollment_no = data.get('enrollment_no')
|
| 76 |
+
|
| 77 |
+
if not job_application_link or not enrollment_no:
|
| 78 |
+
logger.warning("Missing 'job_application_link' or 'enrollment_no' in job analysis request data.")
|
| 79 |
+
return jsonify({
|
| 80 |
+
'success': False,
|
| 81 |
+
'error': 'Job application link and enrollment number are required.'
|
| 82 |
+
}), 400
|
| 83 |
+
|
| 84 |
+
logger.info(f"Initiating job application analysis for link: {job_application_link} and student: {enrollment_no}")
|
| 85 |
+
# Call the analyze_job_application method on the RAG system instance with both arguments
|
| 86 |
+
result = app.rag_system.analyze_job_application(job_application_link, enrollment_no)
|
| 87 |
+
|
| 88 |
+
if result.get("error"):
|
| 89 |
+
logger.warning(f"Job analysis reported an error: {result['error']}")
|
| 90 |
+
return jsonify({
|
| 91 |
+
'success': False,
|
| 92 |
+
'error': result["error"]
|
| 93 |
+
}), 400 # Or maybe 500 if it's an internal processing error?
|
| 94 |
+
|
| 95 |
+
logger.info("Job application analysis completed successfully.")
|
| 96 |
+
return jsonify({
|
| 97 |
+
'success': True,
|
| 98 |
+
'data': result
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
# Now 'logger' is defined and can be used
|
| 103 |
+
logger.error(f"Error in job analysis route: {e}", exc_info=True) # exc_info=True adds traceback
|
| 104 |
+
return jsonify({
|
| 105 |
+
'success': False,
|
| 106 |
+
'error': 'Failed to analyze job application due to an internal server error.'
|
| 107 |
+
}), 500
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# Add this route to your app_copy.py
|
| 111 |
+
@app.route('/api/ask', methods=['POST'])
|
| 112 |
+
def answer_question_route():
|
| 113 |
+
"""Endpoint for answering questions about a student"""
|
| 114 |
+
# Check if RAG system was initialized successfully
|
| 115 |
+
if not app.rag_system:
|
| 116 |
+
logger.error("RAG System not initialized. Cannot answer question.")
|
| 117 |
+
return jsonify({
|
| 118 |
+
'success': False,
|
| 119 |
+
'error': 'Internal server error: Question answering system not available.'
|
| 120 |
+
}), 500
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
data = request.get_json()
|
| 124 |
+
if not data:
|
| 125 |
+
logger.warning("No JSON data received in question request.")
|
| 126 |
+
return jsonify({
|
| 127 |
+
'success': False,
|
| 128 |
+
'error': 'Invalid request: No JSON data provided.'
|
| 129 |
+
}), 400
|
| 130 |
+
|
| 131 |
+
enrollment_no = data.get('enrollment_no')
|
| 132 |
+
question = data.get('question')
|
| 133 |
+
|
| 134 |
+
if not enrollment_no or not question:
|
| 135 |
+
logger.warning("Missing 'enrollment_no' or 'question' in request data.")
|
| 136 |
+
return jsonify({
|
| 137 |
+
'success': False,
|
| 138 |
+
'error': 'Enrollment number and question are required.'
|
| 139 |
+
}), 400
|
| 140 |
+
|
| 141 |
+
logger.info(f"Answering question for enrollment {enrollment_no}: {question}")
|
| 142 |
+
# Call the answer_question method on the RAG system instance
|
| 143 |
+
answer = app.rag_system.answer_question(question, enrollment_no)
|
| 144 |
+
|
| 145 |
+
logger.info("Question answered successfully.")
|
| 146 |
+
return jsonify({
|
| 147 |
+
'success': True,
|
| 148 |
+
'answer': answer
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.error(f"Error in question answering route: {e}", exc_info=True)
|
| 153 |
+
return jsonify({
|
| 154 |
+
'success': False,
|
| 155 |
+
'error': 'Failed to answer question due to an internal server error.'
|
| 156 |
+
}), 500
|
| 157 |
+
|
| 158 |
+
# --- Route for getting student-specific dashboard metrics ---
|
| 159 |
+
@app.route('/api/dashboard/metrics/<enrollment_no>', methods=['GET'])
|
| 160 |
+
def get_student_dashboard_metrics(enrollment_no: str):
|
| 161 |
+
"""Endpoint for getting detailed dashboard metrics for a specific student."""
|
| 162 |
+
if not app.rag_system:
|
| 163 |
+
logger.error("RAG System not initialized. Cannot get dashboard metrics.")
|
| 164 |
+
return jsonify({'error': 'Internal server error: Dashboard system not available.'}), 500
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
logger.info(f"Fetching dashboard metrics for enrollment: {enrollment_no}")
|
| 168 |
+
# Call the new get_student_dashboard_metrics method on the RAG system instance
|
| 169 |
+
metrics_data = app.rag_system.get_student_dashboard_metrics(enrollment_no)
|
| 170 |
+
|
| 171 |
+
if metrics_data.get("error"):
|
| 172 |
+
logger.warning(f"Dashboard metrics error for {enrollment_no}: {metrics_data['error']}")
|
| 173 |
+
return jsonify(metrics_data), 404 # Or appropriate error code
|
| 174 |
+
return jsonify(metrics_data)
|
| 175 |
+
except Exception as e:
|
| 176 |
+
logger.error(f"Error fetching dashboard metrics for {enrollment_no}: {e}", exc_info=True)
|
| 177 |
+
return jsonify({'error': 'Failed to fetch dashboard metrics.'}), 500
|
| 178 |
+
|
| 179 |
+
# --- Other routes (e.g., /api/report/<enrollment_no>, /api/ask, etc.) ---
|
| 180 |
+
# Make sure to use app.rag_system.<method_name>() for calling RAG methods
|
| 181 |
+
# Example placeholder for report generation:
|
| 182 |
+
@app.route('/api/report/<enrollment_no>', methods=['GET'])
|
| 183 |
+
def get_student_report(enrollment_no: str):
|
| 184 |
+
if not app.rag_system:
|
| 185 |
+
logger.error("RAG System not initialized. Cannot generate report.")
|
| 186 |
+
return jsonify({'error': 'Internal server error: Report system not available.'}), 500
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
logger.info(f"Generating report for enrollment: {enrollment_no}")
|
| 190 |
+
report_data = app.rag_system.generate_structured_report(enrollment_no)
|
| 191 |
+
if report_data.get("error"):
|
| 192 |
+
logger.warning(f"Report generation error for {enrollment_no}: {report_data['error']}")
|
| 193 |
+
return jsonify(report_data), 404 # Or appropriate error code
|
| 194 |
+
return jsonify(report_data)
|
| 195 |
+
except Exception as e:
|
| 196 |
+
logger.error(f"Error generating report for {enrollment_no}: {e}", exc_info=True)
|
| 197 |
+
return jsonify({'error': 'Failed to generate report.'}), 500
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
if __name__ == '__main__':
|
| 201 |
+
# Run the app
|
| 202 |
+
|
| 203 |
+
app.run(host='0.0.0.0', port=5000, debug=True) # Adjust host/port/debug as needed
|
dashboard_analyzer.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dashboard_analyzer.py
|
| 2 |
+
import logging
|
| 3 |
+
import json
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
# --- Constants for Scoring and Analysis ---
|
| 9 |
+
# These can be tweaked to adjust the analysis logic. They are data-agnostic.
|
| 10 |
+
THRESHOLDS = {
|
| 11 |
+
'CGPA_EXCELLENT': 8.5,
|
| 12 |
+
'CGPA_GOOD': 7.5,
|
| 13 |
+
'LEETCODE_TOTAL_HIGH': 200,
|
| 14 |
+
'LEETCODE_TOTAL_MEDIUM': 100,
|
| 15 |
+
'GITHUB_STARS_HIGH': 10,
|
| 16 |
+
'GITHUB_REPOS_HIGH': 20,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
WEIGHTS = {
|
| 20 |
+
'LEETCODE_EASY': 0.2,
|
| 21 |
+
'LEETCODE_MEDIUM': 1.0,
|
| 22 |
+
'LEETCODE_HARD': 2.5,
|
| 23 |
+
'GITHUB_STARS': 2.0,
|
| 24 |
+
'GITHUB_FORKS': 3.0,
|
| 25 |
+
'GITHUB_REPOS': 0.5,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
def get_dashboard_metrics(student_data: dict) -> dict:
|
| 29 |
+
"""
|
| 30 |
+
Performs a fully data-driven, advanced analysis of a student's raw JSON data
|
| 31 |
+
to extract a rich set of metrics for the dashboard without any hardcoded assumptions.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
student_data (dict): The dictionary containing all data for one student.
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
dict: A deeply nested dictionary with structured dashboard metrics and insights.
|
| 38 |
+
"""
|
| 39 |
+
if not student_data:
|
| 40 |
+
return {"error": "No student data provided."}
|
| 41 |
+
|
| 42 |
+
# --- Perform analysis on different sections of the profile ---
|
| 43 |
+
academics = _analyze_academics(student_data.get("academic_profile", {}))
|
| 44 |
+
leetcode = _analyze_leetcode(student_data.get("coding_profiles", {}).get("leetcode", {}))
|
| 45 |
+
github = _analyze_github(student_data.get("coding_profiles", {}).get("github", {}))
|
| 46 |
+
skills = _extract_skills(student_data)
|
| 47 |
+
completeness = _calculate_profile_completeness(student_data)
|
| 48 |
+
|
| 49 |
+
# --- Synthesize overall insights from the analyses ---
|
| 50 |
+
archetype = _determine_student_archetype(skills, leetcode, github)
|
| 51 |
+
|
| 52 |
+
# --- Assemble the final, comprehensive metrics object ---
|
| 53 |
+
return {
|
| 54 |
+
"overall_summary": {
|
| 55 |
+
"student_archetype": archetype,
|
| 56 |
+
"profile_completeness": completeness
|
| 57 |
+
},
|
| 58 |
+
"academics": academics,
|
| 59 |
+
"coding_profiles": {
|
| 60 |
+
"leetcode": leetcode,
|
| 61 |
+
"github": github
|
| 62 |
+
},
|
| 63 |
+
"skills_distribution": skills,
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
def _analyze_academics(academic_data: dict) -> dict:
|
| 67 |
+
"""
|
| 68 |
+
Analyzes academic performance dynamically from the data provided.
|
| 69 |
+
Includes trajectory, overall subject performance, and detailed semester overviews.
|
| 70 |
+
"""
|
| 71 |
+
cgpa = academic_data.get("overall_cgpa", 0)
|
| 72 |
+
|
| 73 |
+
# Qualitative Rating based on CGPA
|
| 74 |
+
if cgpa >= THRESHOLDS['CGPA_EXCELLENT']: rating = "Excellent"
|
| 75 |
+
elif cgpa >= THRESHOLDS['CGPA_GOOD']: rating = "Good"
|
| 76 |
+
else: rating = "Needs Improvement"
|
| 77 |
+
|
| 78 |
+
# Academic Trajectory based on SGPA trend
|
| 79 |
+
sgpa_list = [sem.get("sgpa", 0) for sem in academic_data.get("semester_performance", [])]
|
| 80 |
+
trajectory = "Stable"
|
| 81 |
+
if len(sgpa_list) > 2:
|
| 82 |
+
first_half_avg = sum(sgpa_list[:len(sgpa_list)//2]) / (len(sgpa_list)//2)
|
| 83 |
+
second_half_avg = sum(sgpa_list[len(sgpa_list)//2:]) / (len(sgpa_list) - len(sgpa_list)//2)
|
| 84 |
+
if second_half_avg > first_half_avg + 0.2: trajectory = "Improving"
|
| 85 |
+
elif second_half_avg < first_half_avg - 0.2: trajectory = "Declining"
|
| 86 |
+
|
| 87 |
+
# --- Detailed Semester Overviews and Overall Subject Analysis ---
|
| 88 |
+
all_subjects_overall = []
|
| 89 |
+
semester_overviews = []
|
| 90 |
+
|
| 91 |
+
for semester_info in academic_data.get("semester_performance", []):
|
| 92 |
+
semester_subjects = []
|
| 93 |
+
high_grades_count = 0
|
| 94 |
+
|
| 95 |
+
for subject_info in semester_info.get("subjects", []):
|
| 96 |
+
subject_record = {
|
| 97 |
+
"name": subject_info.get("subject"),
|
| 98 |
+
"marks": subject_info.get("marks", 0)
|
| 99 |
+
}
|
| 100 |
+
semester_subjects.append(subject_record)
|
| 101 |
+
all_subjects_overall.append(subject_record)
|
| 102 |
+
|
| 103 |
+
if subject_info.get("grade") in ['O', 'A+']:
|
| 104 |
+
high_grades_count += 1
|
| 105 |
+
|
| 106 |
+
if semester_subjects:
|
| 107 |
+
semester_subjects.sort(key=lambda x: x['marks']) # Sort by marks ascending
|
| 108 |
+
|
| 109 |
+
semester_overviews.append({
|
| 110 |
+
"semester_number": semester_info.get("semester"),
|
| 111 |
+
"sgpa": semester_info.get("sgpa"),
|
| 112 |
+
"percentage": semester_info.get("percentage"),
|
| 113 |
+
"top_subject": semester_subjects[-1], # Last item is highest
|
| 114 |
+
"bottom_subject": semester_subjects[0], # First item is lowest
|
| 115 |
+
"high_grades_count": high_grades_count
|
| 116 |
+
})
|
| 117 |
+
|
| 118 |
+
# Determine overall subject strengths and weaknesses from all semesters
|
| 119 |
+
overall_strengths = []
|
| 120 |
+
overall_weaknesses = []
|
| 121 |
+
if all_subjects_overall:
|
| 122 |
+
all_subjects_overall.sort(key=lambda x: x['marks'], reverse=True) # Sort descending
|
| 123 |
+
overall_strengths = all_subjects_overall[:3] # Top 3 overall
|
| 124 |
+
overall_weaknesses = all_subjects_overall[-3:] # Bottom 3 overall
|
| 125 |
+
|
| 126 |
+
return {
|
| 127 |
+
"cgpa": cgpa,
|
| 128 |
+
"rating": rating,
|
| 129 |
+
"trajectory": trajectory,
|
| 130 |
+
"overall_subject_strengths": overall_strengths,
|
| 131 |
+
"overall_subject_weaknesses": overall_weaknesses,
|
| 132 |
+
"semester_overviews": semester_overviews
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
# --- The following functions are already fully data-driven and remain unchanged ---
|
| 136 |
+
|
| 137 |
+
def _analyze_leetcode(leetcode_data: dict) -> dict:
|
| 138 |
+
"""Performs a nuanced analysis of LeetCode performance."""
|
| 139 |
+
if not leetcode_data: return {"rating": "Not Available", "score": 0, "total_solved": 0}
|
| 140 |
+
total_solved = leetcode_data.get("totalSolved", 0)
|
| 141 |
+
try:
|
| 142 |
+
easy = int(leetcode_data.get("problemsByDifficulty", {}).get("Easy", "0/0").split('/')[0])
|
| 143 |
+
medium = int(leetcode_data.get("problemsByDifficulty", {}).get("Medium", "0/0").split('/')[0])
|
| 144 |
+
hard = int(leetcode_data.get("problemsByDifficulty", {}).get("Hard", "0/0").split('/')[0])
|
| 145 |
+
except (ValueError, IndexError): easy, medium, hard = 0, 0, 0
|
| 146 |
+
raw_score = (easy * WEIGHTS['LEETCODE_EASY'] + medium * WEIGHTS['LEETCODE_MEDIUM'] + hard * WEIGHTS['LEETCODE_HARD'])
|
| 147 |
+
target_score = (150 * WEIGHTS['LEETCODE_EASY'] + 100 * WEIGHTS['LEETCODE_MEDIUM'] + 30 * WEIGHTS['LEETCODE_HARD'])
|
| 148 |
+
normalized_score = round((raw_score / target_score) * 10, 1) if target_score > 0 else 0
|
| 149 |
+
final_score = min(normalized_score, 10.0)
|
| 150 |
+
rating = "Beginner"
|
| 151 |
+
if hard > 10 or medium > 50: rating = "Advanced Problem Solver"
|
| 152 |
+
elif medium > 25 or total_solved > THRESHOLDS['LEETCODE_TOTAL_HIGH']: rating = "Active Competitor"
|
| 153 |
+
elif total_solved > THRESHOLDS['LEETCODE_TOTAL_MEDIUM']: rating = "Consistent Learner"
|
| 154 |
+
return {"rating": rating, "score": final_score, "total_solved": total_solved, "difficulty_breakdown": {"easy": easy, "medium": medium, "hard": hard}}
|
| 155 |
+
|
| 156 |
+
def _analyze_github(github_data: dict) -> dict:
|
| 157 |
+
"""Analyzes GitHub profile for activity, impact, and tech stack."""
|
| 158 |
+
if not github_data: return {"rating": "Not Available", "activity_level": "Unknown"}
|
| 159 |
+
stats, repos = github_data.get("stats", {}), github_data.get("top_repositories", [])
|
| 160 |
+
activity_level = "Low"
|
| 161 |
+
if repos:
|
| 162 |
+
try:
|
| 163 |
+
latest_push = max(datetime.strptime(repo['last_pushed'], "%Y-%m-%d") for repo in repos if repo.get('last_pushed'))
|
| 164 |
+
if (datetime.now() - latest_push).days < 7: activity_level = "Very Active"
|
| 165 |
+
elif (datetime.now() - latest_push).days < 30: activity_level = "Active"
|
| 166 |
+
elif (datetime.now() - latest_push).days < 90: activity_level = "Inactive"
|
| 167 |
+
except (ValueError, TypeError): pass
|
| 168 |
+
impact_score = sum(repo.get('stars', 0) * WEIGHTS['GITHUB_STARS'] + repo.get('forks', 0) * WEIGHTS['GITHUB_FORKS'] for repo in repos)
|
| 169 |
+
top_languages = list(dict.fromkeys([repo.get("language") for repo in repos if repo.get("language")]))[:3]
|
| 170 |
+
rating = "Needs Development"
|
| 171 |
+
if impact_score > 50 or stats.get('public_repos', 0) > THRESHOLDS['GITHUB_REPOS_HIGH']: rating = "Strong Profile"
|
| 172 |
+
elif activity_level in ["Very Active", "Active"] or stats.get('public_repos', 0) > 10: rating = "Good Profile"
|
| 173 |
+
return {"rating": rating, "activity_level": activity_level, "top_languages": top_languages, "stats": stats}
|
| 174 |
+
|
| 175 |
+
def _extract_skills(student_data: dict) -> list:
|
| 176 |
+
"""Extracts, combines, and cleans a list of key skills."""
|
| 177 |
+
resume_skills = student_data.get("resume", {}).get("key_skills", [])
|
| 178 |
+
leetcode_skills = [item.get("skill") for item in student_data.get("coding_profiles", {}).get("leetcode", {}).get("topSkillsSummary", [])]
|
| 179 |
+
normalized_resume = [s.strip().title() for s in resume_skills]
|
| 180 |
+
normalized_leetcode = [s.strip().title() for s in leetcode_skills]
|
| 181 |
+
return list(dict.fromkeys(normalized_resume + normalized_leetcode))
|
| 182 |
+
|
| 183 |
+
def _calculate_profile_completeness(student_data: dict) -> dict:
|
| 184 |
+
"""Scores the profile based on the presence of key data points."""
|
| 185 |
+
checks = {
|
| 186 |
+
"Academics": bool(student_data.get("academic_profile", {}).get("semester_performance")),
|
| 187 |
+
"Resume": bool(student_data.get("resume", {}).get("key_skills")),
|
| 188 |
+
"LeetCode": bool(student_data.get("coding_profiles", {}).get("leetcode")),
|
| 189 |
+
"GitHub": bool(student_data.get("coding_profiles", {}).get("github")),
|
| 190 |
+
"Codeforces": bool(student_data.get("coding_profiles", {}).get("codeforces"))
|
| 191 |
+
}
|
| 192 |
+
score = int((sum(checks.values()) / len(checks)) * 100)
|
| 193 |
+
return {"score_percentage": score, "missing_sections": [key for key, value in checks.items() if not value]}
|
| 194 |
+
|
| 195 |
+
def _determine_student_archetype(skills: list, leetcode_metrics: dict, github_metrics: dict) -> list:
|
| 196 |
+
"""Generates dynamic tags based on analyzed metrics."""
|
| 197 |
+
archetypes = []
|
| 198 |
+
skills_lower = {s.lower() for s in skills}
|
| 199 |
+
if any(kw in skills_lower for kw in ["tensorflow", "pytorch", "ai", "machine learning", "nlp", "computer vision"]): archetypes.append("AI/ML Enthusiast")
|
| 200 |
+
if any(kw in skills_lower for kw in ["react", "node", "flask", "django", "backend", "frontend"]): archetypes.append("Web Developer")
|
| 201 |
+
if leetcode_metrics.get("rating") in ["Advanced Problem Solver", "Active Competitor"]: archetypes.append("Competitive Programmer")
|
| 202 |
+
if any(kw in skills_lower for kw in ["aws", "google cloud", "docker", "kubernetes"]): archetypes.append("Cloud & DevOps Oriented")
|
| 203 |
+
return archetypes if archetypes else ["Generalist"]
|
| 204 |
+
|
| 205 |
+
# --- Testing Block ---
|
| 206 |
+
if __name__ == '__main__':
|
| 207 |
+
print("Testing advanced, fully data-driven dashboard_analyzer.py...")
|
| 208 |
+
try:
|
| 209 |
+
with open('final_cleaned_student_data.json', 'r', encoding='utf-8') as f:
|
| 210 |
+
full_data = json.load(f)
|
| 211 |
+
sample_enrollment = "35214811922"
|
| 212 |
+
student_sample = full_data.get(sample_enrollment)
|
| 213 |
+
if student_sample:
|
| 214 |
+
metrics = get_dashboard_metrics(student_sample)
|
| 215 |
+
print("\n--- Generated Advanced Metrics ---")
|
| 216 |
+
print(json.dumps(metrics, indent=4))
|
| 217 |
+
else:
|
| 218 |
+
print(f"Error: Student with enrollment '{sample_enrollment}' not found.")
|
| 219 |
+
except FileNotFoundError:
|
| 220 |
+
print("Error: `final_cleaned_student_data.json` not found.")
|
| 221 |
+
except Exception as e:
|
| 222 |
+
logger.error(f"An unexpected error occurred during testing: {e}", exc_info=True)
|
final_cleaned_student_data.json
CHANGED
|
@@ -833,6 +833,50 @@
|
|
| 833 |
]
|
| 834 |
}
|
| 835 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
"errors": {}
|
| 837 |
}
|
| 838 |
}
|
|
|
|
| 833 |
]
|
| 834 |
}
|
| 835 |
},
|
| 836 |
+
"resume": {
|
| 837 |
+
"full_text": "Akshit Sharma \nFinal‑Year B.Tech (AI & Data Science) | Backend & AI/ML Engineer | Cloud‑Native Systems \nakshitsharma7096@gmail.com/ +91 8810248097/Github / Linkedin / LeetCode / CodeForces \nSKILLS\n \n●\nProgramming Languages: Python, Java, C/C++, JavaScript, SQL, React, Node.js, TypeScript, Flask, FastAPI \n●\nDatabases & Tools: Pandas, NumPy, Matplotlib, MongoDB, Postgre \n●\nML/AI & Frameworks: TensorFlow, PyTorch, NLP, Computer Vision, Transformers, RAG, LangChain \n●\nCloud & DevOps: AWS, Google Cloud Platform, OpenStack SDK, Docker, Kubernetes \n●\nSystems & Fundamentals: Unix/Linux, TCP/IP Networking, Git, Data Structures & Algorithms, Computer Networks \nEXPERIENCE\n \nResearch Intern | Directorate of Research, Government of Arunachal Pradesh\n\n \n(August 2025 – Present) \n●\nDeveloped a low-resource speech-to-speech translation pipeline using Wav2Vec 2.0 for ASR, MarianMT for NMT, and Tacotron 2 \nfor TTS, focusing on endangered languages with context-dependent meanings. \n●\nOptimised system architecture to reduce translation latency to under 2 seconds, enabling real time deployment for field use. \nDeep Learning Intern | Akanila Technologies \n https://github.com/akshit7093/Chatbot-for-websites \n(July 2024 – December 2024) \n●\nDeveloped a universal chatbot platform by fine‑tuning Llama3.1 LLM using LoRA and integrating RAG with FAISS for \ndomain‑specific retrieval, boosting query accuracy to 90 % and improving response relevance by 25 %. \n●\nDesigned a flexible Python backend with modular components in FastAPI increasing code reusability to 65% . \n●\nImplemented automated deployments on AWS EC2, leveraging Docker for containerization and Kubernetes for container \norchestration. \nMachine Learning Intern | CodSoft \n https://github.com/akshit7093/CODSOFT \n(August 2024 – September 2024) \n●\nDeveloped a credit card fraud detection system using XGBoost, analyzing 1 million transaction records. \n●\nEngineered 20+ features from behavioral and time-series data then trained an XGBoost model on SageMaker to drop false \npositives from 20% to 5% while keeping recall over 90%. \n●\nBuilt an NLP model for SMS spam detection using Python and scikit-learn, achieving 95% accuracy on test data. \nPROJECT\n \nOpenStack Cloud Management System with Natural Language Interface https://github.com/akshit7093/VM_manager_AgenticAi \n●\nBuilt a cloud management system interfacing with OpenStack infrastructure APIs. \n●\nEnabled users to issue natural language prompts (e.g., \"create a server\" or \"delete a volume\"), which an AI agent created using \nLangChain and Google's Gemini-2.5 pro model translated into precise OpenStack API calls. \n●\nBuilt an interactive CLI and a web app for remote management, featuring resource analytics and container monitoring per VM. \n●\nDesigned RESTful backend with Fastapi and containerized the application using Docker. \n●\nTechnologies: Python, OpenStack SDK, Gemini, Fastapi, Docker, LangChain \nSignEase -Video calling platform for individuals with disabilities https://github.com/akshit7093/Sign-language-translator.git \n●\nCreated a video chat application using React and Node.js to enable video communication with ASL translation. \n●\nImplemented American Sign Language (ASL) detection using MediaPipe for landmarks and an LSTM network in TensorFlow, \nreaching 89% accuracy on a small dataset of 20 videos. \n●\nReduced latency from 500ms to 180ms using model quantization (TensorFlow Lite) and frame-rate optimization. \n●\nTechnologies: Python, TensorFlow, WebRTC, React, Node.js, MediaPipe. \nEDUCATION \n \nMaharaja Agrasen Institute of Technology\n\n\n\n\n\n\n(June 2022 - June 2026) \n●\nB.Tech. in Computer Science with a specialization in Artificial Intelligence and Data Science \n●\nCGPA:8.96/10 \n\n\n\n\n\n\n\n \n●\nRelevant Coursework: Machine Learning, Data Mining, Image Processing, Data Structures and Algorithms, Computer Networks \nCERTIFICATIONS \n \n●\nData Science (Pwskills) \n●\nMachine Learning and Deep Learning Specialization (Coursera) \n●\nAWS Solutions Architect Virtual Experience Program (Forage) \n●\nIntroduction to Generative AI (Google) \n●\nDevelop GenAI Apps with Gemini and Streamlit (Google) \n●\nPrompt Design in Vertex AI (Google) \nACHIEVEMENTS \n \n●\nWinner – AceCloud X RTDS Hackathon ‘25",
|
| 838 |
+
"full_text_preview": "Akshit Sharma \nFinal‑Year B.Tech (AI & Data Science) | Backend & AI/ML Engineer | Cloud‑Native Systems \nakshitsharma7096@gmail.com/ +91 8810248097/Github / Linkedin / LeetCode / CodeForces \nSKILLS\n \n●\nProgramming Languages: Python, Java, C/C++, JavaScript, SQL, React, Node.js, TypeScript, Flask, FastAPI \n●\nDatabases & Tools: Pandas, NumPy, Matplotlib, MongoDB, Postgre \n●\nML/AI & Frameworks: TensorFlow, PyTorch, NLP, Computer Vision, Transformers, RAG, LangChain \n●\nCloud & DevOps: AWS, Google...",
|
| 839 |
+
"professional_links": [
|
| 840 |
+
"mailto:akshitsharma7096@gmail.com",
|
| 841 |
+
"https://github.com/akshit7093",
|
| 842 |
+
"https://www.linkedin.com/in/akshit-sharma-475a94271/",
|
| 843 |
+
"https://leetcode.com/u/akshitsharma7093/",
|
| 844 |
+
"https://codeforces.com/profile/akshit7093",
|
| 845 |
+
"https://github.com/akshit7093/Chatbot-for-websites",
|
| 846 |
+
"https://github.com/akshit7093/CODSOFT",
|
| 847 |
+
"https://github.com/akshit7093/VM_manager_AgenticAi",
|
| 848 |
+
"https://github.com/akshit7093/Sign-language-translator.git",
|
| 849 |
+
"https://www.cloudskillsboost.google/public_profiles/1b626606-8403-4450-9b1a-dbba876587d7/badges/9194948",
|
| 850 |
+
"https://www.cloudskillsboost.google/public_profiles/1b626606-8403-4450-9b1a-dbba876587d7/badges/9194066",
|
| 851 |
+
"https://www.cloudskillsboost.google/public_profiles/1b626606-8403-4450-9b1a-dbba876587d7/badges/9140322",
|
| 852 |
+
"https://drive.google.com/file/d/1OeO7jFd7le1gg_6I0oBGjmsBIfA50p73/view?usp=sharing"
|
| 853 |
+
],
|
| 854 |
+
"skills_summary": "Akshit Sharma FinalYear B.Tech (AI Data Science) Backend AIML Engineer CloudNative Systems akshitsharma7096gmail.com 91 8810248097Github Linkedin LeetCode CodeForces SKILLS Programming Languages: Python, Java, CC, JavaScript, SQL, React, Node.js, TypeScript, Flask, FastAPI Databases Tools: Pandas, NumPy, Matplotlib, MongoDB, Postgre MLAI Frameworks: TensorFlow, PyTorch, NLP, Computer Vision, Transformers, RAG, LangChain Cloud DevOps: AWS, Google Cloud Platform, OpenStack SDK, Docker, Kubernetes Systems Fundamentals: UnixLinux, TCPIP Networking, Git, Data Structures Algorithms, Computer Networks EXPERIENCE Research Intern Directorate of Research, Government of Arunachal Pradesh (August 2025 Present) Developed a low-resource speech-to-speech translation pipeline using Wav2Vec 2.0 for ASR, MarianMT for NMT, and Tacotron 2 for TTS, focusing on endangered languages with context-dependent meanings. Optimised system architecture to reduce translation latency to under 2 seconds, enabling real time deployment for field use. Deep Learning Intern Akanila Technologies https:github.comakshit7093Chatbot-for-websites (July 2024 December 2024) Developed a universal chatbot platform by finetuning Llama3.1 LLM using LoRA and integrating RAG with FAISS for domainspecific retrieval, boosting query accuracy to 90 and improving response relevance by 25 . Designed a flexible Python...",
|
| 855 |
+
"key_skills": [
|
| 856 |
+
"Python",
|
| 857 |
+
"Java",
|
| 858 |
+
"Javascript",
|
| 859 |
+
"React",
|
| 860 |
+
"Node",
|
| 861 |
+
"Sql",
|
| 862 |
+
"Mongodb",
|
| 863 |
+
"Aws",
|
| 864 |
+
"Docker",
|
| 865 |
+
"Kubernetes",
|
| 866 |
+
"Git",
|
| 867 |
+
"C++",
|
| 868 |
+
"Typescript",
|
| 869 |
+
"Flask",
|
| 870 |
+
"Tensorflow",
|
| 871 |
+
"Pytorch",
|
| 872 |
+
"Data structures",
|
| 873 |
+
"Algorithms",
|
| 874 |
+
"Backend"
|
| 875 |
+
],
|
| 876 |
+
"total_hyperlinks": 13,
|
| 877 |
+
"professional_link_count": 13,
|
| 878 |
+
"missing_elements": []
|
| 879 |
+
},
|
| 880 |
"errors": {}
|
| 881 |
}
|
| 882 |
}
|
job_scraper.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# job_analyzer.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 7 |
+
from langchain_core.prompts import PromptTemplate
|
| 8 |
+
from youtube_search_tool import YouTubeSearchTool
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger('job_analyzer')
|
| 11 |
+
|
| 12 |
+
class JobApplicationAnalyzer:
|
| 13 |
+
def __init__(self):
|
| 14 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 15 |
+
if not api_key:
|
| 16 |
+
raise ValueError("GOOGLE_API_KEY environment variable not set!")
|
| 17 |
+
|
| 18 |
+
self.llm = ChatGoogleGenerativeAI(
|
| 19 |
+
model="models/gemini-flash-latest", # Using a more powerful model for better comparative analysis
|
| 20 |
+
google_api_key=api_key,
|
| 21 |
+
temperature=0.3 # Allowing for slightly more creative and encouraging coaching language
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
self.youtube_tool = YouTubeSearchTool()
|
| 25 |
+
print("Job Application Analyzer initialized successfully.")
|
| 26 |
+
|
| 27 |
+
def analyze(self, job_application_link: str, student_profile: dict) -> dict:
|
| 28 |
+
"""
|
| 29 |
+
Analyzes a student's profile against a job description and provides a
|
| 30 |
+
personalized action plan. This is the new, primary method.
|
| 31 |
+
"""
|
| 32 |
+
print(" > Starting personalized job analysis...")
|
| 33 |
+
|
| 34 |
+
# Convert the student's profile dictionary to a formatted JSON string for the prompt
|
| 35 |
+
student_context = json.dumps(student_profile, indent=2)
|
| 36 |
+
|
| 37 |
+
# Use the new, highly detailed prompt template
|
| 38 |
+
prompt = PromptTemplate(
|
| 39 |
+
template=self.get_analysis_prompt_template(),
|
| 40 |
+
input_variables=["job_application_link", "student_context"]
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
chain = prompt | self.llm
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
response = chain.invoke({
|
| 47 |
+
"job_application_link": job_application_link,
|
| 48 |
+
"student_context": student_context
|
| 49 |
+
})
|
| 50 |
+
response_text = response.content
|
| 51 |
+
|
| 52 |
+
# Clean and parse the JSON response from the LLM
|
| 53 |
+
json_text = self._extract_json(response_text)
|
| 54 |
+
if not json_text:
|
| 55 |
+
logger.error("No JSON object found in the LLM response for job analysis.")
|
| 56 |
+
return self._get_default_analysis("Failed to extract JSON from LLM response.")
|
| 57 |
+
|
| 58 |
+
analysis_data = json.loads(json_text)
|
| 59 |
+
|
| 60 |
+
# Enhance the analysis with YouTube recommendations based on the AI's suggestions
|
| 61 |
+
if "strategic_areas_for_growth" in analysis_data:
|
| 62 |
+
print(" > Generating YouTube recommendations for growth areas...")
|
| 63 |
+
# Create a new key for recommendations to match the desired output format
|
| 64 |
+
analysis_data["video_recommendations"] = []
|
| 65 |
+
for area in analysis_data.get("strategic_areas_for_growth", []):
|
| 66 |
+
# Use the concise search query provided by the LLM to avoid errors
|
| 67 |
+
search_query = area.get("youtube_search_query")
|
| 68 |
+
if not search_query:
|
| 69 |
+
logger.warning(f"No youtube_search_query found for growth area: {area.get('area_to_develop')}")
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
+
category = self._determine_skill_category(search_query)
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
videos = self.youtube_tool.run({
|
| 76 |
+
"query": search_query,
|
| 77 |
+
"max_results": 3,
|
| 78 |
+
"topic_category": category
|
| 79 |
+
})
|
| 80 |
+
|
| 81 |
+
formatted_videos = [{
|
| 82 |
+
"title": v.get("title", "N/A"),
|
| 83 |
+
"url": v.get("url"),
|
| 84 |
+
"embed_url": v.get("embed_url"),
|
| 85 |
+
"reason": v.get("description", "A recommended video to help you learn this topic.")
|
| 86 |
+
} for v in videos]
|
| 87 |
+
|
| 88 |
+
# Add to the new recommendations list
|
| 89 |
+
analysis_data["video_recommendations"].append({
|
| 90 |
+
"topic": area.get("area_to_develop"),
|
| 91 |
+
"reason": f"This is a key area for you to focus on to better match the job requirements.",
|
| 92 |
+
"category": category,
|
| 93 |
+
"videos": formatted_videos
|
| 94 |
+
})
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error(f"Error getting videos for topic '{search_query}': {e}")
|
| 97 |
+
|
| 98 |
+
return analysis_data
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
logger.error(f"An error occurred during job application analysis: {e}", exc_info=True)
|
| 102 |
+
return self._get_default_analysis(str(e))
|
| 103 |
+
|
| 104 |
+
def _extract_json(self, text: str) -> str:
|
| 105 |
+
"""Safely extracts a JSON object from a string that might contain other text."""
|
| 106 |
+
try:
|
| 107 |
+
# Find the first '{' and the last '}' to isolate the JSON object
|
| 108 |
+
start_index = text.find('{')
|
| 109 |
+
end_index = text.rfind('}') + 1
|
| 110 |
+
if start_index != -1 and end_index != 0:
|
| 111 |
+
return text[start_index:end_index]
|
| 112 |
+
except Exception:
|
| 113 |
+
return None
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
def get_analysis_prompt_template(self) -> str:
|
| 117 |
+
"""
|
| 118 |
+
Returns the new, detailed prompt that forces a personalized, comparative analysis.
|
| 119 |
+
"""
|
| 120 |
+
return """
|
| 121 |
+
**Your Persona:** You are a world-class Senior Career Strategist from Google. You are a mentor speaking directly and encouragingly to a student, using "you" and "your". Your advice is insightful, strategic, and hyper-personalized.
|
| 122 |
+
|
| 123 |
+
**Your Mission:** Analyze the provided student's profile against the requirements of the job description. Your output MUST BE a direct, comparative analysis, creating a personalized action plan to help the student land this specific job. You will not speak about a generic candidate; you will speak directly about the student's provided data.
|
| 124 |
+
|
| 125 |
+
**1. Student's Profile (Context):**
|
| 126 |
+
```json
|
| 127 |
+
{student_context}
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
**2. Job Description Link:**
|
| 131 |
+
{job_application_link}
|
| 132 |
+
|
| 133 |
+
**Your Step-by-Step Thinking Process (Internal Monologue):**
|
| 134 |
+
1. **Deconstruct the Job:** I will identify the top 5 'must-have' technical and soft skills from the job description (e.g., BigQuery, Dataflow, customer-facing skills).
|
| 135 |
+
2. **Analyze the Student:** I will thoroughly review the student's resume, projects, and coding stats. I will note the specific technologies they've used (e.g., Python, SQL, React) and the outcomes of their projects.
|
| 136 |
+
3. **Perform a Comparative Gap Analysis:** This is the most critical step. I will compare the student's specific skills and projects to the job's requirements.
|
| 137 |
+
- I will find direct evidence in their profile that matches the job (e.g., "Your project 'EcoSort' uses Python and computer vision, which directly aligns with the job's need for ML skills.").
|
| 138 |
+
- I will identify the most critical gaps (e.g., "The job requires Google Cloud experience, but your resume and projects only list AWS. This is your main gap to address.").
|
| 139 |
+
4. **Craft the Action Plan:** I will translate this direct comparison into the structured JSON output below, ensuring every point refers back to the student's profile and the job description.
|
| 140 |
+
|
| 141 |
+
**The Output: Your Personalized Action Plan (JSON Format)**
|
| 142 |
+
Provide your response ONLY in the valid JSON format below. Do not include any text before or after the JSON block.
|
| 143 |
+
|
| 144 |
+
{{
|
| 145 |
+
"strategic_overview": {{
|
| 146 |
+
"summary": "Start with an encouraging summary directly referencing the student's background. e.g., 'With your background in [Student's Major/Key Skill], this role is a great potential fit. We need to focus on showcasing how your projects align with their needs and strategically build up your Google Cloud expertise.'",
|
| 147 |
+
"your_key_opportunity": "Identify the single most important thing for you to do. e.g., 'Your biggest opportunity is to frame your AWS project experience as cloud-agnostic engineering excellence, while rapidly learning the GCP specifics.'"
|
| 148 |
+
}},
|
| 149 |
+
"your_core_strengths_for_this_role": [
|
| 150 |
+
{{
|
| 151 |
+
"strength_area": "Reference a specific skill or project from the student's profile. e.g., 'Python and Data Manipulation Skills'",
|
| 152 |
+
"evidence_from_your_profile": "Quote or describe the evidence from the student's JSON data. e.g., 'Your 'Data-Driven-Dialogue' project on GitHub shows strong proficiency in Python with Pandas and Scikit-learn.'",
|
| 153 |
+
"how_it_matches_the_job": "Explain precisely how this evidence meets a key requirement from the job description. e.g., 'This is crucial for the role, which requires scripting and building data prototypes to solve customer problems.'"
|
| 154 |
+
}}
|
| 155 |
+
],
|
| 156 |
+
"strategic_areas_for_growth": [
|
| 157 |
+
{{
|
| 158 |
+
"area_to_develop": "Identify a specific missing skill or experience. e.g., 'Hands-On Google Cloud Data Stack Expertise'",
|
| 159 |
+
"severity": "Categorize as 'Critical Gap', 'High-Impact Area', or 'Nice-to-Have'.",
|
| 160 |
+
"insight": "Explain why this is a gap by comparing their profile to the job description. e.g., 'The role is laser-focused on the GCP ecosystem. While your foundational data skills are strong, your resume does not mention hands-on experience with BigQuery or Dataflow, which are core requirements.'",
|
| 161 |
+
"path_to_improvement": [
|
| 162 |
+
"1. **Certify:** Rapidly study for and pass the Google Cloud Professional Data Engineer certification. This is the strongest signal you can send.",
|
| 163 |
+
"2. **Build & Showcase:** Create a small, end-to-end project using Pub/Sub, Dataflow, and BigQuery and feature it prominently on your GitHub and resume."
|
| 164 |
+
],
|
| 165 |
+
"youtube_search_query": "Provide a short, effective search query (3-5 words) for this topic. e.g., 'Google Cloud Dataflow tutorial' or 'Technical presentation skills for engineers'"
|
| 166 |
+
}}
|
| 167 |
+
]
|
| 168 |
+
}}
|
| 169 |
+
"""
|
| 170 |
+
|
| 171 |
+
def _determine_skill_category(self, skill: str) -> str:
|
| 172 |
+
"""Determines the category for a skill, optimized for short search queries."""
|
| 173 |
+
skill_lower = skill.lower()
|
| 174 |
+
if any(kw in skill_lower for kw in ["cloud", "aws", "azure", "gcp", "docker", "bigquery", "dataflow"]):
|
| 175 |
+
return "Cloud Computing"
|
| 176 |
+
if any(kw in skill_lower for kw in ["python", "java", "sql", "javascript", "c++"]):
|
| 177 |
+
return "Programming Languages"
|
| 178 |
+
if any(kw in skill_lower for kw in ["ml", "ai", "machine learning", "vertex"]):
|
| 179 |
+
return "Machine Learning"
|
| 180 |
+
if any(kw in skill_lower for kw in ["presentation", "communication", "soft skills", "customer"]):
|
| 181 |
+
return "Soft Skills"
|
| 182 |
+
return "Computer Science Fundamentals"
|
| 183 |
+
|
| 184 |
+
def _get_default_analysis(self, error_message: str) -> dict:
|
| 185 |
+
"""Returns a default, structured analysis in case of a processing error."""
|
| 186 |
+
logger.warning(f"Using default job analysis due to processing error: {error_message}")
|
| 187 |
+
return {
|
| 188 |
+
"strategic_overview": {
|
| 189 |
+
"summary": "There was an error while generating your personalized analysis. Please try again.",
|
| 190 |
+
"your_key_opportunity": "Please ensure the job link is active and publicly accessible."
|
| 191 |
+
},
|
| 192 |
+
"your_core_strengths_for_this_role": [],
|
| 193 |
+
"strategic_areas_for_growth": [
|
| 194 |
+
{
|
| 195 |
+
"area_to_develop": "System Processing Error",
|
| 196 |
+
"severity": "Critical Gap",
|
| 197 |
+
"insight": f"The analysis could not be completed due to a system error: {error_message}",
|
| 198 |
+
"path_to_improvement": ["Please try your request again in a few moments."],
|
| 199 |
+
"youtube_search_query": "Fixing application errors"
|
| 200 |
+
}
|
| 201 |
+
],
|
| 202 |
+
"video_recommendations": []
|
| 203 |
+
}
|
prompts.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
# prompts.py (Updated with
|
| 2 |
|
| 3 |
from langchain.prompts import PromptTemplate
|
| 4 |
-
from
|
| 5 |
-
from typing import List
|
| 6 |
|
| 7 |
-
# --- Pydantic Models for Structured Report Output
|
| 8 |
|
| 9 |
class ScoreMetric(BaseModel):
|
| 10 |
parameter: str = Field(description="The name of the parameter being scored, e.g., 'Problem Volume'.")
|
|
@@ -18,28 +18,45 @@ class StrengthWeakness(BaseModel):
|
|
| 18 |
class Recommendation(BaseModel):
|
| 19 |
recommendations: List[str] = Field(description="A list of 2-3 actionable, personalized recommendations for the student.")
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
class StudentReport(BaseModel):
|
| 22 |
"""The complete structured report for a student."""
|
| 23 |
overall_summary: str = Field(description="A one-paragraph 'HR Summary' of the student's overall profile.")
|
| 24 |
detailed_scores: List[ScoreMetric] = Field(description="A list of scores for each parameter.")
|
| 25 |
analysis: StrengthWeakness = Field(description="An analysis of strengths and weaknesses.")
|
| 26 |
actionable_advice: Recommendation = Field(description="Personalized advice for improvement.")
|
|
|
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
# --- Prompt Templates ---
|
| 30 |
|
| 31 |
-
# --- UPDATED REPORT PROMPT TEMPLATE ---
|
| 32 |
REPORT_PROMPT_TEMPLATE = """
|
| 33 |
-
You are an expert AI career coach. Your task is to generate a performance report for a student based on the provided JSON data.
|
| 34 |
|
| 35 |
**Student Profile Data:**
|
| 36 |
{context}
|
| 37 |
|
| 38 |
**Your Instructions:**
|
| 39 |
-
1.
|
| 40 |
-
2.
|
| 41 |
-
3.
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
**Scoring Rubric:**
|
| 45 |
- **Problem Volume (20%):** Score based on total problems solved (LeetCode, Codeforces).
|
|
@@ -50,6 +67,18 @@ You are an expert AI career coach. Your task is to generate a performance report
|
|
| 50 |
- **Programming Language Skill (5%):** Score based on primary language and versatility.
|
| 51 |
- **Recent Activity (5%):** Score based on recent submissions and commits.
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
**Output Format Instructions:**
|
| 54 |
{format_instructions}
|
| 55 |
"""
|
|
|
|
| 1 |
+
# prompts.py (Updated with resume analysis and YouTube recommendations)
|
| 2 |
|
| 3 |
from langchain.prompts import PromptTemplate
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import List, Dict, Optional
|
| 6 |
|
| 7 |
+
# --- Pydantic Models for Structured Report Output ---
|
| 8 |
|
| 9 |
class ScoreMetric(BaseModel):
|
| 10 |
parameter: str = Field(description="The name of the parameter being scored, e.g., 'Problem Volume'.")
|
|
|
|
| 18 |
class Recommendation(BaseModel):
|
| 19 |
recommendations: List[str] = Field(description="A list of 2-3 actionable, personalized recommendations for the student.")
|
| 20 |
|
| 21 |
+
class ResumeAnalysis(BaseModel):
|
| 22 |
+
summary: str = Field(description="Concise summary of the resume content")
|
| 23 |
+
key_skills: List[str] = Field(description="Key technical and soft skills identified from the resume")
|
| 24 |
+
professional_links: List[str] = Field(description="Professional links found in the resume (GitHub, LinkedIn, portfolio)")
|
| 25 |
+
missing_elements: List[str] = Field(description="Important elements missing from the resume")
|
| 26 |
+
|
| 27 |
+
class YouTubeRecommendation(BaseModel):
|
| 28 |
+
title: str = Field(description="Title of the YouTube video")
|
| 29 |
+
url: str = Field(description="URL of the YouTube video")
|
| 30 |
+
reason: str = Field(description="Why this video is recommended for the student")
|
| 31 |
+
embed_url: str = Field(description="Embed URL for the video (convert standard URL to embed format)")
|
| 32 |
+
|
| 33 |
class StudentReport(BaseModel):
|
| 34 |
"""The complete structured report for a student."""
|
| 35 |
overall_summary: str = Field(description="A one-paragraph 'HR Summary' of the student's overall profile.")
|
| 36 |
detailed_scores: List[ScoreMetric] = Field(description="A list of scores for each parameter.")
|
| 37 |
analysis: StrengthWeakness = Field(description="An analysis of strengths and weaknesses.")
|
| 38 |
actionable_advice: Recommendation = Field(description="Personalized advice for improvement.")
|
| 39 |
+
resume_analysis: ResumeAnalysis = Field(description="Analysis of the student's resume")
|
| 40 |
+
youtube_recommendations: List[YouTubeRecommendation] = Field(description="Recommended YouTube videos for improvement")
|
| 41 |
|
| 42 |
|
| 43 |
# --- Prompt Templates ---
|
| 44 |
|
|
|
|
| 45 |
REPORT_PROMPT_TEMPLATE = """
|
| 46 |
+
You are an expert AI career coach. Your task is to generate a comprehensive performance report for a student based on the provided JSON data, including their resume analysis.
|
| 47 |
|
| 48 |
**Student Profile Data:**
|
| 49 |
{context}
|
| 50 |
|
| 51 |
**Your Instructions:**
|
| 52 |
+
1. Analyze all provided data thoroughly, including the resume analysis section.
|
| 53 |
+
2. Adhere strictly to the scoring rubric below to evaluate the student.
|
| 54 |
+
3. For YouTube recommendations, provide 3-5 highly relevant videos that address the student's specific weaknesses or enhance their strengths.
|
| 55 |
+
- Convert standard YouTube URLs to embed format (replace 'watch?v=' with 'embed/')
|
| 56 |
+
- Focus on high-quality, educational content from reputable channels
|
| 57 |
+
- Prioritize recent videos (within last 2 years) for technical topics
|
| 58 |
+
4. You MUST output a single, valid JSON object that conforms to the schema provided.
|
| 59 |
+
5. DO NOT output any text, explanation, or markdown before or after the JSON object. Your entire response must be only the JSON.
|
| 60 |
|
| 61 |
**Scoring Rubric:**
|
| 62 |
- **Problem Volume (20%):** Score based on total problems solved (LeetCode, Codeforces).
|
|
|
|
| 67 |
- **Programming Language Skill (5%):** Score based on primary language and versatility.
|
| 68 |
- **Recent Activity (5%):** Score based on recent submissions and commits.
|
| 69 |
|
| 70 |
+
**Resume Analysis Guidelines:**
|
| 71 |
+
- Extract key skills mentioned in the resume
|
| 72 |
+
- Identify professional links (GitHub, LinkedIn, portfolio)
|
| 73 |
+
- Note any important elements missing (projects, education details, etc.)
|
| 74 |
+
- Compare resume content with coding profiles for consistency
|
| 75 |
+
|
| 76 |
+
**YouTube Recommendation Guidelines:**
|
| 77 |
+
- Match videos to specific weaknesses identified in the analysis
|
| 78 |
+
- Include videos that build on existing strengths
|
| 79 |
+
- Provide clear reasoning for each recommendation
|
| 80 |
+
- Format URLs correctly for embedding (https://www.youtube.com/embed/VIDEO_ID)
|
| 81 |
+
|
| 82 |
**Output Format Instructions:**
|
| 83 |
{format_instructions}
|
| 84 |
"""
|
rag_system.py
CHANGED
|
@@ -1,11 +1,19 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 4 |
-
from langchain_core.output_parsers import JsonOutputParser
|
| 5 |
from prompts import REPORT_PROMPT, QA_PROMPT, StudentReport
|
| 6 |
import json
|
|
|
|
| 7 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
|
|
|
| 9 |
DATA_PATH = "final_cleaned_student_data.json"
|
| 10 |
|
| 11 |
class StudentApiRAG:
|
|
@@ -14,66 +22,365 @@ class StudentApiRAG:
|
|
| 14 |
api_key = os.getenv("GOOGLE_API_KEY")
|
| 15 |
if not api_key:
|
| 16 |
raise ValueError("GOOGLE_API_KEY environment variable not set!")
|
| 17 |
-
|
| 18 |
self.llm = ChatGoogleGenerativeAI(
|
| 19 |
-
model="models/gemini-
|
| 20 |
google_api_key=api_key,
|
| 21 |
temperature=0.2
|
| 22 |
)
|
| 23 |
-
|
|
|
|
| 24 |
print("Loading student data into memory...")
|
| 25 |
with open(DATA_PATH, 'r', encoding='utf-8') as f:
|
| 26 |
self.student_data = json.load(f)
|
| 27 |
print(f"✅ Loaded data for {len(self.student_data)} students. System ready.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def _determine_sources_from_query(self, query: str) -> list:
|
| 30 |
-
# This function remains the same
|
| 31 |
query = query.lower()
|
| 32 |
sources = []
|
| 33 |
-
if any(keyword in query for keyword in ["dsa", "problem solving", "coding", "leetcode", "codeforces"]):
|
| 34 |
-
sources.extend(["leetcode", "codeforces"])
|
| 35 |
if any(keyword in query for keyword in ["project", "experience", "github", "code", "repository"]):
|
| 36 |
sources.append("github")
|
| 37 |
if any(keyword in query for keyword in ["academic", "grade", "gpa", "cgpa", "subject", "marks", "semester"]):
|
| 38 |
sources.append("academic_profile")
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
def generate_structured_report(self, enrollment_no: str) -> dict:
|
| 43 |
-
"""Generates the full, structured student report via API call."""
|
| 44 |
print(f"Generating full report for {enrollment_no}...")
|
| 45 |
-
|
| 46 |
student_profile = self.student_data.get(enrollment_no)
|
| 47 |
if not student_profile:
|
| 48 |
return {"error": "No data found for this student."}
|
| 49 |
-
|
| 50 |
context = json.dumps(student_profile, indent=2)
|
| 51 |
-
|
| 52 |
-
# --- PARSING FIX ---
|
| 53 |
-
# Use JsonOutputParser, which is more robust for this task.
|
| 54 |
-
# We pass the Pydantic model to it so it knows what to expect.
|
| 55 |
parser = JsonOutputParser(pydantic_object=StudentReport)
|
| 56 |
-
|
| 57 |
-
# Update the prompt to include the parser's format instructions
|
| 58 |
prompt_with_format = REPORT_PROMPT.partial(
|
| 59 |
format_instructions=parser.get_format_instructions()
|
| 60 |
)
|
| 61 |
-
|
| 62 |
-
# Use the new LangChain Expression Language (LCEL) syntax
|
| 63 |
chain = prompt_with_format | self.llm | parser
|
| 64 |
-
|
| 65 |
try:
|
| 66 |
-
#
|
| 67 |
report_dict = chain.invoke({"context": context})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
return report_dict
|
| 69 |
except Exception as e:
|
| 70 |
print(f"Error invoking LLM or parsing output: {e}")
|
| 71 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
def answer_question(self, query: str, enrollment_no: str) -> str:
|
| 74 |
"""Answers a specific question using a targeted context from the JSON."""
|
| 75 |
print(f"Answering question for {enrollment_no}: '{query}'")
|
| 76 |
-
|
| 77 |
student_profile = self.student_data.get(enrollment_no)
|
| 78 |
if not student_profile:
|
| 79 |
return "Could not find data for the selected student."
|
|
@@ -84,7 +391,6 @@ class StudentApiRAG:
|
|
| 84 |
targeted_context = {}
|
| 85 |
if "academic_profile" in sources_to_use:
|
| 86 |
targeted_context["academic_profile"] = student_profile.get("academic_profile")
|
| 87 |
-
|
| 88 |
coding_profiles = {}
|
| 89 |
if "leetcode" in sources_to_use:
|
| 90 |
coding_profiles["leetcode"] = student_profile.get("coding_profiles", {}).get("leetcode")
|
|
@@ -92,19 +398,17 @@ class StudentApiRAG:
|
|
| 92 |
coding_profiles["github"] = student_profile.get("coding_profiles", {}).get("github")
|
| 93 |
if "codeforces" in sources_to_use:
|
| 94 |
coding_profiles["codeforces"] = student_profile.get("coding_profiles", {}).get("codeforces")
|
| 95 |
-
|
| 96 |
if coding_profiles:
|
| 97 |
targeted_context["coding_profiles"] = coding_profiles
|
| 98 |
-
|
| 99 |
if "coding_profiles" in sources_to_use and not coding_profiles:
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
|
| 102 |
if not targeted_context:
|
| 103 |
return "I could not find any relevant information in this student's profile to answer that question."
|
| 104 |
|
| 105 |
context_str = json.dumps(targeted_context, indent=2)
|
| 106 |
-
|
| 107 |
chain = QA_PROMPT | self.llm
|
| 108 |
result = chain.invoke({"context": context_str, "question": query})
|
| 109 |
-
|
| 110 |
-
return result.content
|
|
|
|
| 1 |
+
# rag.py
|
|
|
|
| 2 |
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 3 |
+
from langchain_core.output_parsers import JsonOutputParser
|
| 4 |
from prompts import REPORT_PROMPT, QA_PROMPT, StudentReport
|
| 5 |
import json
|
| 6 |
+
from langchain_core.prompts import PromptTemplate
|
| 7 |
import os
|
| 8 |
+
import re
|
| 9 |
+
import logging
|
| 10 |
+
from youtube_search_tool import YouTubeSearchTool
|
| 11 |
+
# Import the new JobApplicationAnalyzer
|
| 12 |
+
from job_scraper import JobApplicationAnalyzer # Corrected import name
|
| 13 |
+
# Import the new dashboard analyzer logic
|
| 14 |
+
from dashboard_analyzer import get_dashboard_metrics # Import the main function
|
| 15 |
|
| 16 |
+
logger = logging.getLogger('rag_system')
|
| 17 |
DATA_PATH = "final_cleaned_student_data.json"
|
| 18 |
|
| 19 |
class StudentApiRAG:
|
|
|
|
| 22 |
api_key = os.getenv("GOOGLE_API_KEY")
|
| 23 |
if not api_key:
|
| 24 |
raise ValueError("GOOGLE_API_KEY environment variable not set!")
|
|
|
|
| 25 |
self.llm = ChatGoogleGenerativeAI(
|
| 26 |
+
model="models/gemini-flash-latest", # Updated model name if needed
|
| 27 |
google_api_key=api_key,
|
| 28 |
temperature=0.2
|
| 29 |
)
|
| 30 |
+
# Initialize the YouTube search tool
|
| 31 |
+
self.youtube_tool = YouTubeSearchTool()
|
| 32 |
print("Loading student data into memory...")
|
| 33 |
with open(DATA_PATH, 'r', encoding='utf-8') as f:
|
| 34 |
self.student_data = json.load(f)
|
| 35 |
print(f"✅ Loaded data for {len(self.student_data)} students. System ready.")
|
| 36 |
+
|
| 37 |
+
# Initialize the new JobApplicationAnalyzer
|
| 38 |
+
self.job_analyzer = JobApplicationAnalyzer()
|
| 39 |
+
|
| 40 |
+
# Define topic categories for better search organization (kept for potential other uses)
|
| 41 |
+
self.topic_categories = {
|
| 42 |
+
"DSA": [
|
| 43 |
+
"Arrays", "Strings", "Linked Lists", "Stacks", "Queues",
|
| 44 |
+
"Trees", "Graphs", "Heaps", "Hashing", "Binary Search",
|
| 45 |
+
"Dynamic Programming", "Greedy Algorithms", "Backtracking",
|
| 46 |
+
"Bit Manipulation", "Math", "Sorting", "Searching", "AIDS303", "AIDS353"
|
| 47 |
+
],
|
| 48 |
+
"Web Development": [
|
| 49 |
+
"HTML", "CSS", "JavaScript", "React", "Angular", "Vue",
|
| 50 |
+
"Node.js", "Express", "Django", "Flask", "REST APIs",
|
| 51 |
+
"TypeScript", "Webpack", "Babel", "CSS Frameworks"
|
| 52 |
+
],
|
| 53 |
+
"Programming Languages": [
|
| 54 |
+
"Python", "Java", "C++", "C#", "JavaScript", "TypeScript",
|
| 55 |
+
"Go", "Rust", "Ruby", "PHP", "Swift", "Kotlin"
|
| 56 |
+
],
|
| 57 |
+
"Computer Science Fundamentals": [
|
| 58 |
+
"Operating Systems", "Computer Networks", "Database Systems",
|
| 59 |
+
"Compilers", "Computer Architecture", "Distributed Systems",
|
| 60 |
+
"Artificial Intelligence", "Machine Learning", "Data Science",
|
| 61 |
+
"Cloud Computing", "Cybersecurity"
|
| 62 |
+
]
|
| 63 |
+
}
|
| 64 |
|
| 65 |
def _determine_sources_from_query(self, query: str) -> list:
|
|
|
|
| 66 |
query = query.lower()
|
| 67 |
sources = []
|
| 68 |
+
if any(keyword in query for keyword in ["dsa", "problem solving", "coding", "leetcode", "codeforces", "resume", "cv", "skills", "video", "youtube", "tutorial"]):
|
| 69 |
+
sources.extend(["leetcode", "codeforces", "resume"])
|
| 70 |
if any(keyword in query for keyword in ["project", "experience", "github", "code", "repository"]):
|
| 71 |
sources.append("github")
|
| 72 |
if any(keyword in query for keyword in ["academic", "grade", "gpa", "cgpa", "subject", "marks", "semester"]):
|
| 73 |
sources.append("academic_profile")
|
| 74 |
+
return list(set(sources)) if sources else ["academic_profile", "coding_profiles", "resume"]
|
| 75 |
+
|
| 76 |
+
def _identify_learning_topics(self, student_report: dict) -> list:
|
| 77 |
+
"""Have the AI identify specific topic areas where the student needs improvement."""
|
| 78 |
+
print(" > Identifying specific learning topics for YouTube recommendations...")
|
| 79 |
+
# Extract relevant information from the report
|
| 80 |
+
weaknesses = student_report.get("analysis", {}).get("weaknesses", [])
|
| 81 |
+
strengths = student_report.get("analysis", {}).get("strengths", [])
|
| 82 |
+
# Get scores from the report
|
| 83 |
+
dev_orientation_score = 5
|
| 84 |
+
dsa_orientation_score = 5
|
| 85 |
+
# Try to extract scores from detailed_scores if available
|
| 86 |
+
for score in student_report.get("detailed_scores", []):
|
| 87 |
+
if "Development" in score["parameter"] or "Project" in score["parameter"]:
|
| 88 |
+
dev_orientation_score = score["score"]
|
| 89 |
+
if "DSA" in score["parameter"] or "Problem" in score["parameter"]:
|
| 90 |
+
dsa_orientation_score = score["score"]
|
| 91 |
+
# Create a more robust prompt template
|
| 92 |
+
prompt_template = """
|
| 93 |
+
Analyze this student's academic and coding profile to identify 3-5 specific topic areas
|
| 94 |
+
where they need improvement. Focus on concrete, actionable topics that have dedicated
|
| 95 |
+
learning resources on YouTube.
|
| 96 |
+
Student Profile:
|
| 97 |
+
- DSA Score: {dsa_orientation_score}/10
|
| 98 |
+
- Development/Project Score: {dev_orientation_score}/10
|
| 99 |
+
- Strengths: {strengths}
|
| 100 |
+
- Weaknesses: {weaknesses}
|
| 101 |
+
Identify specific topic areas where the student needs improvement. For each topic:
|
| 102 |
+
1. Provide a concise, specific topic name (e.g., "Binary Search", "React Hooks", "SQL Joins")
|
| 103 |
+
2. Explain why this topic is important for the student
|
| 104 |
+
3. Ensure the topic is narrow enough to have dedicated YouTube tutorials
|
| 105 |
+
Return ONLY a valid JSON array in this exact format:
|
| 106 |
+
[
|
| 107 |
+
{{
|
| 108 |
+
"topic": "Binary Search",
|
| 109 |
+
"reason": "The student struggles with searching algorithms and needs to understand binary search for efficient problem solving."
|
| 110 |
+
}},
|
| 111 |
+
{{
|
| 112 |
+
"topic": "React State Management",
|
| 113 |
+
"reason": "The student's projects show difficulty managing component state in complex UIs."
|
| 114 |
+
}}
|
| 115 |
+
]
|
| 116 |
+
Make sure your JSON is properly formatted with double quotes around all keys and string values.
|
| 117 |
+
"""
|
| 118 |
+
try:
|
| 119 |
+
# Create a chain to get the topic recommendations
|
| 120 |
+
chain = PromptTemplate(
|
| 121 |
+
template=prompt_template,
|
| 122 |
+
input_variables=["dsa_orientation_score", "dev_orientation_score", "strengths", "weaknesses"]
|
| 123 |
+
) | self.llm
|
| 124 |
+
# Invoke the chain with the actual values
|
| 125 |
+
response = chain.invoke({
|
| 126 |
+
"dsa_orientation_score": dsa_orientation_score,
|
| 127 |
+
"dev_orientation_score": dev_orientation_score,
|
| 128 |
+
"strengths": ', '.join(strengths) if strengths else 'None specifically identified',
|
| 129 |
+
"weaknesses": ', '.join(weaknesses) if weaknesses else 'None specifically identified'
|
| 130 |
+
})
|
| 131 |
+
response_text = response.content
|
| 132 |
+
# Try to extract JSON from the response
|
| 133 |
+
json_start = response_text.find('[')
|
| 134 |
+
json_end = response_text.rfind(']') + 1
|
| 135 |
+
if json_start == -1 or json_end == 0:
|
| 136 |
+
logger.error("No JSON array found in topic identification response")
|
| 137 |
+
logger.debug(f"Response text: {response_text}")
|
| 138 |
+
return self._get_default_topics()
|
| 139 |
+
json_text = response_text[json_start:json_end]
|
| 140 |
+
# Clean up common JSON issues
|
| 141 |
+
json_text = json_text.replace('\n', ' ').replace('\r', '')
|
| 142 |
+
try:
|
| 143 |
+
# Parse JSON
|
| 144 |
+
topics_data = json.loads(json_text)
|
| 145 |
+
except json.JSONDecodeError as e:
|
| 146 |
+
logger.error(f"Failed to parse topic identification as JSON: {e}")
|
| 147 |
+
logger.debug(f"JSON text: {json_text}")
|
| 148 |
+
# Try to fix common JSON issues
|
| 149 |
+
try:
|
| 150 |
+
# Replace single quotes with double quotes
|
| 151 |
+
fixed_json = json_text.replace("'", '"')
|
| 152 |
+
topics_data = json.loads(fixed_json)
|
| 153 |
+
except:
|
| 154 |
+
# If still fails, return default topics
|
| 155 |
+
return self._get_default_topics()
|
| 156 |
+
if not isinstance(topics_data, list):
|
| 157 |
+
logger.error("Topic identification response is not a list")
|
| 158 |
+
return self._get_default_topics()
|
| 159 |
+
# Validate and clean the topics
|
| 160 |
+
valid_topics = []
|
| 161 |
+
for item in topics_data[:5]: # Limit to 5 topics
|
| 162 |
+
topic = item.get("topic", "").strip()
|
| 163 |
+
reason = item.get("reason", "").strip()
|
| 164 |
+
if topic and reason:
|
| 165 |
+
valid_topics.append({
|
| 166 |
+
"topic": topic,
|
| 167 |
+
"reason": reason,
|
| 168 |
+
"category": self._determine_topic_category(topic)
|
| 169 |
+
})
|
| 170 |
+
if not valid_topics:
|
| 171 |
+
logger.warning("No valid topics identified, using defaults")
|
| 172 |
+
return self._get_default_topics()
|
| 173 |
+
print(f" > Identified {len(valid_topics)} specific learning topics.")
|
| 174 |
+
return valid_topics
|
| 175 |
+
except Exception as e:
|
| 176 |
+
logger.error(f"Error identifying learning topics: {e}")
|
| 177 |
+
return self._get_default_topics()
|
| 178 |
+
|
| 179 |
+
def _determine_topic_category(self, topic: str) -> str:
|
| 180 |
+
"""Determine the most appropriate category for a topic."""
|
| 181 |
+
topic_lower = topic.lower()
|
| 182 |
+
# Check against our predefined categories
|
| 183 |
+
for category, topics in self.topic_categories.items():
|
| 184 |
+
for predefined_topic in topics:
|
| 185 |
+
if predefined_topic.lower() in topic_lower or topic_lower in predefined_topic.lower():
|
| 186 |
+
return category
|
| 187 |
+
# Fallback categories based on keywords
|
| 188 |
+
if any(kw in topic_lower for kw in ["algorithm", "data structure", "dsa", "binary", "dynamic", "greedy", "tree", "graph", "array", "string"]):
|
| 189 |
+
return "DSA"
|
| 190 |
+
elif any(kw in topic_lower for kw in ["web", "react", "angular", "vue", "node", "express", "api", "html", "css", "javascript"]):
|
| 191 |
+
return "Web Development"
|
| 192 |
+
elif any(kw in topic_lower for kw in ["python", "java", "c++", "c#", "javascript", "go", "rust"]):
|
| 193 |
+
return "Programming Languages"
|
| 194 |
+
return "Computer Science Fundamentals"
|
| 195 |
+
|
| 196 |
+
def _get_default_topics(self) -> list:
|
| 197 |
+
"""Return default topics in case of errors."""
|
| 198 |
+
return [
|
| 199 |
+
{
|
| 200 |
+
"topic": "Binary Search",
|
| 201 |
+
"reason": "Essential searching algorithm that forms the basis for many problem-solving techniques",
|
| 202 |
+
"category": "DSA"
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"topic": "Dynamic Programming",
|
| 206 |
+
"reason": "Fundamental technique for solving optimization problems with overlapping subproblems",
|
| 207 |
+
"category": "DSA"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"topic": "React Fundamentals",
|
| 211 |
+
"reason": "Core concepts for building modern web applications with component-based architecture",
|
| 212 |
+
"category": "Web Development"
|
| 213 |
+
}
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
def _get_youtube_recommendations(self, student_report: dict) -> list:
|
| 217 |
+
"""Generate real YouTube video recommendations based on specific learning topics."""
|
| 218 |
+
print(" > Generating topic-based YouTube recommendations...")
|
| 219 |
+
# First, identify specific learning topics
|
| 220 |
+
learning_topics = self._identify_learning_topics(student_report)
|
| 221 |
+
# Now search for videos for each topic
|
| 222 |
+
topic_recommendations = []
|
| 223 |
+
for topic_info in learning_topics:
|
| 224 |
+
topic = topic_info["topic"]
|
| 225 |
+
category = topic_info["category"]
|
| 226 |
+
print(f" > Searching for videos on topic: '{topic}' (category: {category})")
|
| 227 |
+
try:
|
| 228 |
+
# Search YouTube for this specific topic
|
| 229 |
+
youtube_videos = self.youtube_tool.run({
|
| 230 |
+
"query": topic,
|
| 231 |
+
"max_results": 5,
|
| 232 |
+
"topic_category": category
|
| 233 |
+
})
|
| 234 |
+
# Format the videos for this topic
|
| 235 |
+
topic_videos = [{
|
| 236 |
+
"title": video["title"],
|
| 237 |
+
"url": video["url"],
|
| 238 |
+
"embed_url": video["embed_url"],
|
| 239 |
+
"reason": video["description"]
|
| 240 |
+
} for video in youtube_videos]
|
| 241 |
+
# Add to recommendations
|
| 242 |
+
topic_recommendations.append({
|
| 243 |
+
"topic": topic,
|
| 244 |
+
"reason": topic_info["reason"],
|
| 245 |
+
"category": category,
|
| 246 |
+
"videos": topic_videos
|
| 247 |
+
})
|
| 248 |
+
print(f" > Found {len(topic_videos)} videos for topic '{topic}'")
|
| 249 |
+
except Exception as e:
|
| 250 |
+
print(f" > Warning: Failed to get videos for topic '{topic}': {e}")
|
| 251 |
+
# Add fallback videos for this topic
|
| 252 |
+
fallback_videos = self.youtube_tool._get_fallback_videos(topic, 5, category)
|
| 253 |
+
topic_videos = [{
|
| 254 |
+
"title": video["title"],
|
| 255 |
+
"url": video["url"],
|
| 256 |
+
"embed_url": video["embed_url"],
|
| 257 |
+
"reason": video["description"]
|
| 258 |
+
} for video in fallback_videos]
|
| 259 |
+
topic_recommendations.append({
|
| 260 |
+
"topic": topic,
|
| 261 |
+
"reason": topic_info["reason"],
|
| 262 |
+
"category": category,
|
| 263 |
+
"videos": topic_videos
|
| 264 |
+
})
|
| 265 |
+
print(f" > Generated {len(topic_recommendations)} topic sections with video recommendations.")
|
| 266 |
+
return topic_recommendations
|
| 267 |
|
| 268 |
def generate_structured_report(self, enrollment_no: str) -> dict:
|
| 269 |
+
"""Generates the full, structured student report via API call including video suggestions."""
|
| 270 |
print(f"Generating full report for {enrollment_no}...")
|
|
|
|
| 271 |
student_profile = self.student_data.get(enrollment_no)
|
| 272 |
if not student_profile:
|
| 273 |
return {"error": "No data found for this student."}
|
|
|
|
| 274 |
context = json.dumps(student_profile, indent=2)
|
| 275 |
+
# Use JsonOutputParser with the Pydantic model
|
|
|
|
|
|
|
|
|
|
| 276 |
parser = JsonOutputParser(pydantic_object=StudentReport)
|
|
|
|
|
|
|
| 277 |
prompt_with_format = REPORT_PROMPT.partial(
|
| 278 |
format_instructions=parser.get_format_instructions()
|
| 279 |
)
|
|
|
|
|
|
|
| 280 |
chain = prompt_with_format | self.llm | parser
|
|
|
|
| 281 |
try:
|
| 282 |
+
# Get the base report
|
| 283 |
report_dict = chain.invoke({"context": context})
|
| 284 |
+
# Now generate topic-based video recommendations
|
| 285 |
+
try:
|
| 286 |
+
youtube_recommendations = self._get_youtube_recommendations(report_dict)
|
| 287 |
+
# Add video suggestions to the report
|
| 288 |
+
report_dict["youtube_recommendations"] = youtube_recommendations
|
| 289 |
+
print(f" > Added {len(youtube_recommendations)} topic sections with video recommendations.")
|
| 290 |
+
except Exception as e:
|
| 291 |
+
print(f" > Warning: Failed to generate video suggestions: {e}")
|
| 292 |
+
report_dict["youtube_recommendations"] = self._get_default_topic_recommendations()
|
| 293 |
return report_dict
|
| 294 |
except Exception as e:
|
| 295 |
print(f"Error invoking LLM or parsing output: {e}")
|
| 296 |
+
return {
|
| 297 |
+
"error": "Failed to generate a valid report from the LLM.",
|
| 298 |
+
"overall_summary": "Error generating report. Please try again later.",
|
| 299 |
+
"detailed_scores": [],
|
| 300 |
+
"analysis": {
|
| 301 |
+
"strengths": ["Report generation error"],
|
| 302 |
+
"weaknesses": ["Unable to analyze profile due to system error"]
|
| 303 |
+
},
|
| 304 |
+
"actionable_advice": {
|
| 305 |
+
"recommendations": ["Please try generating the report again or contact support"]
|
| 306 |
+
},
|
| 307 |
+
"resume_analysis": {
|
| 308 |
+
"summary": "Resume analysis unavailable",
|
| 309 |
+
"key_skills": [],
|
| 310 |
+
"professional_links": [],
|
| 311 |
+
"missing_elements": ["Analysis failed"]
|
| 312 |
+
},
|
| 313 |
+
"youtube_recommendations": self._get_default_topic_recommendations()
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
def _get_default_topic_recommendations(self) -> list:
|
| 317 |
+
"""Return default topic-based recommendations in case of errors."""
|
| 318 |
+
default_topics = self._get_default_topics()
|
| 319 |
+
topic_recommendations = []
|
| 320 |
+
for topic_info in default_topics:
|
| 321 |
+
topic = topic_info["topic"]
|
| 322 |
+
category = topic_info["category"]
|
| 323 |
+
# Get fallback videos for this topic
|
| 324 |
+
fallback_videos = self.youtube_tool._get_fallback_videos(topic, 5, category)
|
| 325 |
+
# Format the videos
|
| 326 |
+
topic_videos = [{
|
| 327 |
+
"title": video["title"],
|
| 328 |
+
"url": video["url"],
|
| 329 |
+
"embed_url": video["embed_url"],
|
| 330 |
+
"reason": video["description"]
|
| 331 |
+
} for video in fallback_videos]
|
| 332 |
+
topic_recommendations.append({
|
| 333 |
+
"topic": topic,
|
| 334 |
+
"reason": topic_info["reason"],
|
| 335 |
+
"category": category,
|
| 336 |
+
"videos": topic_videos
|
| 337 |
+
})
|
| 338 |
+
return topic_recommendations
|
| 339 |
+
|
| 340 |
+
def analyze_job_application(self, job_application_link: str, enrollment_no: str) -> dict:
|
| 341 |
+
"""
|
| 342 |
+
Analyzes a student's profile against a job description link using the new JobApplicationAnalyzer.
|
| 343 |
+
This method now performs a comparative analysis and integrates YouTube recommendations based on specific AI-generated queries.
|
| 344 |
+
It fetches the student profile internally using the enrollment_no.
|
| 345 |
+
"""
|
| 346 |
+
print(f" > Starting job analysis using the new analyzer for link: {job_application_link} and student: {enrollment_no}")
|
| 347 |
+
|
| 348 |
+
# Fetch the student profile internally
|
| 349 |
+
student_profile = self.student_data.get(enrollment_no)
|
| 350 |
+
if not student_profile:
|
| 351 |
+
logger.error(f"No data found for enrollment number: {enrollment_no}")
|
| 352 |
+
return {
|
| 353 |
+
"error": "No data found for the provided student enrollment number.",
|
| 354 |
+
"strategic_overview": {"summary": "Error: Student data not found.", "your_key_opportunity": "Please check the enrollment number."},
|
| 355 |
+
"your_core_strengths_for_this_role": [],
|
| 356 |
+
"strategic_areas_for_growth": [],
|
| 357 |
+
"video_recommendations": []
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
# Use the imported JobApplicationAnalyzer's analyze method
|
| 361 |
+
analysis_result = self.job_analyzer.analyze(job_application_link, student_profile)
|
| 362 |
+
|
| 363 |
+
return analysis_result
|
| 364 |
+
|
| 365 |
+
def get_student_dashboard_metrics(self, enrollment_no: str) -> dict:
|
| 366 |
+
"""
|
| 367 |
+
Retrieves and analyzes a specific student's profile data using the dashboard_analyzer logic.
|
| 368 |
+
"""
|
| 369 |
+
print(f" > Calculating dashboard metrics for student: {enrollment_no}")
|
| 370 |
+
student_profile = self.student_data.get(enrollment_no)
|
| 371 |
+
if not student_profile:
|
| 372 |
+
logger.error(f"No data found for enrollment number: {enrollment_no}")
|
| 373 |
+
return {"error": "No data found for the provided student enrollment number."}
|
| 374 |
+
|
| 375 |
+
# Use the imported get_dashboard_metrics function from dashboard_analyzer.py
|
| 376 |
+
metrics = get_dashboard_metrics(student_profile)
|
| 377 |
+
|
| 378 |
+
return metrics
|
| 379 |
+
|
| 380 |
|
| 381 |
def answer_question(self, query: str, enrollment_no: str) -> str:
|
| 382 |
"""Answers a specific question using a targeted context from the JSON."""
|
| 383 |
print(f"Answering question for {enrollment_no}: '{query}'")
|
|
|
|
| 384 |
student_profile = self.student_data.get(enrollment_no)
|
| 385 |
if not student_profile:
|
| 386 |
return "Could not find data for the selected student."
|
|
|
|
| 391 |
targeted_context = {}
|
| 392 |
if "academic_profile" in sources_to_use:
|
| 393 |
targeted_context["academic_profile"] = student_profile.get("academic_profile")
|
|
|
|
| 394 |
coding_profiles = {}
|
| 395 |
if "leetcode" in sources_to_use:
|
| 396 |
coding_profiles["leetcode"] = student_profile.get("coding_profiles", {}).get("leetcode")
|
|
|
|
| 398 |
coding_profiles["github"] = student_profile.get("coding_profiles", {}).get("github")
|
| 399 |
if "codeforces" in sources_to_use:
|
| 400 |
coding_profiles["codeforces"] = student_profile.get("coding_profiles", {}).get("codeforces")
|
|
|
|
| 401 |
if coding_profiles:
|
| 402 |
targeted_context["coding_profiles"] = coding_profiles
|
|
|
|
| 403 |
if "coding_profiles" in sources_to_use and not coding_profiles:
|
| 404 |
+
targeted_context["coding_profiles"] = student_profile.get("coding_profiles")
|
| 405 |
+
if "resume" in sources_to_use:
|
| 406 |
+
targeted_context["resume"] = student_profile.get("resume")
|
| 407 |
|
| 408 |
if not targeted_context:
|
| 409 |
return "I could not find any relevant information in this student's profile to answer that question."
|
| 410 |
|
| 411 |
context_str = json.dumps(targeted_context, indent=2)
|
|
|
|
| 412 |
chain = QA_PROMPT | self.llm
|
| 413 |
result = chain.invoke({"context": context_str, "question": query})
|
| 414 |
+
return result.content
|
|
|
requirements.txt
CHANGED
|
@@ -1,13 +1,102 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
Flask==3.0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
gunicorn==22.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
Werkzeug==3.0.6
|
| 6 |
-
|
| 7 |
-
pyngrok
|
| 8 |
-
pycryptodome
|
| 9 |
-
bs4
|
| 10 |
-
google-generativeai
|
| 11 |
-
langchain-google-genai
|
| 12 |
-
langchain
|
| 13 |
-
pydantic
|
|
|
|
| 1 |
+
acres==0.5.0
|
| 2 |
+
annotated-types==0.7.0
|
| 3 |
+
anyio==4.11.0
|
| 4 |
+
autopep8==2.3.2
|
| 5 |
+
beautifulsoup4==4.14.2
|
| 6 |
+
blinker==1.9.0
|
| 7 |
+
breadability==0.1.20
|
| 8 |
+
bs4==0.0.2
|
| 9 |
+
cachetools==6.2.0
|
| 10 |
+
certifi==2025.10.5
|
| 11 |
+
chardet==5.2.0
|
| 12 |
+
charset-normalizer==3.4.3
|
| 13 |
+
ci-info==0.3.0
|
| 14 |
+
click==8.3.0
|
| 15 |
+
configobj==5.0.9
|
| 16 |
+
configparser==7.2.0
|
| 17 |
+
docopt==0.6.2
|
| 18 |
+
etelemetry==0.3.1
|
| 19 |
+
filelock==3.19.1
|
| 20 |
+
filetype==1.2.0
|
| 21 |
+
fitz==0.0.1.dev2
|
| 22 |
Flask==3.0.3
|
| 23 |
+
google-ai-generativelanguage==0.6.15
|
| 24 |
+
google-api-core==2.25.2
|
| 25 |
+
google-api-python-client==2.184.0
|
| 26 |
+
google-auth==2.41.1
|
| 27 |
+
google-auth-httplib2==0.2.0
|
| 28 |
+
google-generativeai==0.8.5
|
| 29 |
+
googleapis-common-protos==1.70.0
|
| 30 |
+
greenlet==3.2.4
|
| 31 |
+
grpcio==1.75.1
|
| 32 |
+
grpcio-status==1.71.2
|
| 33 |
gunicorn==22.0.0
|
| 34 |
+
h11==0.16.0
|
| 35 |
+
httpcore==1.0.9
|
| 36 |
+
httplib2==0.31.0
|
| 37 |
+
httpx==0.28.1
|
| 38 |
+
idna==3.10
|
| 39 |
+
importlib_resources==6.5.2
|
| 40 |
+
itsdangerous==2.2.0
|
| 41 |
+
Jinja2==3.1.6
|
| 42 |
+
joblib==1.5.2
|
| 43 |
+
jsonpatch==1.33
|
| 44 |
+
jsonpointer==3.0.0
|
| 45 |
+
langchain==0.3.27
|
| 46 |
+
langchain-core==0.3.78
|
| 47 |
+
langchain-google-genai==2.0.10
|
| 48 |
+
langchain-text-splitters==0.3.11
|
| 49 |
+
langsmith==0.4.32
|
| 50 |
+
looseversion==1.3.0
|
| 51 |
+
lxml==6.0.2
|
| 52 |
+
MarkupSafe==3.0.3
|
| 53 |
+
networkx==3.5
|
| 54 |
+
nibabel==5.3.2
|
| 55 |
+
nipype==1.10.0
|
| 56 |
+
nltk==3.9.2
|
| 57 |
+
numpy==2.3.3
|
| 58 |
+
orjson==3.11.3
|
| 59 |
+
packaging==25.0
|
| 60 |
+
pandas==2.3.3
|
| 61 |
+
pathlib==1.0.1
|
| 62 |
+
proto-plus==1.26.1
|
| 63 |
+
protobuf==5.29.5
|
| 64 |
+
prov==2.1.1
|
| 65 |
+
puremagic==1.30
|
| 66 |
+
pyasn1==0.6.1
|
| 67 |
+
pyasn1_modules==0.4.2
|
| 68 |
+
pycodestyle==2.14.0
|
| 69 |
+
pycountry==24.6.1
|
| 70 |
+
pycryptodome==3.23.0
|
| 71 |
+
pydantic==2.11.10
|
| 72 |
+
pydantic_core==2.33.2
|
| 73 |
+
pydot==4.0.1
|
| 74 |
+
PyMuPDF==1.26.4
|
| 75 |
+
pyngrok==7.4.0
|
| 76 |
+
pyparsing==3.2.5
|
| 77 |
+
python-dateutil==2.9.0.post0
|
| 78 |
+
pytz==2025.2
|
| 79 |
+
pyxnat==1.6.3
|
| 80 |
+
PyYAML==6.0.3
|
| 81 |
+
rdflib==7.2.1
|
| 82 |
+
regex==2025.9.18
|
| 83 |
+
requests==2.32.5
|
| 84 |
+
requests-toolbelt==1.0.0
|
| 85 |
+
rsa==4.9.1
|
| 86 |
+
scipy==1.16.2
|
| 87 |
+
simplejson==3.20.2
|
| 88 |
+
six==1.17.0
|
| 89 |
+
sniffio==1.3.1
|
| 90 |
+
soupsieve==2.8
|
| 91 |
+
SQLAlchemy==2.0.43
|
| 92 |
+
sumy==0.11.0
|
| 93 |
+
tenacity==9.1.2
|
| 94 |
+
tqdm==4.67.1
|
| 95 |
+
traits==7.0.2
|
| 96 |
+
typing-inspection==0.4.2
|
| 97 |
+
typing_extensions==4.15.0
|
| 98 |
+
tzdata==2025.2
|
| 99 |
+
uritemplate==4.2.0
|
| 100 |
+
urllib3==2.5.0
|
| 101 |
Werkzeug==3.0.6
|
| 102 |
+
zstandard==0.25.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resume.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b2af74790d52e5d36b337cfcbe934cd22fb60780ed896fc7b4a20587601a3924
|
| 3 |
+
size 238760
|
resume_parser.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# resume_parser.py
|
| 2 |
+
import fitz # PyMuPDF
|
| 3 |
+
import re
|
| 4 |
+
from sumy.parsers.plaintext import PlaintextParser
|
| 5 |
+
from sumy.nlp.tokenizers import Tokenizer
|
| 6 |
+
from sumy.summarizers.lex_rank import LexRankSummarizer
|
| 7 |
+
|
| 8 |
+
def parse_resume(pdf_path):
|
| 9 |
+
"""
|
| 10 |
+
Parse PDF resume to extract text, hyperlinks, and generate summary
|
| 11 |
+
Returns: {
|
| 12 |
+
"full_text": str,
|
| 13 |
+
"hyperlinks": list,
|
| 14 |
+
"summary": str
|
| 15 |
+
}
|
| 16 |
+
"""
|
| 17 |
+
doc = fitz.open(pdf_path)
|
| 18 |
+
full_text = ""
|
| 19 |
+
hyperlinks = []
|
| 20 |
+
|
| 21 |
+
# Extract text and hyperlinks
|
| 22 |
+
for page_num in range(len(doc)):
|
| 23 |
+
page = doc[page_num]
|
| 24 |
+
full_text += page.get_text() + "\n"
|
| 25 |
+
|
| 26 |
+
# Extract hyperlinks
|
| 27 |
+
links = page.get_links()
|
| 28 |
+
for link in links:
|
| 29 |
+
if link.get('uri'):
|
| 30 |
+
# Clean up common PDF hyperlink artifacts
|
| 31 |
+
url = re.sub(r'\s+', '', link['uri'])
|
| 32 |
+
if url.startswith(('http://', 'https://', 'mailto:')):
|
| 33 |
+
hyperlinks.append(url)
|
| 34 |
+
|
| 35 |
+
# Remove duplicates while preserving order
|
| 36 |
+
hyperlinks = list(dict.fromkeys(hyperlinks))
|
| 37 |
+
|
| 38 |
+
# Generate summary (fallback to first 200 words if summarization fails)
|
| 39 |
+
try:
|
| 40 |
+
parser = PlaintextParser.from_string(full_text, Tokenizer("english"))
|
| 41 |
+
summarizer = LexRankSummarizer()
|
| 42 |
+
# Summarize to 5 sentences
|
| 43 |
+
summary_sentences = summarizer(parser.document, 5)
|
| 44 |
+
summary = " ".join(str(sentence) for sentence in summary_sentences)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
print(f"Warning: Summarization failed ({e}). Using fallback summary.")
|
| 47 |
+
summary = " ".join(full_text.split()[:200]) + "..."
|
| 48 |
+
|
| 49 |
+
return {
|
| 50 |
+
"full_text": full_text.strip(),
|
| 51 |
+
"hyperlinks": hyperlinks,
|
| 52 |
+
"summary": summary.strip()
|
| 53 |
+
}
|
static/final.css
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* --- CSS Variables for Theming --- */
|
| 2 |
+
:root {
|
| 3 |
+
--bg-color: #0D0D0D; /* Near black background */
|
| 4 |
+
--panel-color: #1A1A1A; /* Darkest gray for panels/cards */
|
| 5 |
+
--input-bg: #2C2C2C; /* Lighter gray for inputs */
|
| 6 |
+
--border-color: #333333; /* Subtle border color */
|
| 7 |
+
--text-color: #E0E0E0; /* Light gray for text */
|
| 8 |
+
--text-color-muted: #8A8A8A; /* Dimmer text for labels/placeholders */
|
| 9 |
+
--accent-color: #00E5FF; /* Vibrant cyan/teal for highlights */
|
| 10 |
+
--accent-color-hover: #00B8CC;
|
| 11 |
+
--font-family: 'Inter', sans-serif;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/* --- Global & Reset Styles --- */
|
| 15 |
+
* {
|
| 16 |
+
box-sizing: border-box;
|
| 17 |
+
margin: 0;
|
| 18 |
+
padding: 0;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
body {
|
| 22 |
+
font-family: var(--font-family);
|
| 23 |
+
background-color: var(--bg-color);
|
| 24 |
+
color: var(--text-color);
|
| 25 |
+
font-size: 16px;
|
| 26 |
+
overflow-x: hidden;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
h1, h2, h3, h4 {
|
| 30 |
+
font-weight: 600;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
h2 {
|
| 34 |
+
font-size: 1.75rem;
|
| 35 |
+
margin-bottom: 24px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h3 {
|
| 39 |
+
font-size: 1.1rem;
|
| 40 |
+
margin-bottom: 12px;
|
| 41 |
+
color: var(--text-color);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
a {
|
| 45 |
+
color: var(--accent-color);
|
| 46 |
+
text-decoration: none;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
ul {
|
| 50 |
+
list-style-position: inside;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/* --- Main Layout --- */
|
| 54 |
+
.app-container {
|
| 55 |
+
display: grid;
|
| 56 |
+
grid-template-columns: 240px 1fr;
|
| 57 |
+
height: 100vh;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.sidebar {
|
| 61 |
+
background-color: var(--bg-color);
|
| 62 |
+
border-right: 1px solid var(--border-color);
|
| 63 |
+
padding: 24px;
|
| 64 |
+
display: flex;
|
| 65 |
+
flex-direction: column;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.main-content {
|
| 69 |
+
padding: 24px 48px;
|
| 70 |
+
overflow-y: auto;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* --- Sidebar --- */
|
| 74 |
+
.app-header {
|
| 75 |
+
margin-bottom: 40px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.app-header h1 {
|
| 79 |
+
font-size: 2rem;
|
| 80 |
+
color: var(--accent-color);
|
| 81 |
+
font-weight: 700;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.app-header p {
|
| 85 |
+
font-size: 0.9rem;
|
| 86 |
+
color: var(--text-color-muted);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.main-nav ul {
|
| 90 |
+
list-style: none;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.main-nav .nav-link {
|
| 94 |
+
display: block;
|
| 95 |
+
color: var(--text-color-muted);
|
| 96 |
+
padding: 12px 16px;
|
| 97 |
+
margin-bottom: 8px;
|
| 98 |
+
border-radius: 8px;
|
| 99 |
+
font-weight: 500;
|
| 100 |
+
transition: background-color 0.2s, color 0.2s;
|
| 101 |
+
position: relative;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.main-nav .nav-link:hover {
|
| 105 |
+
background-color: var(--panel-color);
|
| 106 |
+
color: var(--text-color);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.main-nav .nav-link.active {
|
| 110 |
+
background-color: var(--panel-color);
|
| 111 |
+
color: var(--text-color);
|
| 112 |
+
font-weight: 600;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.main-nav .nav-link.active::before {
|
| 116 |
+
content: '';
|
| 117 |
+
position: absolute;
|
| 118 |
+
left: 0;
|
| 119 |
+
top: 50%;
|
| 120 |
+
transform: translateY(-50%);
|
| 121 |
+
width: 4px;
|
| 122 |
+
height: 24px;
|
| 123 |
+
background-color: var(--accent-color);
|
| 124 |
+
border-radius: 0 4px 4px 0;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* --- Top Control Bar --- */
|
| 128 |
+
.top-bar {
|
| 129 |
+
display: flex;
|
| 130 |
+
justify-content: space-between;
|
| 131 |
+
align-items: center;
|
| 132 |
+
gap: 20px;
|
| 133 |
+
background-color: var(--panel-color);
|
| 134 |
+
padding: 16px 24px;
|
| 135 |
+
border-radius: 12px;
|
| 136 |
+
margin-bottom: 32px;
|
| 137 |
+
border: 1px solid var(--border-color);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.input-group {
|
| 141 |
+
display: flex;
|
| 142 |
+
align-items: center;
|
| 143 |
+
gap: 12px;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.input-group label {
|
| 147 |
+
font-size: 0.9rem;
|
| 148 |
+
color: var(--text-color-muted);
|
| 149 |
+
font-weight: 500;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* --- Buttons & Inputs --- */
|
| 153 |
+
.btn {
|
| 154 |
+
padding: 10px 20px;
|
| 155 |
+
border: none;
|
| 156 |
+
border-radius: 8px;
|
| 157 |
+
font-weight: 600;
|
| 158 |
+
font-size: 0.9rem;
|
| 159 |
+
cursor: pointer;
|
| 160 |
+
transition: background-color 0.2s, transform 0.1s;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.btn:disabled {
|
| 164 |
+
cursor: not-allowed;
|
| 165 |
+
opacity: 0.5;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.btn:active:not(:disabled) {
|
| 169 |
+
transform: scale(0.98);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.btn-primary {
|
| 173 |
+
background-color: var(--accent-color);
|
| 174 |
+
color: #000;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.btn-primary:hover:not(:disabled) {
|
| 178 |
+
background-color: var(--accent-color-hover);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.btn-secondary {
|
| 182 |
+
background-color: var(--input-bg);
|
| 183 |
+
color: var(--text-color);
|
| 184 |
+
border: 1px solid var(--border-color);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.btn-secondary:hover:not(:disabled) {
|
| 188 |
+
background-color: #383838;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
input[type="text"],
|
| 192 |
+
select {
|
| 193 |
+
background-color: var(--input-bg);
|
| 194 |
+
border: 1px solid var(--border-color);
|
| 195 |
+
color: var(--text-color);
|
| 196 |
+
padding: 10px 14px;
|
| 197 |
+
border-radius: 8px;
|
| 198 |
+
min-width: 250px;
|
| 199 |
+
font-family: var(--font-family);
|
| 200 |
+
font-size: 0.9rem;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
input[type="text"]::placeholder {
|
| 204 |
+
color: var(--text-color-muted);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
/* --- Content Sections & Cards --- */
|
| 209 |
+
.content-section.hidden {
|
| 210 |
+
display: none;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.card {
|
| 214 |
+
background: rgba(26, 26, 26, 0.7); /* The semi-transparent panel color for the gloss effect */
|
| 215 |
+
backdrop-filter: blur(10px); /* The "frosted glass" effect */
|
| 216 |
+
border: 1px solid var(--border-color);
|
| 217 |
+
border-radius: 16px;
|
| 218 |
+
padding: 24px;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.chart-placeholder {
|
| 222 |
+
display: flex;
|
| 223 |
+
align-items: center;
|
| 224 |
+
justify-content: center;
|
| 225 |
+
height: 100%;
|
| 226 |
+
color: var(--text-color-muted);
|
| 227 |
+
font-size: 1.2rem;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* --- Dashboard --- */
|
| 231 |
+
.dashboard-grid {
|
| 232 |
+
display: grid;
|
| 233 |
+
grid-template-columns: repeat(3, 1fr);
|
| 234 |
+
grid-gap: 24px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.large-card {
|
| 238 |
+
grid-column: 1 / 3;
|
| 239 |
+
grid-row: 1 / 3;
|
| 240 |
+
min-height: 400px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.metric-cards-grid {
|
| 244 |
+
grid-column: 3 / 4;
|
| 245 |
+
display: grid;
|
| 246 |
+
grid-template-rows: repeat(3, 1fr);
|
| 247 |
+
gap: 24px;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.metric {
|
| 251 |
+
font-size: 2.5rem;
|
| 252 |
+
font-weight: 700;
|
| 253 |
+
color: var(--accent-color);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
/* --- Report & Job Analysis Grids --- */
|
| 258 |
+
.report-grid, .job-analysis-grid {
|
| 259 |
+
display: grid;
|
| 260 |
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
| 261 |
+
gap: 24px;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.skills-list {
|
| 265 |
+
display: flex;
|
| 266 |
+
flex-wrap: wrap;
|
| 267 |
+
gap: 8px;
|
| 268 |
+
}
|
| 269 |
+
.skill-tag {
|
| 270 |
+
background-color: var(--input-bg);
|
| 271 |
+
padding: 6px 12px;
|
| 272 |
+
border-radius: 6px;
|
| 273 |
+
font-size: 0.85rem;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
/* --- Chat Interface --- */
|
| 277 |
+
.chat-container {
|
| 278 |
+
display: flex;
|
| 279 |
+
flex-direction: column;
|
| 280 |
+
height: 70vh; /* Adjust height as needed */
|
| 281 |
+
padding: 0;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
#chat-history {
|
| 285 |
+
flex-grow: 1;
|
| 286 |
+
overflow-y: auto;
|
| 287 |
+
padding: 24px;
|
| 288 |
+
display: flex;
|
| 289 |
+
flex-direction: column;
|
| 290 |
+
gap: 16px;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
#chat-form {
|
| 294 |
+
display: flex;
|
| 295 |
+
gap: 10px;
|
| 296 |
+
padding: 24px;
|
| 297 |
+
border-top: 1px solid var(--border-color);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
#chat-input {
|
| 301 |
+
flex-grow: 1;
|
| 302 |
+
min-width: 0;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.chat-message {
|
| 306 |
+
padding: 12px 16px;
|
| 307 |
+
border-radius: 12px;
|
| 308 |
+
max-width: 80%;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.user-message {
|
| 312 |
+
background-color: var(--accent-color);
|
| 313 |
+
color: #000;
|
| 314 |
+
align-self: flex-end;
|
| 315 |
+
border-bottom-right-radius: 0;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.ai-message {
|
| 319 |
+
background-color: var(--input-bg);
|
| 320 |
+
color: var(--text-color);
|
| 321 |
+
align-self: flex-start;
|
| 322 |
+
border-bottom-left-radius: 0;
|
| 323 |
+
}
|
| 324 |
+
.ai-message strong {
|
| 325 |
+
color: var(--accent-color);
|
| 326 |
+
}
|
| 327 |
+
.ai-message p {
|
| 328 |
+
white-space: pre-wrap;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
/* --- Loading Spinner --- */
|
| 333 |
+
.loading-overlay {
|
| 334 |
+
position: fixed;
|
| 335 |
+
top: 0;
|
| 336 |
+
left: 0;
|
| 337 |
+
width: 100%;
|
| 338 |
+
height: 100%;
|
| 339 |
+
background-color: rgba(0, 0, 0, 0.7);
|
| 340 |
+
display: flex;
|
| 341 |
+
flex-direction: column;
|
| 342 |
+
align-items: center;
|
| 343 |
+
justify-content: center;
|
| 344 |
+
z-index: 1000;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.spinner {
|
| 348 |
+
border: 4px solid rgba(255, 255, 255, 0.3);
|
| 349 |
+
border-radius: 50%;
|
| 350 |
+
border-top: 4px solid var(--accent-color);
|
| 351 |
+
width: 50px;
|
| 352 |
+
height: 50px;
|
| 353 |
+
animation: spin 1s linear infinite;
|
| 354 |
+
margin-bottom: 20px;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
@keyframes spin {
|
| 358 |
+
0% { transform: rotate(0deg); }
|
| 359 |
+
100% { transform: rotate(360deg); }
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.loading-overlay p {
|
| 363 |
+
color: var(--text-color);
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.hidden {
|
| 367 |
+
display: none !important;
|
| 368 |
+
}
|
| 369 |
+
/* Add these rules to your existing final.css */
|
| 370 |
+
|
| 371 |
+
/* Ensure the chart containers have a defined size */
|
| 372 |
+
.chart-container {
|
| 373 |
+
/* You can adjust these values as needed */
|
| 374 |
+
min-height: 300px; /* Set a minimum height */
|
| 375 |
+
/* Optional: Set a max-height if needed */
|
| 376 |
+
/* max-height: 400px; */
|
| 377 |
+
/* Ensure the canvas fills the container */
|
| 378 |
+
display: flex;
|
| 379 |
+
flex-direction: column; /* Stack chart title and canvas vertically */
|
| 380 |
+
justify-content: center; /* Center the chart vertically if needed */
|
| 381 |
+
align-items: center; /* Center the chart horizontally */
|
| 382 |
+
position: relative; /* Needed for Chart.js responsiveness */
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/* Ensure the canvas elements themselves have a defined size */
|
| 386 |
+
.chart-container canvas {
|
| 387 |
+
width: 100% !important; /* Override any inline styles Chart.js might add */
|
| 388 |
+
height: 100% !important; /* Override any inline styles Chart.js might add */
|
| 389 |
+
max-width: 100%; /* Prevent overflow */
|
| 390 |
+
max-height: 100%; /* Prevent overflow */
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/* Optional: Style the placeholder text if no data is shown */
|
| 394 |
+
.chart-placeholder {
|
| 395 |
+
display: flex;
|
| 396 |
+
align-items: center;
|
| 397 |
+
justify-content: center;
|
| 398 |
+
width: 100%;
|
| 399 |
+
height: 100%;
|
| 400 |
+
color: var(--gray); /* Use your defined CSS variable */
|
| 401 |
+
font-style: italic;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
/* Adjust the grid layout if necessary */
|
| 405 |
+
.dashboard-grid {
|
| 406 |
+
display: grid;
|
| 407 |
+
grid-template-columns: 1fr; /* Default to single column */
|
| 408 |
+
gap: 20px;
|
| 409 |
+
margin-top: 20px;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.metric-cards-grid {
|
| 413 |
+
display: grid;
|
| 414 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Adjust minmax width as needed */
|
| 415 |
+
gap: 20px;
|
| 416 |
+
margin-bottom: 20px; /* Space below the metric row */
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
/* Responsive grid for dashboard sections */
|
| 420 |
+
@media (min-width: 992px) {
|
| 421 |
+
.dashboard-grid {
|
| 422 |
+
grid-template-columns: 1fr 1fr; /* Two columns for most dashboard items */
|
| 423 |
+
}
|
| 424 |
+
.metric-cards-grid {
|
| 425 |
+
grid-column: span 2; /* Metrics span full width */
|
| 426 |
+
}
|
| 427 |
+
.large-card {
|
| 428 |
+
grid-column: span 2; /* Large card spans 2 columns */
|
| 429 |
+
}
|
| 430 |
+
.chart-row {
|
| 431 |
+
grid-column: span 2; /* Chart row spans 2 columns */
|
| 432 |
+
display: grid;
|
| 433 |
+
grid-template-columns: 1fr 1fr; /* Two charts side-by-side within the row */
|
| 434 |
+
gap: 20px;
|
| 435 |
+
}
|
| 436 |
+
.chart-container {
|
| 437 |
+
/* Remove min-height from individual containers if using chart-row */
|
| 438 |
+
min-height: auto;
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
/* Fallback for smaller screens */
|
| 443 |
+
@media (max-width: 991px) {
|
| 444 |
+
.chart-row {
|
| 445 |
+
display: grid;
|
| 446 |
+
grid-template-columns: 1fr; /* Charts stack vertically on smaller screens */
|
| 447 |
+
gap: 20px;
|
| 448 |
+
}
|
| 449 |
+
}
|
static/final.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Helper function to convert basic markdown to HTML
|
| 2 |
+
// This handles **bold**, *italic*, and \n (newlines)
|
| 3 |
+
function convertMarkdownToHTML(text) {
|
| 4 |
+
if (typeof text !== 'string') {
|
| 5 |
+
return text; // Return as-is if not a string
|
| 6 |
+
}
|
| 7 |
+
let htmlText = text;
|
| 8 |
+
// Handle **bold**
|
| 9 |
+
htmlText = htmlText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
| 10 |
+
// Handle *italic* (single underscores might interfere with URLs, so using single asterisks here)
|
| 11 |
+
htmlText = htmlText.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
| 12 |
+
// Handle line breaks (\n)
|
| 13 |
+
htmlText = htmlText.replace(/\n/g, '<br>');
|
| 14 |
+
return htmlText;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 18 |
+
const studentSelector = document.getElementById('student-selector');
|
| 19 |
+
const generateReportBtn = document.getElementById('generate-report-btn');
|
| 20 |
+
const jobApplicationInput = document.getElementById('job-application-input');
|
| 21 |
+
const analyzeJobBtn = document.getElementById('analyze-job-btn');
|
| 22 |
+
const loadingSpinner = document.getElementById('loading-spinner');
|
| 23 |
+
const reportContainer = document.getElementById('reports'); // Updated to match new HTML ID
|
| 24 |
+
const jobAnalysisContainer = document.getElementById('job-analysis'); // Updated to match new HTML ID
|
| 25 |
+
const chatbotContainer = document.getElementById('chat'); // Updated to match new HTML ID
|
| 26 |
+
const chatForm = document.getElementById('chat-form');
|
| 27 |
+
const chatInput = document.getElementById('chat-input');
|
| 28 |
+
const chatHistory = document.getElementById('chat-history');
|
| 29 |
+
|
| 30 |
+
// --- Chart Instances (To destroy/recreate when data changes) ---
|
| 31 |
+
let skillsChart = null;
|
| 32 |
+
let dsaChart = null;
|
| 33 |
+
let jobMatchChart = null; // This one might be tricky without specific data
|
| 34 |
+
|
| 35 |
+
// --- Navigation Elements ---
|
| 36 |
+
const navLinks = document.querySelectorAll('.nav-link');
|
| 37 |
+
const contentSections = document.querySelectorAll('.content-section');
|
| 38 |
+
|
| 39 |
+
// --- Sidebar Navigation ---
|
| 40 |
+
navLinks.forEach(link => {
|
| 41 |
+
link.addEventListener('click', (e) => {
|
| 42 |
+
e.preventDefault();
|
| 43 |
+
|
| 44 |
+
const targetId = link.getAttribute('href').substring(1); // Get ID without '#'
|
| 45 |
+
|
| 46 |
+
// Update active link
|
| 47 |
+
navLinks.forEach(l => l.classList.remove('active'));
|
| 48 |
+
link.classList.add('active');
|
| 49 |
+
|
| 50 |
+
// Show target section, hide others
|
| 51 |
+
contentSections.forEach(section => {
|
| 52 |
+
section.classList.add('hidden');
|
| 53 |
+
if (section.id === targetId) {
|
| 54 |
+
section.classList.remove('hidden');
|
| 55 |
+
section.classList.add('active');
|
| 56 |
+
// Load dashboard data when the dashboard section becomes active
|
| 57 |
+
if (targetId === 'dashboard') {
|
| 58 |
+
console.log("Dashboard section activated, attempting to load data.");
|
| 59 |
+
loadDashboardData(); // Call loadDashboardData here
|
| 60 |
+
}
|
| 61 |
+
} else {
|
| 62 |
+
section.classList.remove('active');
|
| 63 |
+
}
|
| 64 |
+
});
|
| 65 |
+
});
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// --- Initial Navigation Setup (Show Dashboard by default) ---
|
| 69 |
+
const dashboardLink = document.querySelector('.nav-link[href="#dashboard"]');
|
| 70 |
+
if (dashboardLink) {
|
| 71 |
+
dashboardLink.click(); // Programmatically click the dashboard link to show it initially
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
// 1. Populate student dropdown on page load
|
| 76 |
+
fetch('/api/students')
|
| 77 |
+
.then(response => response.json())
|
| 78 |
+
.then(students => {
|
| 79 |
+
students.forEach(student => {
|
| 80 |
+
const option = document.createElement('option');
|
| 81 |
+
option.value = student.enrollment_no;
|
| 82 |
+
option.textContent = `${student.name} (${student.enrollment_no})`;
|
| 83 |
+
studentSelector.appendChild(option);
|
| 84 |
+
});
|
| 85 |
+
})
|
| 86 |
+
.catch(error => console.error('Error fetching students:', error));
|
| 87 |
+
|
| 88 |
+
// 2. Enable buttons when inputs are filled
|
| 89 |
+
studentSelector.addEventListener('change', () => {
|
| 90 |
+
const hasSelection = !!studentSelector.value;
|
| 91 |
+
generateReportBtn.disabled = !hasSelection;
|
| 92 |
+
// Note: chatbotContainer is now a section, not tied directly to student selection visibility here
|
| 93 |
+
// reportContainer.classList.add('hidden'); // Hide old report on new selection - Handled by nav now
|
| 94 |
+
// chatHistory.innerHTML = ''; // Clear chat history - Handled by nav or separately if needed
|
| 95 |
+
console.log("Student selection changed to:", studentSelector.value);
|
| 96 |
+
// Reload dashboard data if currently on the dashboard page
|
| 97 |
+
if (document.querySelector('#dashboard')?.classList.contains('active')) { // Added optional chaining
|
| 98 |
+
console.log("Currently on dashboard, reloading data for new selection.");
|
| 99 |
+
loadDashboardData();
|
| 100 |
+
}
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
jobApplicationInput.addEventListener('input', () => {
|
| 104 |
+
analyzeJobBtn.disabled = !jobApplicationInput.value.trim();
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
// 3. Handle "Generate Report" button click
|
| 108 |
+
generateReportBtn.addEventListener('click', () => {
|
| 109 |
+
const enrollmentNo = studentSelector.value;
|
| 110 |
+
if (!enrollmentNo) return;
|
| 111 |
+
|
| 112 |
+
loadingSpinner.classList.remove('hidden');
|
| 113 |
+
// Hide other sections while loading
|
| 114 |
+
reportContainer.classList.add('hidden');
|
| 115 |
+
jobAnalysisContainer.classList.add('hidden');
|
| 116 |
+
// chatbotContainer.classList.add('hidden'); // Don't hide chat, just report
|
| 117 |
+
|
| 118 |
+
// Navigate to the reports section
|
| 119 |
+
document.querySelector('.nav-link[href="#reports"]').click();
|
| 120 |
+
|
| 121 |
+
fetch(`/api/report/${enrollmentNo}`)
|
| 122 |
+
.then(response => {
|
| 123 |
+
if (!response.ok) {
|
| 124 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 125 |
+
}
|
| 126 |
+
return response.json();
|
| 127 |
+
})
|
| 128 |
+
.then(report => {
|
| 129 |
+
loadingSpinner.classList.add('hidden');
|
| 130 |
+
if (report.error) {
|
| 131 |
+
alert(`Error generating report: ${report.error}`);
|
| 132 |
+
} else {
|
| 133 |
+
displayReport(report);
|
| 134 |
+
// The section is shown by the navigation click above
|
| 135 |
+
}
|
| 136 |
+
})
|
| 137 |
+
.catch(error => {
|
| 138 |
+
loadingSpinner.classList.add('hidden');
|
| 139 |
+
console.error('Report generation error:', error);
|
| 140 |
+
alert(`An unexpected error occurred: ${error.message}`);
|
| 141 |
+
});
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
// 4. Handle "Analyze Job Application" button click
|
| 145 |
+
analyzeJobBtn.addEventListener('click', () => {
|
| 146 |
+
const jobApplicationLink = jobApplicationInput.value.trim();
|
| 147 |
+
// Get the enrollment number from the currently selected student in the dropdown
|
| 148 |
+
const enrollmentNo = studentSelector.value;
|
| 149 |
+
|
| 150 |
+
if (!jobApplicationLink || !enrollmentNo) { // Check if both values exist
|
| 151 |
+
alert('Please select a student and provide a job application link.');
|
| 152 |
+
return; // Stop execution if either value is missing
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
loadingSpinner.classList.remove('hidden');
|
| 156 |
+
// Hide other sections while loading
|
| 157 |
+
reportContainer.classList.add('hidden');
|
| 158 |
+
jobAnalysisContainer.classList.add('hidden');
|
| 159 |
+
// chatbotContainer.classList.add('hidden'); // Don't hide chat, just job analysis
|
| 160 |
+
|
| 161 |
+
// Navigate to the job analysis section
|
| 162 |
+
document.querySelector('.nav-link[href="#job-analysis"]').click();
|
| 163 |
+
|
| 164 |
+
fetch('/api/job-analysis', {
|
| 165 |
+
method: 'POST',
|
| 166 |
+
headers: { 'Content-Type': 'application/json' },
|
| 167 |
+
// Include both the job link and the enrollment number in the request body
|
| 168 |
+
body: JSON.stringify({
|
| 169 |
+
job_application_link: jobApplicationLink,
|
| 170 |
+
enrollment_no: enrollmentNo // <-- Add this line
|
| 171 |
+
})
|
| 172 |
+
})
|
| 173 |
+
.then(response => {
|
| 174 |
+
if (!response.ok) {
|
| 175 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 176 |
+
}
|
| 177 |
+
return response.json();
|
| 178 |
+
})
|
| 179 |
+
.then(data => {
|
| 180 |
+
loadingSpinner.classList.add('hidden');
|
| 181 |
+
if (data.error) {
|
| 182 |
+
alert(`Error analyzing job application: ${data.error}`);
|
| 183 |
+
} else {
|
| 184 |
+
displayJobAnalysis(data.data); // Access data.data as per API response structure
|
| 185 |
+
// The section is shown by the navigation click above
|
| 186 |
+
}
|
| 187 |
+
})
|
| 188 |
+
.catch(error => {
|
| 189 |
+
loadingSpinner.classList.add('hidden');
|
| 190 |
+
console.error('Job analysis error:', error);
|
| 191 |
+
alert(`An unexpected error occurred: ${error.message}`);
|
| 192 |
+
});
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
// 5. Handle chat form submission
|
| 196 |
+
chatForm.addEventListener('submit', (e) => {
|
| 197 |
+
e.preventDefault();
|
| 198 |
+
const enrollmentNo = studentSelector.value;
|
| 199 |
+
const question = chatInput.value.trim();
|
| 200 |
+
|
| 201 |
+
if (!question || !enrollmentNo) return;
|
| 202 |
+
|
| 203 |
+
// Navigate to the chat section if not already there
|
| 204 |
+
document.querySelector('.nav-link[href="#chat"]').click();
|
| 205 |
+
|
| 206 |
+
appendMessage(question, 'user');
|
| 207 |
+
chatInput.value = '';
|
| 208 |
+
appendMessage('Thinking...', 'ai', true); // Show loading indicator
|
| 209 |
+
|
| 210 |
+
fetch('/api/ask', {
|
| 211 |
+
method: 'POST',
|
| 212 |
+
headers: { 'Content-Type': 'application/json' },
|
| 213 |
+
body: JSON.stringify({ enrollment_no: enrollmentNo, question: question })
|
| 214 |
+
})
|
| 215 |
+
.then(response => {
|
| 216 |
+
if (!response.ok) {
|
| 217 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 218 |
+
}
|
| 219 |
+
return response.json();
|
| 220 |
+
})
|
| 221 |
+
.then(data => {
|
| 222 |
+
const loadingElement = chatHistory.querySelector('.loading');
|
| 223 |
+
if (loadingElement) {
|
| 224 |
+
loadingElement.parentElement.remove();
|
| 225 |
+
}
|
| 226 |
+
// Apply markdown formatting to the AI's answer before displaying
|
| 227 |
+
appendMessage(data.answer, 'ai', false, true); // Pass true for markdown formatting
|
| 228 |
+
chatHistory.scrollTop = chatHistory.scrollHeight; // Scroll to bottom after adding message
|
| 229 |
+
})
|
| 230 |
+
.catch(error => {
|
| 231 |
+
console.error('Chat error:', error);
|
| 232 |
+
const loadingElement = chatHistory.querySelector('.loading');
|
| 233 |
+
if (loadingElement) {
|
| 234 |
+
loadingElement.parentElement.remove();
|
| 235 |
+
}
|
| 236 |
+
appendMessage('Sorry, an error occurred while fetching the answer.', 'ai', false, true); // Apply markdown to error message too
|
| 237 |
+
});
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// --- Helper Functions ---
|
| 241 |
+
|
| 242 |
+
function displayReport(report) {
|
| 243 |
+
const reportTitleElement = document.getElementById('report-title');
|
| 244 |
+
if (reportTitleElement) {
|
| 245 |
+
reportTitleElement.textContent = `Performance Report for ${studentSelector.options[studentSelector.selectedIndex].text}`;
|
| 246 |
+
}
|
| 247 |
+
// Apply markdown formatting to the summary
|
| 248 |
+
const summaryTextElement = document.getElementById('summary-text');
|
| 249 |
+
if (summaryTextElement) {
|
| 250 |
+
summaryTextElement.innerHTML = convertMarkdownToHTML(report.overall_summary);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// Display resume analysis
|
| 254 |
+
displayResumeAnalysis(report.resume_analysis);
|
| 255 |
+
|
| 256 |
+
const scoresGrid = document.getElementById('scores-grid');
|
| 257 |
+
if (scoresGrid) {
|
| 258 |
+
scoresGrid.innerHTML = '';
|
| 259 |
+
report.detailed_scores.forEach(item => {
|
| 260 |
+
// Apply markdown to justification
|
| 261 |
+
scoresGrid.innerHTML += `
|
| 262 |
+
<div class="score-card">
|
| 263 |
+
<div class="parameter">
|
| 264 |
+
<span>${item.parameter}</span>
|
| 265 |
+
<span class="score">${item.score}/10</span>
|
| 266 |
+
</div>
|
| 267 |
+
<div class="justification">${convertMarkdownToHTML(item.justification)}</div>
|
| 268 |
+
</div>
|
| 269 |
+
`;
|
| 270 |
+
});
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
const createListItems = (items) => items.map(item => `<li>${convertMarkdownToHTML(item)}</li>`).join(''); // Apply to list items
|
| 274 |
+
|
| 275 |
+
const strengthsListElement = document.getElementById('strengths-list');
|
| 276 |
+
if (strengthsListElement) {
|
| 277 |
+
strengthsListElement.innerHTML = createListItems(report.analysis.strengths);
|
| 278 |
+
}
|
| 279 |
+
const weaknessesListElement = document.getElementById('weaknesses-list');
|
| 280 |
+
if (weaknessesListElement) {
|
| 281 |
+
weaknessesListElement.innerHTML = createListItems(report.analysis.weaknesses);
|
| 282 |
+
}
|
| 283 |
+
const adviceListElement = document.getElementById('advice-list');
|
| 284 |
+
if (adviceListElement) {
|
| 285 |
+
adviceListElement.innerHTML = createListItems(report.actionable_advice.recommendations);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// Display YouTube recommendations
|
| 289 |
+
displayYouTubeRecommendations(report.youtube_recommendations);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
function displayResumeAnalysis(resumeAnalysis) {
|
| 293 |
+
// Display skills as tags
|
| 294 |
+
const skillsContainer = document.getElementById('resume-skills');
|
| 295 |
+
if (skillsContainer) {
|
| 296 |
+
skillsContainer.innerHTML = '';
|
| 297 |
+
resumeAnalysis.key_skills.forEach(skill => {
|
| 298 |
+
const tag = document.createElement('span');
|
| 299 |
+
tag.className = 'skill-tag';
|
| 300 |
+
tag.textContent = skill;
|
| 301 |
+
skillsContainer.appendChild(tag);
|
| 302 |
+
});
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
// Display professional links
|
| 307 |
+
const linksContainer = document.getElementById('resume-links');
|
| 308 |
+
if (linksContainer) {
|
| 309 |
+
linksContainer.innerHTML = '';
|
| 310 |
+
resumeAnalysis.professional_links.forEach(link => {
|
| 311 |
+
const li = document.createElement('li');
|
| 312 |
+
const a = document.createElement('a');
|
| 313 |
+
a.href = link;
|
| 314 |
+
a.target = '_blank';
|
| 315 |
+
|
| 316 |
+
// Extract domain for display
|
| 317 |
+
try {
|
| 318 |
+
const url = new URL(link);
|
| 319 |
+
a.textContent = url.hostname.replace('www.', '');
|
| 320 |
+
} catch (e) {
|
| 321 |
+
a.textContent = link;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
li.appendChild(a);
|
| 325 |
+
linksContainer.appendChild(li);
|
| 326 |
+
});
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
// Display missing elements
|
| 331 |
+
const missingContainer = document.getElementById('resume-missing');
|
| 332 |
+
if (missingContainer) {
|
| 333 |
+
missingContainer.innerHTML = '';
|
| 334 |
+
resumeAnalysis.missing_elements.forEach(item => {
|
| 335 |
+
const li = document.createElement('li');
|
| 336 |
+
li.className = 'missing-items';
|
| 337 |
+
// Apply markdown to missing elements (though unlikely to have formatting)
|
| 338 |
+
li.innerHTML = convertMarkdownToHTML(item);
|
| 339 |
+
missingContainer.appendChild(li);
|
| 340 |
+
});
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
function displayYouTubeRecommendations(recommendations) {
|
| 345 |
+
const container = document.getElementById('youtube-recommendations');
|
| 346 |
+
if (!container) {
|
| 347 |
+
console.error("Container #youtube-recommendations not found.");
|
| 348 |
+
return;
|
| 349 |
+
}
|
| 350 |
+
container.innerHTML = '';
|
| 351 |
+
|
| 352 |
+
if (!recommendations || recommendations.length === 0) {
|
| 353 |
+
container.innerHTML = '<p>No YouTube recommendations available for this student.</p>';
|
| 354 |
+
return;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
recommendations.forEach(topic => {
|
| 358 |
+
// Check if this is a topic with videos or a single video
|
| 359 |
+
if (topic.videos && Array.isArray(topic.videos)) {
|
| 360 |
+
// This is a topic with multiple videos
|
| 361 |
+
const topicSection = document.createElement('div');
|
| 362 |
+
topicSection.className = 'topic-section';
|
| 363 |
+
|
| 364 |
+
const topicHeader = document.createElement('h3');
|
| 365 |
+
topicHeader.textContent = topic.topic;
|
| 366 |
+
topicSection.appendChild(topicHeader);
|
| 367 |
+
|
| 368 |
+
const topicReason = document.createElement('p');
|
| 369 |
+
topicReason.className = 'topic-reason';
|
| 370 |
+
// Apply markdown to reason/description
|
| 371 |
+
topicReason.innerHTML = convertMarkdownToHTML(topic.reason);
|
| 372 |
+
topicSection.appendChild(topicReason);
|
| 373 |
+
|
| 374 |
+
const videosContainer = document.createElement('div');
|
| 375 |
+
videosContainer.className = 'videos-container';
|
| 376 |
+
|
| 377 |
+
topic.videos.forEach(video => {
|
| 378 |
+
const card = document.createElement('div');
|
| 379 |
+
card.className = 'youtube-card';
|
| 380 |
+
|
| 381 |
+
// Fix URL formatting - remove extra spaces
|
| 382 |
+
const embedUrl = (video.embed_url || video.url).replace(/\s+/g, '');
|
| 383 |
+
|
| 384 |
+
card.innerHTML = `
|
| 385 |
+
<div class="youtube-embed">
|
| 386 |
+
<iframe src="${embedUrl}"
|
| 387 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 388 |
+
allowfullscreen></iframe>
|
| 389 |
+
</div>
|
| 390 |
+
<div class="youtube-info">
|
| 391 |
+
<h3 class="youtube-title">${video.title}</h3>
|
| 392 |
+
<p class="youtube-reason">${convertMarkdownToHTML(video.reason || video.description)}</p>
|
| 393 |
+
</div>
|
| 394 |
+
`;
|
| 395 |
+
|
| 396 |
+
videosContainer.appendChild(card);
|
| 397 |
+
});
|
| 398 |
+
|
| 399 |
+
topicSection.appendChild(videosContainer);
|
| 400 |
+
container.appendChild(topicSection);
|
| 401 |
+
} else {
|
| 402 |
+
// This is a single video (fallback case)
|
| 403 |
+
const card = document.createElement('div');
|
| 404 |
+
card.className = 'youtube-card';
|
| 405 |
+
|
| 406 |
+
// Fix URL formatting - remove extra spaces
|
| 407 |
+
const embedUrl = (topic.embed_url || topic.url).replace(/\s+/g, '');
|
| 408 |
+
|
| 409 |
+
card.innerHTML = `
|
| 410 |
+
<div class="youtube-embed">
|
| 411 |
+
<iframe src="${embedUrl}"
|
| 412 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 413 |
+
allowfullscreen></iframe>
|
| 414 |
+
</div>
|
| 415 |
+
<div class="youtube-info">
|
| 416 |
+
<h3 class="youtube-title">${topic.title || 'Untitled Video'}</h3>
|
| 417 |
+
<p class="youtube-reason">${convertMarkdownToHTML(topic.reason || topic.description)}</p>
|
| 418 |
+
</div>
|
| 419 |
+
`;
|
| 420 |
+
|
| 421 |
+
container.appendChild(card);
|
| 422 |
+
}
|
| 423 |
+
});
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function displayJobAnalysis(data) {
|
| 427 |
+
console.log("Job analysis ", data); // Debug log
|
| 428 |
+
|
| 429 |
+
// --- Map new structure to expected display structure ---
|
| 430 |
+
|
| 431 |
+
// 1. Display Strategic Overview (Summary and Key Opportunity)
|
| 432 |
+
const reportTitleElement = document.getElementById('report-title'); // Reuse title element or create a new one if needed for job analysis
|
| 433 |
+
if (reportTitleElement) {
|
| 434 |
+
reportTitleElement.textContent = `Job Application Analysis`; // Or use data.strategic_overview.summary if it's a full sentence
|
| 435 |
+
}
|
| 436 |
+
// Assuming you might want to display summary and key opportunity in a dedicated area if available
|
| 437 |
+
// const overviewSummaryElement = document.getElementById('overview-summary'); // Add this element in HTML if needed
|
| 438 |
+
// if (overviewSummaryElement && data.strategic_overview && data.strategic_overview.summary) {
|
| 439 |
+
// overviewSummaryElement.innerHTML = convertMarkdownToHTML(data.strategic_overview.summary);
|
| 440 |
+
// }
|
| 441 |
+
// const keyOpportunityElement = document.getElementById('key-opportunity'); // Add this element in HTML if needed
|
| 442 |
+
// if (keyOpportunityElement && data.strategic_overview && data.strategic_overview.your_key_opportunity) {
|
| 443 |
+
// keyOpportunityElement.innerHTML = convertMarkdownToHTML(data.strategic_overview.your_key_opportunity);
|
| 444 |
+
// }
|
| 445 |
+
|
| 446 |
+
// 2. Display Strengths (from 'your_core_strengths_for_this_role')
|
| 447 |
+
const strengthsContainer = document.getElementById('job-strengths-list');
|
| 448 |
+
if (strengthsContainer) {
|
| 449 |
+
strengthsContainer.innerHTML = '';
|
| 450 |
+
|
| 451 |
+
const strengths = data['your_core_strengths_for_this_role'] || [];
|
| 452 |
+
|
| 453 |
+
if (Array.isArray(strengths) && strengths.length > 0) {
|
| 454 |
+
strengths.forEach(strength => {
|
| 455 |
+
const item = document.createElement('div');
|
| 456 |
+
item.className = 'job-strength-item';
|
| 457 |
+
// Map the new keys to the expected keys in the HTML structure
|
| 458 |
+
// Apply markdown formatting to description and relevance
|
| 459 |
+
item.innerHTML = `
|
| 460 |
+
<div class="job-item-aspect">${convertMarkdownToHTML(strength.strength_area || 'N/A')}</div>
|
| 461 |
+
<div class="job-item-description">${convertMarkdownToHTML(strength.evidence_from_your_profile || 'N/A')}</div>
|
| 462 |
+
<div class="job-item-relevance">${convertMarkdownToHTML(strength.how_it_matches_the_job || 'N/A')}</div>
|
| 463 |
+
`;
|
| 464 |
+
strengthsContainer.appendChild(item);
|
| 465 |
+
});
|
| 466 |
+
} else {
|
| 467 |
+
strengthsContainer.innerHTML = '<p class="no-data">No strengths data available.</p>';
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
// 3. Display Weaknesses (from 'strategic_areas_for_growth')
|
| 472 |
+
const weaknessesContainer = document.getElementById('job-weaknesses-list');
|
| 473 |
+
if (weaknessesContainer) {
|
| 474 |
+
weaknessesContainer.innerHTML = '';
|
| 475 |
+
|
| 476 |
+
const weaknesses = data['strategic_areas_for_growth'] || [];
|
| 477 |
+
|
| 478 |
+
if (Array.isArray(weaknesses) && weaknesses.length > 0) {
|
| 479 |
+
weaknesses.forEach(weakness => {
|
| 480 |
+
const item = document.createElement('div');
|
| 481 |
+
item.className = 'job-weakness-item';
|
| 482 |
+
// Map the new keys to the expected keys in the HTML structure
|
| 483 |
+
// Apply markdown formatting to description, importance, and suggestion
|
| 484 |
+
item.innerHTML = `
|
| 485 |
+
<div class="job-item-aspect">${convertMarkdownToHTML(weakness.area_to_develop || 'N/A')}</div>
|
| 486 |
+
<div class="job-item-description">${convertMarkdownToHTML(weakness.insight || 'N/A')}</div>
|
| 487 |
+
<div class="job-item-importance">${convertMarkdownToHTML("Importance: " + (weakness.severity || 'N/A'))}</div>
|
| 488 |
+
<div class="job-item-suggestion">${convertMarkdownToHTML(weakness.path_to_improvement ? weakness.path_to_improvement.join('<br>') : 'N/A')}</div>
|
| 489 |
+
`;
|
| 490 |
+
weaknessesContainer.appendChild(item);
|
| 491 |
+
});
|
| 492 |
+
} else {
|
| 493 |
+
weaknessesContainer.innerHTML = '<p class="no-data">No weaknesses data available.</p>';
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// 4. Display Enhancement Recommendations (from 'strategic_areas_for_growth' as well)
|
| 498 |
+
const enhancementsContainer = document.getElementById('job-enhancements-list');
|
| 499 |
+
if (enhancementsContainer) {
|
| 500 |
+
enhancementsContainer.innerHTML = '';
|
| 501 |
+
|
| 502 |
+
// We can reuse the 'strategic_areas_for_growth' for enhancement recommendations
|
| 503 |
+
const weaknessesForRecs = data['strategic_areas_for_growth'] || []; // Use the same array
|
| 504 |
+
if (Array.isArray(weaknessesForRecs) && weaknessesForRecs.length > 0) {
|
| 505 |
+
weaknessesForRecs.forEach(rec => {
|
| 506 |
+
const item = document.createElement('div');
|
| 507 |
+
item.className = 'job-enhancement-item';
|
| 508 |
+
// Map the new keys to the expected keys in the HTML structure
|
| 509 |
+
// Apply markdown formatting to description and priority
|
| 510 |
+
item.innerHTML = `
|
| 511 |
+
<div class="job-item-aspect">${convertMarkdownToHTML(rec.area_to_develop || 'N/A')}</div>
|
| 512 |
+
<div class="job-item-description">${convertMarkdownToHTML(rec.path_to_improvement ? rec.path_to_improvement.join('<br>') : 'N/A')}</div>
|
| 513 |
+
<div class="job-item-importance">${convertMarkdownToHTML("Priority: " + (rec.severity || 'N/A'))}</div>
|
| 514 |
+
`;
|
| 515 |
+
enhancementsContainer.appendChild(item);
|
| 516 |
+
});
|
| 517 |
+
} else {
|
| 518 |
+
enhancementsContainer.innerHTML = '<p class="no-data">No enhancement recommendations available.</p>';
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
// 5. Display YouTube recommendations (should already be in the correct format)
|
| 524 |
+
displayJobYouTubeRecommendations(data.video_recommendations);
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
function displayJobYouTubeRecommendations(recommendations) {
|
| 528 |
+
const container = document.getElementById('job-youtube-recommendations');
|
| 529 |
+
if (!container) {
|
| 530 |
+
console.error("Container #job-youtube-recommendations not found.");
|
| 531 |
+
return;
|
| 532 |
+
}
|
| 533 |
+
container.innerHTML = '';
|
| 534 |
+
|
| 535 |
+
if (!recommendations || !Array.isArray(recommendations) || recommendations.length === 0) {
|
| 536 |
+
container.innerHTML = '<p class="no-data">No YouTube recommendations available for this job application.</p>';
|
| 537 |
+
return;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
recommendations.forEach(topic => {
|
| 541 |
+
// Check if this is a topic with videos or a single video
|
| 542 |
+
if (topic.videos && Array.isArray(topic.videos)) {
|
| 543 |
+
// This is a topic with multiple videos
|
| 544 |
+
const topicSection = document.createElement('div');
|
| 545 |
+
topicSection.className = 'topic-section';
|
| 546 |
+
|
| 547 |
+
const topicHeader = document.createElement('h3');
|
| 548 |
+
topicHeader.textContent = topic.topic || 'Recommended Topic';
|
| 549 |
+
topicSection.appendChild(topicHeader);
|
| 550 |
+
|
| 551 |
+
const topicReason = document.createElement('p');
|
| 552 |
+
topicReason.className = 'topic-reason';
|
| 553 |
+
// Apply markdown to reason
|
| 554 |
+
topicReason.innerHTML = convertMarkdownToHTML(topic.reason || 'Recommended to improve your skills');
|
| 555 |
+
topicSection.appendChild(topicReason);
|
| 556 |
+
|
| 557 |
+
const videosContainer = document.createElement('div');
|
| 558 |
+
videosContainer.className = 'videos-container';
|
| 559 |
+
|
| 560 |
+
topic.videos.forEach(video => {
|
| 561 |
+
const card = document.createElement('div');
|
| 562 |
+
card.className = 'youtube-card';
|
| 563 |
+
|
| 564 |
+
// Fix URL formatting - remove extra spaces
|
| 565 |
+
const embedUrl = (video.embed_url || video.url).replace(/\s+/g, '');
|
| 566 |
+
|
| 567 |
+
card.innerHTML = `
|
| 568 |
+
<div class="youtube-embed">
|
| 569 |
+
<iframe src="${embedUrl}"
|
| 570 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 571 |
+
allowfullscreen></iframe>
|
| 572 |
+
</div>
|
| 573 |
+
<div class="youtube-info">
|
| 574 |
+
<h3 class="youtube-title">${video.title || 'Untitled Video'}</h3>
|
| 575 |
+
<p class="youtube-reason">${convertMarkdownToHTML(video.reason || video.description || 'Recommended for skill development')}</p>
|
| 576 |
+
</div>
|
| 577 |
+
`;
|
| 578 |
+
|
| 579 |
+
videosContainer.appendChild(card);
|
| 580 |
+
});
|
| 581 |
+
|
| 582 |
+
topicSection.appendChild(videosContainer);
|
| 583 |
+
container.appendChild(topicSection);
|
| 584 |
+
} else {
|
| 585 |
+
// This is a single video (fallback case)
|
| 586 |
+
const card = document.createElement('div');
|
| 587 |
+
card.className = 'youtube-card';
|
| 588 |
+
|
| 589 |
+
// Fix URL formatting - remove extra spaces
|
| 590 |
+
const embedUrl = (topic.embed_url || topic.url).replace(/\s+/g, '');
|
| 591 |
+
|
| 592 |
+
card.innerHTML = `
|
| 593 |
+
<div class="youtube-embed">
|
| 594 |
+
<iframe src="${embedUrl}"
|
| 595 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 596 |
+
allowfullscreen></iframe>
|
| 597 |
+
</div>
|
| 598 |
+
<div class="youtube-info">
|
| 599 |
+
<h3 class="youtube-title">${topic.title || 'Untitled Video'}</h3>
|
| 600 |
+
<p class="youtube-reason">${convertMarkdownToHTML(topic.reason || topic.description || 'Recommended for skill development')}</p>
|
| 601 |
+
</div>
|
| 602 |
+
`;
|
| 603 |
+
|
| 604 |
+
container.appendChild(card);
|
| 605 |
+
}
|
| 606 |
+
});
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// --- Updated Dashboard Data Loading Function with Chart.js ---
|
| 610 |
+
async function loadDashboardData() {
|
| 611 |
+
console.log("Loading dashboard data for selected student...");
|
| 612 |
+
const selectedEnrollment = studentSelector.value;
|
| 613 |
+
|
| 614 |
+
// Clear previous charts *first* to prevent infinite stretching if new data fails
|
| 615 |
+
if (skillsChart) { skillsChart.destroy(); skillsChart = null; }
|
| 616 |
+
if (dsaChart) { dsaChart.destroy(); dsaChart = null; }
|
| 617 |
+
if (jobMatchChart) { jobMatchChart.destroy(); jobMatchChart = null; }
|
| 618 |
+
|
| 619 |
+
// Get canvas elements and their contexts
|
| 620 |
+
const skillsCanvas = document.getElementById('skills-chart-canvas');
|
| 621 |
+
const dsaCanvas = document.getElementById('dsa-chart-canvas');
|
| 622 |
+
const jobMatchCanvas = document.getElementById('job-match-chart-canvas');
|
| 623 |
+
|
| 624 |
+
// Check if canvases exist before proceeding
|
| 625 |
+
if (!skillsCanvas || !dsaCanvas || !jobMatchCanvas) {
|
| 626 |
+
console.error("One or more dashboard chart canvases not found in the DOM.");
|
| 627 |
+
return;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
const skillsCtx = skillsCanvas.getContext('2d');
|
| 631 |
+
const dsaCtx = dsaCanvas.getContext('2d');
|
| 632 |
+
const jobMatchCtx = jobMatchCanvas.getContext('2d');
|
| 633 |
+
|
| 634 |
+
// Clear canvases
|
| 635 |
+
if (skillsCtx) skillsCtx.clearRect(0, 0, skillsCtx.canvas.width, skillsCtx.canvas.height);
|
| 636 |
+
if (dsaCtx) dsaCtx.clearRect(0, 0, dsaCtx.canvas.width, dsaCtx.canvas.height);
|
| 637 |
+
if (jobMatchCtx) jobMatchCtx.clearRect(0, 0, jobMatchCtx.canvas.width, jobMatchCtx.canvas.height);
|
| 638 |
+
|
| 639 |
+
if (!selectedEnrollment) {
|
| 640 |
+
console.log("No student selected, showing placeholder.");
|
| 641 |
+
// Show placeholder text on canvases if contexts are available
|
| 642 |
+
[skillsCtx, dsaCtx, jobMatchCtx].forEach(ctx => {
|
| 643 |
+
if (ctx) { // Check if context exists
|
| 644 |
+
ctx.font = "16px Arial";
|
| 645 |
+
ctx.fillStyle = "gray";
|
| 646 |
+
ctx.textAlign = "center";
|
| 647 |
+
ctx.fillText("Select a student to see details", ctx.canvas.width / 2, ctx.canvas.height / 2);
|
| 648 |
+
}
|
| 649 |
+
});
|
| 650 |
+
// Reset metric counters if needed (e.g., to 0 or N/A)
|
| 651 |
+
const totalStudentsEl = document.querySelector('#total-students-count');
|
| 652 |
+
const reportsGeneratedEl = document.querySelector('#reports-generated-count');
|
| 653 |
+
const jobAnalysesEl = document.querySelector('#job-analyses-count');
|
| 654 |
+
if (totalStudentsEl) totalStudentsEl.textContent = 'N/A';
|
| 655 |
+
if (reportsGeneratedEl) reportsGeneratedEl.textContent = 'N/A';
|
| 656 |
+
if (jobAnalysesEl) jobAnalysesEl.textContent = 'N/A';
|
| 657 |
+
return; // Exit if no student is selected
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
try {
|
| 661 |
+
console.log(`Fetching dashboard metrics for enrollment: ${selectedEnrollment}`);
|
| 662 |
+
const response = await fetch(`/api/dashboard/metrics/${selectedEnrollment}`);
|
| 663 |
+
if (!response.ok) {
|
| 664 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 665 |
+
}
|
| 666 |
+
const dashboardData = await response.json();
|
| 667 |
+
console.log("Dashboard data for selected student:", dashboardData);
|
| 668 |
+
|
| 669 |
+
// --- Update Dashboard Elements ---
|
| 670 |
+
// 1. Metrics Cards (Example: Using data from student's profile if available)
|
| 671 |
+
// document.querySelector('#total-students-count').textContent = dashboardData.academics.cgpa || 0; // Example for CGPA
|
| 672 |
+
// This section requires more specific data points from your dashboard analyzer output.
|
| 673 |
+
|
| 674 |
+
// 2. Skills Distribution Chart
|
| 675 |
+
if (dashboardData.skills_distribution && skillsCtx) { // Check if data and context exist
|
| 676 |
+
const topSkills = Object.entries(dashboardData.skills_distribution)
|
| 677 |
+
.sort((a, b) => b[1] - a[1])
|
| 678 |
+
.slice(0, 5); // Get top 5
|
| 679 |
+
const labels = topSkills.map(item => item[0]);
|
| 680 |
+
const data = topSkills.map(item => item[1]);
|
| 681 |
+
|
| 682 |
+
skillsChart = new Chart(skillsCtx, {
|
| 683 |
+
type: 'bar', // or 'doughnut', 'pie', etc.
|
| 684 |
+
{
|
| 685 |
+
labels: labels,
|
| 686 |
+
datasets: [{
|
| 687 |
+
label: 'Skill Count',
|
| 688 |
+
data: data,
|
| 689 |
+
backgroundColor: [
|
| 690 |
+
'rgba(255, 99, 132, 0.2)',
|
| 691 |
+
'rgba(54, 162, 235, 0.2)',
|
| 692 |
+
'rgba(255, 205, 86, 0.2)',
|
| 693 |
+
'rgba(75, 192, 192, 0.2)',
|
| 694 |
+
'rgba(153, 102, 255, 0.2)'
|
| 695 |
+
],
|
| 696 |
+
borderColor: [
|
| 697 |
+
'rgb(255, 99, 132)',
|
| 698 |
+
'rgb(54, 162, 235)',
|
| 699 |
+
'rgb(255, 205, 86)',
|
| 700 |
+
'rgb(75, 192, 192)',
|
| 701 |
+
'rgb(153, 102, 255)'
|
| 702 |
+
],
|
| 703 |
+
borderWidth: 1
|
| 704 |
+
}]
|
| 705 |
+
},
|
| 706 |
+
options: {
|
| 707 |
+
responsive: true,
|
| 708 |
+
maintainAspectRatio: false, // Allows the chart to fill its container
|
| 709 |
+
scales: {
|
| 710 |
+
y: {
|
| 711 |
+
beginAtZero: true
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
}
|
| 715 |
+
});
|
| 716 |
+
} else if (skillsCtx) { // Check context exists before drawing
|
| 717 |
+
// If no data, show a message
|
| 718 |
+
skillsCtx.font = "16px Arial";
|
| 719 |
+
skillsCtx.fillStyle = "gray";
|
| 720 |
+
skillsCtx.textAlign = "center";
|
| 721 |
+
skillsCtx.fillText("No Skills Data Available", skillsCtx.canvas.width / 2, skillsCtx.canvas.height / 2);
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
|
| 725 |
+
// 3. DSA Performance Chart
|
| 726 |
+
if (dashboardData.coding_profiles?.leetcode?.score !== undefined && dsaCtx) { // Check if data and context exist
|
| 727 |
+
// Example: Using a radial gauge or a simple bar chart for score
|
| 728 |
+
// For simplicity, let's use a bar chart here showing score out of 10
|
| 729 |
+
dsaChart = new Chart(dsaCtx, {
|
| 730 |
+
type: 'bar', // Could be 'radar', 'doughnut' for score
|
| 731 |
+
{
|
| 732 |
+
labels: ['DSA Score'],
|
| 733 |
+
datasets: [{
|
| 734 |
+
label: 'DSA Performance (0-10)',
|
| 735 |
+
data: [dashboardData.coding_profiles.leetcode.score],
|
| 736 |
+
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
| 737 |
+
borderColor: 'rgb(75, 192, 192)',
|
| 738 |
+
borderWidth: 1
|
| 739 |
+
}]
|
| 740 |
+
},
|
| 741 |
+
options: {
|
| 742 |
+
responsive: true,
|
| 743 |
+
maintainAspectRatio: false,
|
| 744 |
+
scales: {
|
| 745 |
+
y: {
|
| 746 |
+
beginAtZero: true,
|
| 747 |
+
max: 10 // Set max scale to 10
|
| 748 |
+
}
|
| 749 |
+
}
|
| 750 |
+
}
|
| 751 |
+
});
|
| 752 |
+
} else if (dsaCtx) { // Check context exists before drawing
|
| 753 |
+
// If no data, show a message
|
| 754 |
+
dsaCtx.font = "16px Arial";
|
| 755 |
+
dsaCtx.fillStyle = "gray";
|
| 756 |
+
dsaCtx.textAlign = "center";
|
| 757 |
+
dsaCtx.fillText("No DSA Data Available", dsaCtx.canvas.width / 2, dsaCtx.canvas.height / 2);
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
// 4. Job Match Score Chart (Placeholder - requires specific data from job analysis)
|
| 761 |
+
// This would need data from a job analysis call, which is tied to a specific job link.
|
| 762 |
+
// For now, show a placeholder or message.
|
| 763 |
+
if (jobMatchCtx) { // Check context exists before drawing
|
| 764 |
+
jobMatchCtx.font = "16px Arial";
|
| 765 |
+
jobMatchCtx.fillStyle = "gray";
|
| 766 |
+
jobMatchCtx.textAlign = "center";
|
| 767 |
+
jobMatchCtx.fillText("Job Match Score (requires Job Analysis)", jobMatchCtx.canvas.width / 2, jobMatchCtx.canvas.height / 2);
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
} catch (error) {
|
| 771 |
+
console.error('Error loading dashboard data for selected student:', error);
|
| 772 |
+
// Show error message on canvases if contexts are available
|
| 773 |
+
[skillsCtx, dsaCtx, jobMatchCtx].forEach(ctx => {
|
| 774 |
+
if (ctx) { // Check if context exists
|
| 775 |
+
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); // Clear again just in case
|
| 776 |
+
ctx.font = "16px Arial";
|
| 777 |
+
ctx.fillStyle = "red";
|
| 778 |
+
ctx.textAlign = "center";
|
| 779 |
+
ctx.fillText("Error Loading Data", ctx.canvas.width / 2, ctx.canvas.height / 2);
|
| 780 |
+
}
|
| 781 |
+
});
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
// Example: Static metrics for Total Students, Reports Generated, Job Analyses
|
| 785 |
+
// These would ideally come from a server-side counter or an aggregate API call
|
| 786 |
+
// For now, keep them static or update based on global counts if available separately
|
| 787 |
+
// const totalStudentsEl = document.querySelector('#total-students-count');
|
| 788 |
+
// const reportsGeneratedEl = document.querySelector('#reports-generated-count');
|
| 789 |
+
// const jobAnalysesEl = document.querySelector('#job-analyses-count');
|
| 790 |
+
// if (totalStudentsEl) totalStudentsEl.textContent = '1'; // Total Students
|
| 791 |
+
// if (reportsGeneratedEl) reportsGeneratedEl.textContent = '0'; // Reports Generated
|
| 792 |
+
// if (jobAnalysesEl) jobAnalysesEl.textContent = '0'; // Job Analyses
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
// Modified appendMessage function to accept a markdown flag
|
| 797 |
+
function appendMessage(text, sender, isLoading = false, useMarkdown = false) {
|
| 798 |
+
const messageWrapper = document.createElement('div');
|
| 799 |
+
messageWrapper.classList.add('chat-message', `${sender}-message`);
|
| 800 |
+
|
| 801 |
+
const messageP = document.createElement('p');
|
| 802 |
+
// Apply markdown formatting if the flag is true (e.g., for AI responses)
|
| 803 |
+
if (useMarkdown) {
|
| 804 |
+
messageP.innerHTML = text; // Use innerHTML to render the formatted HTML
|
| 805 |
+
} else {
|
| 806 |
+
messageP.textContent = text; // Use textContent for plain text (e.g., user messages, loading)
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
if (isLoading) {
|
| 810 |
+
messageP.classList.add('loading');
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
messageWrapper.appendChild(messageP);
|
| 814 |
+
chatHistory.appendChild(messageWrapper);
|
| 815 |
+
chatHistory.scrollTop = chatHistory.scrollHeight; // Auto-scroll to bottom
|
| 816 |
+
}
|
| 817 |
+
});
|
templates/final.html
CHANGED
|
@@ -4,403 +4,190 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>AI Student Analyzer</title>
|
| 7 |
-
|
| 8 |
-
<
|
| 9 |
-
<
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
--bg-color: #f0f2f5;
|
| 14 |
-
--card-bg: #ffffff;
|
| 15 |
-
--text-color: #333;
|
| 16 |
-
--text-light: #666;
|
| 17 |
-
--primary-color: #007bff;
|
| 18 |
-
--border-color: #e0e0e0;
|
| 19 |
-
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
body {
|
| 23 |
-
font-family: 'Inter', sans-serif;
|
| 24 |
-
background-color: var(--bg-color);
|
| 25 |
-
color: var(--text-color);
|
| 26 |
-
margin: 0;
|
| 27 |
-
padding: 20px;
|
| 28 |
-
line-height: 1.6;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
.container {
|
| 32 |
-
max-width: 900px;
|
| 33 |
-
margin: 0 auto;
|
| 34 |
-
background: var(--card-bg);
|
| 35 |
-
border-radius: 12px;
|
| 36 |
-
box-shadow: var(--shadow);
|
| 37 |
-
padding: 30px;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
header {
|
| 41 |
-
text-align: center;
|
| 42 |
-
margin-bottom: 30px;
|
| 43 |
-
border-bottom: 1px solid var(--border-color);
|
| 44 |
-
padding-bottom: 20px;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
header h1 {
|
| 48 |
-
margin: 0;
|
| 49 |
-
font-size: 2rem;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
header p {
|
| 53 |
-
color: var(--text-light);
|
| 54 |
-
font-size: 1.1rem;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
.controls {
|
| 58 |
-
display: flex;
|
| 59 |
-
gap: 15px;
|
| 60 |
-
margin-bottom: 20px;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
#student-selector, button {
|
| 64 |
-
padding: 12px 18px;
|
| 65 |
-
border: 1px solid var(--border-color);
|
| 66 |
-
border-radius: 8px;
|
| 67 |
-
font-size: 1rem;
|
| 68 |
-
font-family: 'Inter', sans-serif;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
#student-selector {
|
| 72 |
-
flex-grow: 1;
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
button {
|
| 76 |
-
background-color: var(--primary-color);
|
| 77 |
-
color: white;
|
| 78 |
-
font-weight: 600;
|
| 79 |
-
cursor: pointer;
|
| 80 |
-
transition: background-color 0.2s;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
button:disabled {
|
| 84 |
-
background-color: #ccc;
|
| 85 |
-
cursor: not-allowed;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
button:hover:not(:disabled) {
|
| 89 |
-
background-color: #0056b3;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
.hidden { display: none !important; }
|
| 93 |
-
|
| 94 |
-
/* Spinner */
|
| 95 |
-
.spinner {
|
| 96 |
-
border: 4px solid rgba(0,0,0,0.1);
|
| 97 |
-
width: 36px;
|
| 98 |
-
height: 36px;
|
| 99 |
-
border-radius: 50%;
|
| 100 |
-
border-left-color: var(--primary-color);
|
| 101 |
-
animation: spin 1s ease infinite;
|
| 102 |
-
margin: 20px auto;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
@keyframes spin {
|
| 106 |
-
0% { transform: rotate(0deg); }
|
| 107 |
-
100% { transform: rotate(360deg); }
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
/* Report Styling */
|
| 111 |
-
#report-title {
|
| 112 |
-
text-align: center;
|
| 113 |
-
margin-top: 30px;
|
| 114 |
-
}
|
| 115 |
-
.report-section {
|
| 116 |
-
background-color: var(--bg-color);
|
| 117 |
-
padding: 20px;
|
| 118 |
-
border-radius: 8px;
|
| 119 |
-
margin-bottom: 20px;
|
| 120 |
-
}
|
| 121 |
-
.report-section h3, .report-section h4 {
|
| 122 |
-
margin-top: 0;
|
| 123 |
-
color: var(--primary-color);
|
| 124 |
-
border-bottom: 2px solid var(--border-color);
|
| 125 |
-
padding-bottom: 8px;
|
| 126 |
-
margin-bottom: 15px;
|
| 127 |
-
}
|
| 128 |
-
.report-section ul {
|
| 129 |
-
padding-left: 20px;
|
| 130 |
-
line-height: 1.7;
|
| 131 |
-
}
|
| 132 |
-
#scores-grid {
|
| 133 |
-
display: grid;
|
| 134 |
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 135 |
-
gap: 15px;
|
| 136 |
-
}
|
| 137 |
-
.score-card {
|
| 138 |
-
background: var(--card-bg);
|
| 139 |
-
padding: 15px;
|
| 140 |
-
border-radius: 8px;
|
| 141 |
-
border-left: 4px solid var(--primary-color);
|
| 142 |
-
box-shadow: 0 2px 4px rgba(0,0,0,0.03);
|
| 143 |
-
}
|
| 144 |
-
.score-card .parameter {
|
| 145 |
-
font-weight: 600;
|
| 146 |
-
font-size: 1.1rem;
|
| 147 |
-
display: flex;
|
| 148 |
-
justify-content: space-between;
|
| 149 |
-
align-items: center;
|
| 150 |
-
}
|
| 151 |
-
.score-card .score {
|
| 152 |
-
font-size: 1.5rem;
|
| 153 |
-
font-weight: 700;
|
| 154 |
-
}
|
| 155 |
-
.score-card .justification {
|
| 156 |
-
color: var(--text-light);
|
| 157 |
-
font-size: 0.9rem;
|
| 158 |
-
margin-top: 8px;
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
/* Chatbot Styling */
|
| 162 |
-
#chatbot-container {
|
| 163 |
-
margin-top: 30px;
|
| 164 |
-
border-top: 1px solid var(--border-color);
|
| 165 |
-
padding-top: 20px;
|
| 166 |
-
}
|
| 167 |
-
#chat-history {
|
| 168 |
-
height: 300px;
|
| 169 |
-
overflow-y: auto;
|
| 170 |
-
border: 1px solid var(--border-color);
|
| 171 |
-
border-radius: 8px;
|
| 172 |
-
padding: 15px;
|
| 173 |
-
margin-bottom: 15px;
|
| 174 |
-
background-color: #fafafa;
|
| 175 |
-
}
|
| 176 |
-
.chat-message {
|
| 177 |
-
margin-bottom: 15px;
|
| 178 |
-
line-height: 1.5;
|
| 179 |
-
display: flex;
|
| 180 |
-
flex-direction: column;
|
| 181 |
-
}
|
| 182 |
-
.user-message {
|
| 183 |
-
align-items: flex-end;
|
| 184 |
-
}
|
| 185 |
-
.user-message p {
|
| 186 |
-
background-color: var(--primary-color);
|
| 187 |
-
color: white;
|
| 188 |
-
border-radius: 15px 15px 0 15px;
|
| 189 |
-
}
|
| 190 |
-
.ai-message {
|
| 191 |
-
align-items: flex-start;
|
| 192 |
-
}
|
| 193 |
-
.ai-message p {
|
| 194 |
-
background-color: #e9ecef;
|
| 195 |
-
border-radius: 15px 15px 15px 0;
|
| 196 |
-
}
|
| 197 |
-
.chat-message p {
|
| 198 |
-
display: inline-block;
|
| 199 |
-
padding: 10px 15px;
|
| 200 |
-
max-width: 80%;
|
| 201 |
-
margin: 0;
|
| 202 |
-
}
|
| 203 |
-
#chat-form {
|
| 204 |
-
display: flex;
|
| 205 |
-
gap: 10px;
|
| 206 |
-
}
|
| 207 |
-
#chat-input {
|
| 208 |
-
flex-grow: 1;
|
| 209 |
-
}
|
| 210 |
-
</style>
|
| 211 |
</head>
|
| 212 |
<body>
|
| 213 |
-
<div class="container">
|
| 214 |
-
<
|
| 215 |
-
|
| 216 |
-
<
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
-
|
| 220 |
-
<
|
| 221 |
-
<
|
| 222 |
-
|
| 223 |
-
<
|
| 224 |
-
</div>
|
| 225 |
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
<
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
</div>
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
</div>
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
</div>
|
| 248 |
</div>
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
</div>
|
| 253 |
-
</
|
| 254 |
-
</
|
| 255 |
-
|
| 256 |
-
<!-- Chatbot Interface -->
|
| 257 |
-
<div id="chatbot-container" class="hidden">
|
| 258 |
-
<h2>Ask a Question</h2>
|
| 259 |
-
<div id="chat-history"></div>
|
| 260 |
-
<form id="chat-form">
|
| 261 |
-
<input type="text" id="chat-input" placeholder="e.g., What are their strongest DSA topics?" autocomplete="off" required>
|
| 262 |
-
<button type="submit">Send</button>
|
| 263 |
-
</form>
|
| 264 |
-
</div>
|
| 265 |
</div>
|
| 266 |
|
| 267 |
-
<
|
| 268 |
-
<script>
|
| 269 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 270 |
-
const studentSelector = document.getElementById('student-selector');
|
| 271 |
-
const generateReportBtn = document.getElementById('generate-report-btn');
|
| 272 |
-
const loadingSpinner = document.getElementById('loading-spinner');
|
| 273 |
-
const reportContainer = document.getElementById('report-container');
|
| 274 |
-
const chatbotContainer = document.getElementById('chatbot-container');
|
| 275 |
-
const chatForm = document.getElementById('chat-form');
|
| 276 |
-
const chatInput = document.getElementById('chat-input');
|
| 277 |
-
const chatHistory = document.getElementById('chat-history');
|
| 278 |
-
|
| 279 |
-
// 1. Populate student dropdown on page load
|
| 280 |
-
fetch('/api/students')
|
| 281 |
-
.then(response => response.json())
|
| 282 |
-
.then(students => {
|
| 283 |
-
students.forEach(student => {
|
| 284 |
-
const option = document.createElement('option');
|
| 285 |
-
option.value = student.enrollment_no;
|
| 286 |
-
option.textContent = `${student.name} (${student.enrollment_no})`;
|
| 287 |
-
studentSelector.appendChild(option);
|
| 288 |
-
});
|
| 289 |
-
})
|
| 290 |
-
.catch(error => console.error('Error fetching students:', error));
|
| 291 |
-
|
| 292 |
-
// 2. Enable button when a student is selected
|
| 293 |
-
studentSelector.addEventListener('change', () => {
|
| 294 |
-
const hasSelection = !!studentSelector.value;
|
| 295 |
-
generateReportBtn.disabled = !hasSelection;
|
| 296 |
-
chatbotContainer.classList.toggle('hidden', !hasSelection);
|
| 297 |
-
reportContainer.classList.add('hidden'); // Hide old report on new selection
|
| 298 |
-
chatHistory.innerHTML = ''; // Clear chat history
|
| 299 |
-
});
|
| 300 |
-
|
| 301 |
-
// 3. Handle "Generate Report" button click
|
| 302 |
-
generateReportBtn.addEventListener('click', () => {
|
| 303 |
-
const enrollmentNo = studentSelector.value;
|
| 304 |
-
if (!enrollmentNo) return;
|
| 305 |
-
|
| 306 |
-
loadingSpinner.classList.remove('hidden');
|
| 307 |
-
reportContainer.classList.add('hidden');
|
| 308 |
-
chatbotContainer.classList.add('hidden');
|
| 309 |
-
|
| 310 |
-
fetch(`/api/report/${enrollmentNo}`)
|
| 311 |
-
.then(response => response.json())
|
| 312 |
-
.then(report => {
|
| 313 |
-
loadingSpinner.classList.add('hidden');
|
| 314 |
-
if (report.error) {
|
| 315 |
-
alert(`Error generating report: ${report.error}`);
|
| 316 |
-
} else {
|
| 317 |
-
displayReport(report);
|
| 318 |
-
reportContainer.classList.remove('hidden');
|
| 319 |
-
chatbotContainer.classList.remove('hidden');
|
| 320 |
-
}
|
| 321 |
-
})
|
| 322 |
-
.catch(error => {
|
| 323 |
-
loadingSpinner.classList.add('hidden');
|
| 324 |
-
alert(`An unexpected error occurred: ${error}`);
|
| 325 |
-
});
|
| 326 |
-
});
|
| 327 |
-
|
| 328 |
-
// 4. Handle chat form submission
|
| 329 |
-
chatForm.addEventListener('submit', (e) => {
|
| 330 |
-
e.preventDefault();
|
| 331 |
-
const enrollmentNo = studentSelector.value;
|
| 332 |
-
const question = chatInput.value.trim();
|
| 333 |
-
|
| 334 |
-
if (!question || !enrollmentNo) return;
|
| 335 |
-
|
| 336 |
-
appendMessage(question, 'user');
|
| 337 |
-
chatInput.value = '';
|
| 338 |
-
appendMessage('Thinking...', 'ai', true); // Show loading indicator
|
| 339 |
-
|
| 340 |
-
fetch('/api/ask', {
|
| 341 |
-
method: 'POST',
|
| 342 |
-
headers: { 'Content-Type': 'application/json' },
|
| 343 |
-
body: JSON.stringify({ enrollment_no: enrollmentNo, question: question })
|
| 344 |
-
})
|
| 345 |
-
.then(response => response.json())
|
| 346 |
-
.then(data => {
|
| 347 |
-
const loadingElement = chatHistory.querySelector('.loading');
|
| 348 |
-
if (loadingElement) {
|
| 349 |
-
loadingElement.parentElement.remove();
|
| 350 |
-
}
|
| 351 |
-
appendMessage(data.answer, 'ai');
|
| 352 |
-
})
|
| 353 |
-
.catch(error => {
|
| 354 |
-
const loadingElement = chatHistory.querySelector('.loading');
|
| 355 |
-
if (loadingElement) {
|
| 356 |
-
loadingElement.parentElement.remove();
|
| 357 |
-
}
|
| 358 |
-
appendMessage('Sorry, an error occurred while fetching the answer.', 'ai');
|
| 359 |
-
});
|
| 360 |
-
});
|
| 361 |
-
|
| 362 |
-
// --- Helper Functions ---
|
| 363 |
-
|
| 364 |
-
function displayReport(report) {
|
| 365 |
-
document.getElementById('report-title').textContent = `Performance Report for ${studentSelector.options[studentSelector.selectedIndex].text}`;
|
| 366 |
-
document.getElementById('summary-text').textContent = report.overall_summary;
|
| 367 |
-
|
| 368 |
-
const scoresGrid = document.getElementById('scores-grid');
|
| 369 |
-
scoresGrid.innerHTML = '';
|
| 370 |
-
report.detailed_scores.forEach(item => {
|
| 371 |
-
scoresGrid.innerHTML += `
|
| 372 |
-
<div class="score-card">
|
| 373 |
-
<div class="parameter">
|
| 374 |
-
<span>${item.parameter}</span>
|
| 375 |
-
<span class="score">${item.score}/10</span>
|
| 376 |
-
</div>
|
| 377 |
-
<div class="justification">${item.justification}</div>
|
| 378 |
-
</div>
|
| 379 |
-
`;
|
| 380 |
-
});
|
| 381 |
-
|
| 382 |
-
const createListItems = (items) => items.map(item => `<li>${item}</li>`).join('');
|
| 383 |
-
|
| 384 |
-
document.getElementById('strengths-list').innerHTML = createListItems(report.analysis.strengths);
|
| 385 |
-
document.getElementById('weaknesses-list').innerHTML = createListItems(report.analysis.weaknesses);
|
| 386 |
-
document.getElementById('advice-list').innerHTML = createListItems(report.actionable_advice.recommendations);
|
| 387 |
-
}
|
| 388 |
-
|
| 389 |
-
function appendMessage(text, sender, isLoading = false) {
|
| 390 |
-
const messageWrapper = document.createElement('div');
|
| 391 |
-
messageWrapper.classList.add('chat-message', `${sender}-message`);
|
| 392 |
-
|
| 393 |
-
const messageP = document.createElement('p');
|
| 394 |
-
messageP.textContent = text;
|
| 395 |
-
if (isLoading) {
|
| 396 |
-
messageP.classList.add('loading');
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
messageWrapper.appendChild(messageP);
|
| 400 |
-
chatHistory.appendChild(messageWrapper);
|
| 401 |
-
chatHistory.scrollTop = chatHistory.scrollHeight; // Auto-scroll to bottom
|
| 402 |
-
}
|
| 403 |
-
});
|
| 404 |
-
</script>
|
| 405 |
</body>
|
| 406 |
</html>
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>AI Student Analyzer</title>
|
| 7 |
+
<link rel="stylesheet" href="../static/final.css">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
+
<!-- Include Chart.js from CDN -->
|
| 12 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
+
<div class="app-container">
|
| 16 |
+
<!-- Sidebar -->
|
| 17 |
+
<aside class="sidebar">
|
| 18 |
+
<header class="app-header">
|
| 19 |
+
<h1>Ai</h1>
|
| 20 |
+
<p>AI Student Analyzer</p>
|
| 21 |
+
</header>
|
| 22 |
+
<nav class="main-nav">
|
| 23 |
+
<ul>
|
| 24 |
+
<li><a href="#dashboard" class="nav-link active">Dashboard</a></li>
|
| 25 |
+
<li><a href="#students" class="nav-link">Students</a></li>
|
| 26 |
+
<li><a href="#reports" class="nav-link">Reports</a></li>
|
| 27 |
+
<li><a href="#job-analysis" class="nav-link">Job Analysis</a></li>
|
| 28 |
+
<li><a href="#chat" class="nav-link">Chat</a></li>
|
| 29 |
+
</ul>
|
| 30 |
+
</nav>
|
| 31 |
+
</aside>
|
| 32 |
+
|
| 33 |
+
<!-- Main Content Area -->
|
| 34 |
+
<main class="main-content">
|
| 35 |
+
<!-- Top Control Bar -->
|
| 36 |
+
<header class="top-bar">
|
| 37 |
+
<div class="input-group">
|
| 38 |
+
<label for="student-selector">Select Student</label>
|
| 39 |
+
<select id="student-selector">
|
| 40 |
+
<option value="">-- Select a Student --</option>
|
| 41 |
+
<!-- Options will be populated by JS -->
|
| 42 |
+
</select>
|
| 43 |
+
<button id="generate-report-btn" class="btn btn-secondary" disabled>Generate Report</button>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="input-group">
|
| 46 |
+
<label for="job-application-input">Job Application Link</label>
|
| 47 |
+
<input type="text" id="job-application-input" placeholder="Paste job application link here...">
|
| 48 |
+
<button id="analyze-job-btn" class="btn btn-primary" disabled>Analyze</button>
|
| 49 |
+
</div>
|
| 50 |
+
</header>
|
| 51 |
|
| 52 |
+
<!-- Loading Spinner -->
|
| 53 |
+
<div id="loading-spinner" class="loading-overlay hidden">
|
| 54 |
+
<div class="spinner"></div>
|
| 55 |
+
<p>Processing...</p>
|
| 56 |
+
</div>
|
|
|
|
| 57 |
|
| 58 |
+
<!-- Dashboard / Overview -->
|
| 59 |
+
<section id="dashboard" class="content-section active">
|
| 60 |
+
<h2>Dashboard Overview</h2>
|
| 61 |
+
<div class="dashboard-grid">
|
| 62 |
+
<!-- Metric Cards Row -->
|
| 63 |
+
<div class="metric-cards-grid">
|
| 64 |
+
<div class="card">
|
| 65 |
+
<h3>Total Students</h3>
|
| 66 |
+
<p class="metric" id="total-students-count">1</p> <!-- ID added for JS -->
|
| 67 |
+
</div>
|
| 68 |
+
<div class="card">
|
| 69 |
+
<h3>Reports Generated</h3>
|
| 70 |
+
<p class="metric" id="reports-generated-count">0</p> <!-- ID added for JS -->
|
| 71 |
+
</div>
|
| 72 |
+
<div class="card">
|
| 73 |
+
<h3>Job Analyses</h3>
|
| 74 |
+
<p class="metric" id="job-analyses-count">0</p> <!-- ID added for JS -->
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
|
| 78 |
+
<!-- Large Card for Skills Distribution -->
|
| 79 |
+
<div class="card large-card chart-container">
|
| 80 |
+
<h3>Skills Distribution (for selected student)</h3>
|
| 81 |
+
<!-- Replace placeholder with canvas for Chart.js -->
|
| 82 |
+
<canvas id="skills-chart-canvas"></canvas>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
<!-- Row for DSA and Job Match -->
|
| 86 |
+
<div class="chart-row">
|
| 87 |
+
<div class="card chart-container">
|
| 88 |
+
<h3>DSA Performance (for selected student)</h3>
|
| 89 |
+
<!-- Replace placeholder with canvas for Chart.js -->
|
| 90 |
+
<canvas id="dsa-chart-canvas"></canvas>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="card chart-container">
|
| 93 |
+
<h3>Job Match Score (for selected student)</h3>
|
| 94 |
+
<!-- Replace placeholder with canvas for Chart.js -->
|
| 95 |
+
<canvas id="job-match-chart-canvas"></canvas>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
</div>
|
| 99 |
+
</section>
|
| 100 |
+
|
| 101 |
+
<!-- Student Report Display Area -->
|
| 102 |
+
<section id="reports" class="content-section hidden">
|
| 103 |
+
<h2 id="report-title">Student Performance Report</h2>
|
| 104 |
+
<div class="report-grid">
|
| 105 |
+
<div class="report-section summary card">
|
| 106 |
+
<h3>HR Summary</h3>
|
| 107 |
+
<p id="summary-text">Summary will appear here...</p>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="report-section resume card">
|
| 110 |
+
<h3>Resume Analysis</h3>
|
| 111 |
+
<div class="resume-section">
|
| 112 |
+
<div class="resume-column">
|
| 113 |
+
<h4>Key Skills</h4>
|
| 114 |
+
<div class="skills-list" id="resume-skills"></div>
|
| 115 |
+
<h4 style="margin-top: 20px;">Professional Links</h4>
|
| 116 |
+
<ul class="links-list" id="resume-links"></ul>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="resume-column">
|
| 119 |
+
<h4>Missing Elements</h4>
|
| 120 |
+
<ul id="resume-missing"></ul>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="report-section scores card">
|
| 125 |
+
<h3>Detailed Scores</h3>
|
| 126 |
+
<div id="scores-grid"></div>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="report-section analysis card">
|
| 129 |
+
<h3>Analysis</h3>
|
| 130 |
+
<div id="analysis-content">
|
| 131 |
+
<div class="analysis-grid">
|
| 132 |
+
<div class="analysis-column">
|
| 133 |
+
<h4>Strengths</h4>
|
| 134 |
+
<ul id="strengths-list"></ul>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="analysis-column">
|
| 137 |
+
<h4>Weaknesses</h4>
|
| 138 |
+
<ul id="weaknesses-list"></ul>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="report-section advice card">
|
| 144 |
+
<h3>Actionable Advice</h3>
|
| 145 |
+
<ul id="advice-list"></ul>
|
| 146 |
+
</div>
|
| 147 |
+
<div class="report-section youtube card">
|
| 148 |
+
<h3>Recommended Learning Resources</h3>
|
| 149 |
+
<div class="youtube-recommendations" id="youtube-recommendations"></div>
|
| 150 |
+
</div>
|
| 151 |
</div>
|
| 152 |
+
</section>
|
| 153 |
+
|
| 154 |
+
<!-- Job Application Analysis Display Area -->
|
| 155 |
+
<section id="job-analysis" class="content-section hidden">
|
| 156 |
+
<h2>Job Application Analysis</h2>
|
| 157 |
+
<div class="job-analysis-grid">
|
| 158 |
+
<div class="job-section job-strengths card">
|
| 159 |
+
<h3>Strengths</h3>
|
| 160 |
+
<div id="job-strengths-list"></div>
|
| 161 |
+
</div>
|
| 162 |
+
<div class="job-section job-weaknesses card">
|
| 163 |
+
<h3>Weaknesses</h3>
|
| 164 |
+
<div id="job-weaknesses-list"></div>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="job-section job-enhancements card">
|
| 167 |
+
<h3>Enhancement Recommendations</h3>
|
| 168 |
+
<div id="job-enhancements-list"></div>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="job-section youtube card">
|
| 171 |
+
<h3>Recommended Learning Resources</h3>
|
| 172 |
+
<div class="youtube-recommendations" id="job-youtube-recommendations"></div>
|
| 173 |
</div>
|
| 174 |
</div>
|
| 175 |
+
</section>
|
| 176 |
+
|
| 177 |
+
<!-- Chatbot Interface -->
|
| 178 |
+
<section id="chat" class="content-section hidden">
|
| 179 |
+
<h2>Student Q&A</h2>
|
| 180 |
+
<div class="chat-container card">
|
| 181 |
+
<div id="chat-history"></div>
|
| 182 |
+
<form id="chat-form">
|
| 183 |
+
<input type="text" id="chat-input" placeholder="e.g., What are their strongest DSA topics?" autocomplete="off" required>
|
| 184 |
+
<button type="submit" class="btn btn-primary">Send</button>
|
| 185 |
+
</form>
|
| 186 |
</div>
|
| 187 |
+
</section>
|
| 188 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
</div>
|
| 190 |
|
| 191 |
+
<script src="../static/final.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
</body>
|
| 193 |
</html>
|
templates/script.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
const studentSelector = document.getElementById('student-selector');
|
| 3 |
+
const generateReportBtn = document.getElementById('generate-report-btn');
|
| 4 |
+
const jobApplicationInput = document.getElementById('job-application-input');
|
| 5 |
+
const analyzeJobBtn = document.getElementById('analyze-job-btn');
|
| 6 |
+
const loadingSpinner = document.getElementById('loading-spinner');
|
| 7 |
+
const reportContainer = document.getElementById('report-container');
|
| 8 |
+
const jobAnalysisContainer = document.getElementById('job-analysis-container');
|
| 9 |
+
const chatbotContainer = document.getElementById('chatbot-container');
|
| 10 |
+
const chatForm = document.getElementById('chat-form');
|
| 11 |
+
const chatInput = document.getElementById('chat-input');
|
| 12 |
+
const chatHistory = document.getElementById('chat-history');
|
| 13 |
+
|
| 14 |
+
// 1. Populate student dropdown on page load
|
| 15 |
+
fetch('/api/students')
|
| 16 |
+
.then(response => response.json())
|
| 17 |
+
.then(students => {
|
| 18 |
+
students.forEach(student => {
|
| 19 |
+
const option = document.createElement('option');
|
| 20 |
+
option.value = student.enrollment_no;
|
| 21 |
+
option.textContent = `${student.name} (${student.enrollment_no})`;
|
| 22 |
+
studentSelector.appendChild(option);
|
| 23 |
+
});
|
| 24 |
+
})
|
| 25 |
+
.catch(error => console.error('Error fetching students:', error));
|
| 26 |
+
|
| 27 |
+
// 2. Enable buttons when inputs are filled
|
| 28 |
+
studentSelector.addEventListener('change', () => {
|
| 29 |
+
const hasSelection = !!studentSelector.value;
|
| 30 |
+
generateReportBtn.disabled = !hasSelection;
|
| 31 |
+
chatbotContainer.classList.toggle('hidden', !hasSelection);
|
| 32 |
+
reportContainer.classList.add('hidden'); // Hide old report on new selection
|
| 33 |
+
chatHistory.innerHTML = ''; // Clear chat history
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
jobApplicationInput.addEventListener('input', () => {
|
| 37 |
+
analyzeJobBtn.disabled = !jobApplicationInput.value.trim();
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// 3. Handle "Generate Report" button click
|
| 41 |
+
generateReportBtn.addEventListener('click', () => {
|
| 42 |
+
const enrollmentNo = studentSelector.value;
|
| 43 |
+
if (!enrollmentNo) return;
|
| 44 |
+
|
| 45 |
+
loadingSpinner.classList.remove('hidden');
|
| 46 |
+
reportContainer.classList.add('hidden');
|
| 47 |
+
jobAnalysisContainer.classList.add('hidden');
|
| 48 |
+
chatbotContainer.classList.add('hidden');
|
| 49 |
+
|
| 50 |
+
fetch(`/api/report/${enrollmentNo}`)
|
| 51 |
+
.then(response => {
|
| 52 |
+
if (!response.ok) {
|
| 53 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 54 |
+
}
|
| 55 |
+
return response.json();
|
| 56 |
+
})
|
| 57 |
+
.then(report => {
|
| 58 |
+
loadingSpinner.classList.add('hidden');
|
| 59 |
+
if (report.error) {
|
| 60 |
+
alert(`Error generating report: ${report.error}`);
|
| 61 |
+
} else {
|
| 62 |
+
displayReport(report);
|
| 63 |
+
reportContainer.classList.remove('hidden');
|
| 64 |
+
chatbotContainer.classList.remove('hidden');
|
| 65 |
+
}
|
| 66 |
+
})
|
| 67 |
+
.catch(error => {
|
| 68 |
+
loadingSpinner.classList.add('hidden');
|
| 69 |
+
console.error('Report generation error:', error);
|
| 70 |
+
alert(`An unexpected error occurred: ${error.message}`);
|
| 71 |
+
});
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
// 4. Handle "Analyze Job Application" button click
|
| 75 |
+
analyzeJobBtn.addEventListener('click', () => {
|
| 76 |
+
const jobApplicationLink = jobApplicationInput.value.trim();
|
| 77 |
+
if (!jobApplicationLink) return;
|
| 78 |
+
|
| 79 |
+
loadingSpinner.classList.remove('hidden');
|
| 80 |
+
reportContainer.classList.add('hidden');
|
| 81 |
+
jobAnalysisContainer.classList.add('hidden');
|
| 82 |
+
chatbotContainer.classList.add('hidden');
|
| 83 |
+
|
| 84 |
+
fetch('/api/job-analysis', {
|
| 85 |
+
method: 'POST',
|
| 86 |
+
headers: { 'Content-Type': 'application/json' },
|
| 87 |
+
body: JSON.stringify({ job_application_link: jobApplicationLink })
|
| 88 |
+
})
|
| 89 |
+
.then(response => {
|
| 90 |
+
if (!response.ok) {
|
| 91 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 92 |
+
}
|
| 93 |
+
return response.json();
|
| 94 |
+
})
|
| 95 |
+
.then(data => {
|
| 96 |
+
loadingSpinner.classList.add('hidden');
|
| 97 |
+
if (data.error) {
|
| 98 |
+
alert(`Error analyzing job application: ${data.error}`);
|
| 99 |
+
} else {
|
| 100 |
+
displayJobAnalysis(data.data); // Access data.data as per API response structure
|
| 101 |
+
jobAnalysisContainer.classList.remove('hidden');
|
| 102 |
+
}
|
| 103 |
+
})
|
| 104 |
+
.catch(error => {
|
| 105 |
+
loadingSpinner.classList.add('hidden');
|
| 106 |
+
console.error('Job analysis error:', error);
|
| 107 |
+
alert(`An unexpected error occurred: ${error.message}`);
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
// 5. Handle chat form submission
|
| 112 |
+
chatForm.addEventListener('submit', (e) => {
|
| 113 |
+
e.preventDefault();
|
| 114 |
+
const enrollmentNo = studentSelector.value;
|
| 115 |
+
const question = chatInput.value.trim();
|
| 116 |
+
|
| 117 |
+
if (!question || !enrollmentNo) return;
|
| 118 |
+
|
| 119 |
+
appendMessage(question, 'user');
|
| 120 |
+
chatInput.value = '';
|
| 121 |
+
appendMessage('Thinking...', 'ai', true); // Show loading indicator
|
| 122 |
+
|
| 123 |
+
fetch('/api/ask', {
|
| 124 |
+
method: 'POST',
|
| 125 |
+
headers: { 'Content-Type': 'application/json' },
|
| 126 |
+
body: JSON.stringify({ enrollment_no: enrollmentNo, question: question })
|
| 127 |
+
})
|
| 128 |
+
.then(response => {
|
| 129 |
+
if (!response.ok) {
|
| 130 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 131 |
+
}
|
| 132 |
+
return response.json();
|
| 133 |
+
})
|
| 134 |
+
.then(data => {
|
| 135 |
+
const loadingElement = chatHistory.querySelector('.loading');
|
| 136 |
+
if (loadingElement) {
|
| 137 |
+
loadingElement.parentElement.remove();
|
| 138 |
+
}
|
| 139 |
+
appendMessage(data.answer, 'ai');
|
| 140 |
+
})
|
| 141 |
+
.catch(error => {
|
| 142 |
+
console.error('Chat error:', error);
|
| 143 |
+
const loadingElement = chatHistory.querySelector('.loading');
|
| 144 |
+
if (loadingElement) {
|
| 145 |
+
loadingElement.parentElement.remove();
|
| 146 |
+
}
|
| 147 |
+
appendMessage('Sorry, an error occurred while fetching the answer.', 'ai');
|
| 148 |
+
});
|
| 149 |
+
});
|
| 150 |
+
|
| 151 |
+
// --- Helper Functions ---
|
| 152 |
+
|
| 153 |
+
function displayReport(report) {
|
| 154 |
+
document.getElementById('report-title').textContent = `Performance Report for ${studentSelector.options[studentSelector.selectedIndex].text}`;
|
| 155 |
+
document.getElementById('summary-text').textContent = report.overall_summary;
|
| 156 |
+
|
| 157 |
+
// Display resume analysis
|
| 158 |
+
displayResumeAnalysis(report.resume_analysis);
|
| 159 |
+
|
| 160 |
+
const scoresGrid = document.getElementById('scores-grid');
|
| 161 |
+
scoresGrid.innerHTML = '';
|
| 162 |
+
report.detailed_scores.forEach(item => {
|
| 163 |
+
scoresGrid.innerHTML += `
|
| 164 |
+
<div class="score-card">
|
| 165 |
+
<div class="parameter">
|
| 166 |
+
<span>${item.parameter}</span>
|
| 167 |
+
<span class="score">${item.score}/10</span>
|
| 168 |
+
</div>
|
| 169 |
+
<div class="justification">${item.justification}</div>
|
| 170 |
+
</div>
|
| 171 |
+
`;
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
const createListItems = (items) => items.map(item => `<li>${item}</li>`).join('');
|
| 175 |
+
|
| 176 |
+
document.getElementById('strengths-list').innerHTML = createListItems(report.analysis.strengths);
|
| 177 |
+
document.getElementById('weaknesses-list').innerHTML = createListItems(report.analysis.weaknesses);
|
| 178 |
+
document.getElementById('advice-list').innerHTML = createListItems(report.actionable_advice.recommendations);
|
| 179 |
+
|
| 180 |
+
// Display YouTube recommendations
|
| 181 |
+
displayYouTubeRecommendations(report.youtube_recommendations);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function displayResumeAnalysis(resumeAnalysis) {
|
| 185 |
+
// Display skills as tags
|
| 186 |
+
const skillsContainer = document.getElementById('resume-skills');
|
| 187 |
+
skillsContainer.innerHTML = '';
|
| 188 |
+
resumeAnalysis.key_skills.forEach(skill => {
|
| 189 |
+
const tag = document.createElement('span');
|
| 190 |
+
tag.className = 'skill-tag';
|
| 191 |
+
tag.textContent = skill;
|
| 192 |
+
skillsContainer.appendChild(tag);
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
// Display professional links
|
| 196 |
+
const linksContainer = document.getElementById('resume-links');
|
| 197 |
+
linksContainer.innerHTML = '';
|
| 198 |
+
resumeAnalysis.professional_links.forEach(link => {
|
| 199 |
+
const li = document.createElement('li');
|
| 200 |
+
const a = document.createElement('a');
|
| 201 |
+
a.href = link;
|
| 202 |
+
a.target = '_blank';
|
| 203 |
+
|
| 204 |
+
// Extract domain for display
|
| 205 |
+
try {
|
| 206 |
+
const url = new URL(link);
|
| 207 |
+
a.textContent = url.hostname.replace('www.', '');
|
| 208 |
+
} catch (e) {
|
| 209 |
+
a.textContent = link;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
li.appendChild(a);
|
| 213 |
+
linksContainer.appendChild(li);
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// Display missing elements
|
| 217 |
+
const missingContainer = document.getElementById('resume-missing');
|
| 218 |
+
missingContainer.innerHTML = '';
|
| 219 |
+
resumeAnalysis.missing_elements.forEach(item => {
|
| 220 |
+
const li = document.createElement('li');
|
| 221 |
+
li.className = 'missing-items';
|
| 222 |
+
li.textContent = item;
|
| 223 |
+
missingContainer.appendChild(li);
|
| 224 |
+
});
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
function displayYouTubeRecommendations(recommendations) {
|
| 228 |
+
const container = document.getElementById('youtube-recommendations');
|
| 229 |
+
container.innerHTML = '';
|
| 230 |
+
|
| 231 |
+
if (!recommendations || recommendations.length === 0) {
|
| 232 |
+
container.innerHTML = '<p>No YouTube recommendations available for this student.</p>';
|
| 233 |
+
return;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
recommendations.forEach(topic => {
|
| 237 |
+
// Check if this is a topic with videos or a single video
|
| 238 |
+
if (topic.videos && Array.isArray(topic.videos)) {
|
| 239 |
+
// This is a topic with multiple videos
|
| 240 |
+
const topicSection = document.createElement('div');
|
| 241 |
+
topicSection.className = 'topic-section';
|
| 242 |
+
|
| 243 |
+
const topicHeader = document.createElement('h3');
|
| 244 |
+
topicHeader.textContent = topic.topic;
|
| 245 |
+
topicSection.appendChild(topicHeader);
|
| 246 |
+
|
| 247 |
+
const topicReason = document.createElement('p');
|
| 248 |
+
topicReason.className = 'topic-reason';
|
| 249 |
+
topicReason.textContent = topic.reason;
|
| 250 |
+
topicSection.appendChild(topicReason);
|
| 251 |
+
|
| 252 |
+
const videosContainer = document.createElement('div');
|
| 253 |
+
videosContainer.className = 'videos-container';
|
| 254 |
+
|
| 255 |
+
topic.videos.forEach(video => {
|
| 256 |
+
const card = document.createElement('div');
|
| 257 |
+
card.className = 'youtube-card';
|
| 258 |
+
|
| 259 |
+
// Fix URL formatting - remove extra spaces
|
| 260 |
+
const embedUrl = (video.embed_url || video.url).replace(/\s+/g, '');
|
| 261 |
+
|
| 262 |
+
card.innerHTML = `
|
| 263 |
+
<div class="youtube-embed">
|
| 264 |
+
<iframe src="${embedUrl}"
|
| 265 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 266 |
+
allowfullscreen></iframe>
|
| 267 |
+
</div>
|
| 268 |
+
<div class="youtube-info">
|
| 269 |
+
<h3 class="youtube-title">${video.title}</h3>
|
| 270 |
+
<p class="youtube-reason">${video.reason || video.description}</p>
|
| 271 |
+
</div>
|
| 272 |
+
`;
|
| 273 |
+
|
| 274 |
+
videosContainer.appendChild(card);
|
| 275 |
+
});
|
| 276 |
+
|
| 277 |
+
topicSection.appendChild(videosContainer);
|
| 278 |
+
container.appendChild(topicSection);
|
| 279 |
+
} else {
|
| 280 |
+
// This is a single video (fallback case)
|
| 281 |
+
const card = document.createElement('div');
|
| 282 |
+
card.className = 'youtube-card';
|
| 283 |
+
|
| 284 |
+
// Fix URL formatting - remove extra spaces
|
| 285 |
+
const embedUrl = (topic.embed_url || topic.url).replace(/\s+/g, '');
|
| 286 |
+
|
| 287 |
+
card.innerHTML = `
|
| 288 |
+
<div class="youtube-embed">
|
| 289 |
+
<iframe src="${embedUrl}"
|
| 290 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 291 |
+
allowfullscreen></iframe>
|
| 292 |
+
</div>
|
| 293 |
+
<div class="youtube-info">
|
| 294 |
+
<h3 class="youtube-title">${topic.title}</h3>
|
| 295 |
+
<p class="youtube-reason">${topic.reason || topic.description}</p>
|
| 296 |
+
</div>
|
| 297 |
+
`;
|
| 298 |
+
|
| 299 |
+
container.appendChild(card);
|
| 300 |
+
}
|
| 301 |
+
});
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
function displayJobAnalysis(data) {
|
| 305 |
+
console.log("Job analysis data:", data); // Debug log
|
| 306 |
+
|
| 307 |
+
// Display strengths
|
| 308 |
+
const strengthsContainer = document.getElementById('job-strengths-list');
|
| 309 |
+
strengthsContainer.innerHTML = '';
|
| 310 |
+
|
| 311 |
+
// Check if strengths exist and is an array
|
| 312 |
+
if (data.strengths && Array.isArray(data.strengths)) {
|
| 313 |
+
data.strengths.forEach(strength => {
|
| 314 |
+
const item = document.createElement('div');
|
| 315 |
+
item.className = 'job-strength-item';
|
| 316 |
+
item.innerHTML = `
|
| 317 |
+
<div class="job-item-aspect">${strength.aspect || 'N/A'}</div>
|
| 318 |
+
<div class="job-item-description">${strength.description || 'N/A'}</div>
|
| 319 |
+
<div class="job-item-relevance">${strength.relevance || 'N/A'}</div>
|
| 320 |
+
`;
|
| 321 |
+
strengthsContainer.appendChild(item);
|
| 322 |
+
});
|
| 323 |
+
} else {
|
| 324 |
+
strengthsContainer.innerHTML = '<p>No strengths data available.</p>';
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// Display weaknesses
|
| 328 |
+
const weaknessesContainer = document.getElementById('job-weaknesses-list');
|
| 329 |
+
weaknessesContainer.innerHTML = '';
|
| 330 |
+
|
| 331 |
+
// Check if weaknesses exist and is an array
|
| 332 |
+
if (data.weaknesses && Array.isArray(data.weaknesses)) {
|
| 333 |
+
data.weaknesses.forEach(weakness => {
|
| 334 |
+
const item = document.createElement('div');
|
| 335 |
+
item.className = 'job-weakness-item';
|
| 336 |
+
item.innerHTML = `
|
| 337 |
+
<div class="job-item-aspect">${weakness.aspect || 'N/A'}</div>
|
| 338 |
+
<div class="job-item-description">${weakness.description || 'N/A'}</div>
|
| 339 |
+
<div class="job-item-importance">Importance: ${weakness.importance || 'N/A'}</div>
|
| 340 |
+
<div class="job-item-suggestion">${weakness.improvement_suggestion || 'N/A'}</div>
|
| 341 |
+
`;
|
| 342 |
+
weaknessesContainer.appendChild(item);
|
| 343 |
+
});
|
| 344 |
+
} else {
|
| 345 |
+
weaknessesContainer.innerHTML = '<p>No weaknesses data available.</p>';
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
// Display enhancement recommendations
|
| 349 |
+
const enhancementsContainer = document.getElementById('job-enhancements-list');
|
| 350 |
+
enhancementsContainer.innerHTML = '';
|
| 351 |
+
|
| 352 |
+
// Check if enhancement_recommendations exist and is an array
|
| 353 |
+
if (data.enhancement_recommendations && Array.isArray(data.enhancement_recommendations)) {
|
| 354 |
+
data.enhancement_recommendations.forEach(rec => {
|
| 355 |
+
const item = document.createElement('div');
|
| 356 |
+
item.className = 'job-enhancement-item';
|
| 357 |
+
item.innerHTML = `
|
| 358 |
+
<div class="job-item-aspect">${rec.area || 'N/A'}</div>
|
| 359 |
+
<div class="job-item-description">${rec.suggestion || 'N/A'}</div>
|
| 360 |
+
<div class="job-item-importance">Priority: ${rec.priority || 'N/A'}</div>
|
| 361 |
+
`;
|
| 362 |
+
enhancementsContainer.appendChild(item);
|
| 363 |
+
});
|
| 364 |
+
} else {
|
| 365 |
+
enhancementsContainer.innerHTML = '<p>No enhancement recommendations available.</p>';
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// Display YouTube recommendations
|
| 369 |
+
displayJobYouTubeRecommendations(data.video_recommendations);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
function displayJobYouTubeRecommendations(recommendations) {
|
| 373 |
+
const container = document.getElementById('job-youtube-recommendations');
|
| 374 |
+
container.innerHTML = '';
|
| 375 |
+
|
| 376 |
+
if (!recommendations || !Array.isArray(recommendations) || recommendations.length === 0) {
|
| 377 |
+
container.innerHTML = '<p>No YouTube recommendations available for this job application.</p>';
|
| 378 |
+
return;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
recommendations.forEach(topic => {
|
| 382 |
+
// Check if this is a topic with videos or a single video
|
| 383 |
+
if (topic.videos && Array.isArray(topic.videos)) {
|
| 384 |
+
// This is a topic with multiple videos
|
| 385 |
+
const topicSection = document.createElement('div');
|
| 386 |
+
topicSection.className = 'topic-section';
|
| 387 |
+
|
| 388 |
+
const topicHeader = document.createElement('h3');
|
| 389 |
+
topicHeader.textContent = topic.topic || 'Recommended Topic';
|
| 390 |
+
topicSection.appendChild(topicHeader);
|
| 391 |
+
|
| 392 |
+
const topicReason = document.createElement('p');
|
| 393 |
+
topicReason.className = 'topic-reason';
|
| 394 |
+
topicReason.textContent = topic.reason || 'Recommended to improve your skills';
|
| 395 |
+
topicSection.appendChild(topicReason);
|
| 396 |
+
|
| 397 |
+
const videosContainer = document.createElement('div');
|
| 398 |
+
videosContainer.className = 'videos-container';
|
| 399 |
+
|
| 400 |
+
topic.videos.forEach(video => {
|
| 401 |
+
const card = document.createElement('div');
|
| 402 |
+
card.className = 'youtube-card';
|
| 403 |
+
|
| 404 |
+
// Fix URL formatting - remove extra spaces
|
| 405 |
+
const embedUrl = (video.embed_url || video.url).replace(/\s+/g, '');
|
| 406 |
+
|
| 407 |
+
card.innerHTML = `
|
| 408 |
+
<div class="youtube-embed">
|
| 409 |
+
<iframe src="${embedUrl}"
|
| 410 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 411 |
+
allowfullscreen></iframe>
|
| 412 |
+
</div>
|
| 413 |
+
<div class="youtube-info">
|
| 414 |
+
<h3 class="youtube-title">${video.title || 'Untitled Video'}</h3>
|
| 415 |
+
<p class="youtube-reason">${video.reason || video.description || 'Recommended for skill development'}</p>
|
| 416 |
+
</div>
|
| 417 |
+
`;
|
| 418 |
+
|
| 419 |
+
videosContainer.appendChild(card);
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
topicSection.appendChild(videosContainer);
|
| 423 |
+
container.appendChild(topicSection);
|
| 424 |
+
} else {
|
| 425 |
+
// This is a single video (fallback case)
|
| 426 |
+
const card = document.createElement('div');
|
| 427 |
+
card.className = 'youtube-card';
|
| 428 |
+
|
| 429 |
+
// Fix URL formatting - remove extra spaces
|
| 430 |
+
const embedUrl = (topic.embed_url || topic.url).replace(/\s+/g, '');
|
| 431 |
+
|
| 432 |
+
card.innerHTML = `
|
| 433 |
+
<div class="youtube-embed">
|
| 434 |
+
<iframe src="${embedUrl}"
|
| 435 |
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
| 436 |
+
allowfullscreen></iframe>
|
| 437 |
+
</div>
|
| 438 |
+
<div class="youtube-info">
|
| 439 |
+
<h3 class="youtube-title">${topic.title || 'Untitled Video'}</h3>
|
| 440 |
+
<p class="youtube-reason">${topic.reason || topic.description || 'Recommended for skill development'}</p>
|
| 441 |
+
</div>
|
| 442 |
+
`;
|
| 443 |
+
|
| 444 |
+
container.appendChild(card);
|
| 445 |
+
}
|
| 446 |
+
});
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function appendMessage(text, sender, isLoading = false) {
|
| 450 |
+
const messageWrapper = document.createElement('div');
|
| 451 |
+
messageWrapper.classList.add('chat-message', `${sender}-message`);
|
| 452 |
+
|
| 453 |
+
const messageP = document.createElement('p');
|
| 454 |
+
messageP.textContent = text;
|
| 455 |
+
if (isLoading) {
|
| 456 |
+
messageP.classList.add('loading');
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
messageWrapper.appendChild(messageP);
|
| 460 |
+
chatHistory.appendChild(messageWrapper);
|
| 461 |
+
chatHistory.scrollTop = chatHistory.scrollHeight; // Auto-scroll to bottom
|
| 462 |
+
}
|
| 463 |
+
});
|
templates/style.css
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--bg-color: #f0f2f5;
|
| 5 |
+
--card-bg: #ffffff;
|
| 6 |
+
--text-color: #333;
|
| 7 |
+
--text-light: #666;
|
| 8 |
+
--primary-color: #007bff;
|
| 9 |
+
--border-color: #e0e0e0;
|
| 10 |
+
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
| 11 |
+
--job-bg: #f8f9fa;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: 'Inter', sans-serif;
|
| 16 |
+
background-color: var(--bg-color);
|
| 17 |
+
color: var(--text-color);
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
line-height: 1.6;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.container {
|
| 24 |
+
max-width: 900px;
|
| 25 |
+
margin: 0 auto;
|
| 26 |
+
background: var(--card-bg);
|
| 27 |
+
border-radius: 12px;
|
| 28 |
+
box-shadow: var(--shadow);
|
| 29 |
+
padding: 30px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
header {
|
| 33 |
+
text-align: center;
|
| 34 |
+
margin-bottom: 30px;
|
| 35 |
+
border-bottom: 1px solid var(--border-color);
|
| 36 |
+
padding-bottom: 20px;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
header h1 {
|
| 40 |
+
margin: 0;
|
| 41 |
+
font-size: 2rem;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
header p {
|
| 45 |
+
color: var(--text-light);
|
| 46 |
+
font-size: 1.1rem;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.controls {
|
| 50 |
+
display: flex;
|
| 51 |
+
gap: 15px;
|
| 52 |
+
margin-bottom: 20px;
|
| 53 |
+
flex-wrap: wrap;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
#student-selector, #job-application-input, button {
|
| 57 |
+
padding: 12px 18px;
|
| 58 |
+
border: 1px solid var(--border-color);
|
| 59 |
+
border-radius: 8px;
|
| 60 |
+
font-size: 1rem;
|
| 61 |
+
font-family: 'Inter', sans-serif;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
#student-selector, #job-application-input {
|
| 65 |
+
flex-grow: 1;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
#job-application-input {
|
| 69 |
+
flex-grow: 2;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
button {
|
| 73 |
+
background-color: var(--primary-color);
|
| 74 |
+
color: white;
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: background-color 0.2s;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
button:disabled {
|
| 81 |
+
background-color: #ccc;
|
| 82 |
+
cursor: not-allowed;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
button:hover:not(:disabled) {
|
| 86 |
+
background-color: #0056b3;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.hidden { display: none !important; }
|
| 90 |
+
|
| 91 |
+
/* Spinner */
|
| 92 |
+
.spinner {
|
| 93 |
+
border: 4px solid rgba(0,0,0,0.1);
|
| 94 |
+
width: 36px;
|
| 95 |
+
height: 36px;
|
| 96 |
+
border-radius: 50%;
|
| 97 |
+
border-left-color: var(--primary-color);
|
| 98 |
+
animation: spin 1s ease infinite;
|
| 99 |
+
margin: 20px auto;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
@keyframes spin {
|
| 103 |
+
0% { transform: rotate(0deg); }
|
| 104 |
+
100% { transform: rotate(360deg); }
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Report Styling */
|
| 108 |
+
#report-title {
|
| 109 |
+
text-align: center;
|
| 110 |
+
margin-top: 30px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.report-section {
|
| 114 |
+
background-color: var(--bg-color);
|
| 115 |
+
padding: 20px;
|
| 116 |
+
border-radius: 8px;
|
| 117 |
+
margin-bottom: 20px;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.job-section {
|
| 121 |
+
background-color: var(--job-bg);
|
| 122 |
+
border-left: 4px solid #28a745;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.report-section h3, .report-section h4 {
|
| 126 |
+
margin-top: 0;
|
| 127 |
+
color: var(--primary-color);
|
| 128 |
+
border-bottom: 2px solid var(--border-color);
|
| 129 |
+
padding-bottom: 8px;
|
| 130 |
+
margin-bottom: 15px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.job-section h3, .job-section h4 {
|
| 134 |
+
color: #28a745;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.report-section ul {
|
| 138 |
+
padding-left: 20px;
|
| 139 |
+
line-height: 1.7;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
#scores-grid {
|
| 143 |
+
display: grid;
|
| 144 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 145 |
+
gap: 15px;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.score-card {
|
| 149 |
+
background: var(--card-bg);
|
| 150 |
+
padding: 15px;
|
| 151 |
+
border-radius: 8px;
|
| 152 |
+
border-left: 4px solid var(--primary-color);
|
| 153 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.03);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.score-card .parameter {
|
| 157 |
+
font-weight: 600;
|
| 158 |
+
font-size: 1.1rem;
|
| 159 |
+
display: flex;
|
| 160 |
+
justify-content: space-between;
|
| 161 |
+
align-items: center;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.score-card .score {
|
| 165 |
+
font-size: 1.5rem;
|
| 166 |
+
font-weight: 700;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.score-card .justification {
|
| 170 |
+
color: var(--text-light);
|
| 171 |
+
font-size: 0.9rem;
|
| 172 |
+
margin-top: 8px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* Resume Section Styling */
|
| 176 |
+
.resume-section {
|
| 177 |
+
display: grid;
|
| 178 |
+
grid-template-columns: 1fr 1fr;
|
| 179 |
+
gap: 15px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.resume-column {
|
| 183 |
+
background: white;
|
| 184 |
+
padding: 15px;
|
| 185 |
+
border-radius: 8px;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.resume-column h4 {
|
| 189 |
+
margin-top: 0;
|
| 190 |
+
color: var(--primary-color);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.skills-list {
|
| 194 |
+
display: flex;
|
| 195 |
+
flex-wrap: wrap;
|
| 196 |
+
gap: 8px;
|
| 197 |
+
margin-top: 10px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.skill-tag {
|
| 201 |
+
background-color: #e9ecef;
|
| 202 |
+
padding: 5px 10px;
|
| 203 |
+
border-radius: 20px;
|
| 204 |
+
font-size: 0.9rem;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.links-list {
|
| 208 |
+
list-style: none;
|
| 209 |
+
padding-left: 0;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.links-list li {
|
| 213 |
+
margin-bottom: 8px;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.links-list a {
|
| 217 |
+
color: var(--primary-color);
|
| 218 |
+
text-decoration: none;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.links-list a:hover {
|
| 222 |
+
text-decoration: underline;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.missing-items {
|
| 226 |
+
color: #dc3545;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
/* YouTube Recommendations Styling */
|
| 230 |
+
.youtube-recommendations {
|
| 231 |
+
display: grid;
|
| 232 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 233 |
+
gap: 20px;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.youtube-card {
|
| 237 |
+
border: 1px solid var(--border-color);
|
| 238 |
+
border-radius: 8px;
|
| 239 |
+
overflow: hidden;
|
| 240 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.youtube-embed {
|
| 244 |
+
position: relative;
|
| 245 |
+
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
| 246 |
+
height: 0;
|
| 247 |
+
overflow: hidden;
|
| 248 |
+
max-width: 100%;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.youtube-embed iframe {
|
| 252 |
+
position: absolute;
|
| 253 |
+
top: 0;
|
| 254 |
+
left: 0;
|
| 255 |
+
width: 100%;
|
| 256 |
+
height: 100%;
|
| 257 |
+
border: 0;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.youtube-info {
|
| 261 |
+
padding: 15px;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.youtube-title {
|
| 265 |
+
font-weight: 600;
|
| 266 |
+
margin: 0 0 8px 0;
|
| 267 |
+
color: var(--text-color);
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.youtube-reason {
|
| 271 |
+
color: var(--text-light);
|
| 272 |
+
font-size: 0.9rem;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/* Chatbot Styling */
|
| 276 |
+
#chatbot-container {
|
| 277 |
+
margin-top: 30px;
|
| 278 |
+
border-top: 1px solid var(--border-color);
|
| 279 |
+
padding-top: 20px;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
#chat-history {
|
| 283 |
+
height: 300px;
|
| 284 |
+
overflow-y: auto;
|
| 285 |
+
border: 1px solid var(--border-color);
|
| 286 |
+
border-radius: 8px;
|
| 287 |
+
padding: 15px;
|
| 288 |
+
margin-bottom: 15px;
|
| 289 |
+
background-color: #fafafa;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.chat-message {
|
| 293 |
+
margin-bottom: 15px;
|
| 294 |
+
line-height: 1.5;
|
| 295 |
+
display: flex;
|
| 296 |
+
flex-direction: column;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.user-message {
|
| 300 |
+
align-items: flex-end;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.user-message p {
|
| 304 |
+
background-color: var(--primary-color);
|
| 305 |
+
color: white;
|
| 306 |
+
border-radius: 15px 15px 0 15px;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.ai-message {
|
| 310 |
+
align-items: flex-start;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.ai-message p {
|
| 314 |
+
background-color: #e9ecef;
|
| 315 |
+
border-radius: 15px 15px 15px 0;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.chat-message p {
|
| 319 |
+
display: inline-block;
|
| 320 |
+
padding: 10px 15px;
|
| 321 |
+
max-width: 80%;
|
| 322 |
+
margin: 0;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
#chat-form {
|
| 326 |
+
display: flex;
|
| 327 |
+
gap: 10px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
#chat-input {
|
| 331 |
+
flex-grow: 1;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/* Job Application Analysis */
|
| 335 |
+
.job-analysis-container {
|
| 336 |
+
margin-top: 30px;
|
| 337 |
+
padding-top: 20px;
|
| 338 |
+
border-top: 1px solid var(--border-color);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.job-analysis-container h2 {
|
| 342 |
+
margin-top: 0;
|
| 343 |
+
color: #28a745;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.job-section-content {
|
| 347 |
+
margin-top: 20px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.job-strengths, .job-weaknesses, .job-enhancements {
|
| 351 |
+
margin-bottom: 25px;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.job-strengths h4, .job-weaknesses h4, .job-enhancements h4 {
|
| 355 |
+
color: #28a745;
|
| 356 |
+
border-bottom: 1px solid #c3e6cb;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.job-strengths ul, .job-weaknesses ul, .job-enhancements ul {
|
| 360 |
+
padding-left: 20px;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.job-strength-item, .job-weakness-item, .job-enhancement-item {
|
| 364 |
+
margin-bottom: 10px;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.job-item-aspect {
|
| 368 |
+
font-weight: bold;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.job-item-description {
|
| 372 |
+
margin-top: 5px;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.job-item-relevance, .job-item-importance, .job-item-suggestion {
|
| 376 |
+
font-size: 0.9rem;
|
| 377 |
+
color: var(--text-light);
|
| 378 |
+
}
|
youtube_search_tool.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.tools import BaseTool
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import Type, List, Dict, Any, Optional
|
| 4 |
+
import os
|
| 5 |
+
import requests
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
from bs4 import BeautifulSoup
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import random
|
| 12 |
+
import urllib.parse
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger('youtube_search_tool')
|
| 17 |
+
logger.setLevel(logging.INFO)
|
| 18 |
+
if not logger.handlers:
|
| 19 |
+
handler = logging.StreamHandler()
|
| 20 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 21 |
+
handler.setFormatter(formatter)
|
| 22 |
+
logger.addHandler(handler)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class YouTubeSearchInput(BaseModel):
|
| 27 |
+
"""Input schema for YouTube search tool"""
|
| 28 |
+
query: str = Field(..., description="The search query for educational videos")
|
| 29 |
+
max_results: int = Field(default=5, ge=1, le=10, description="Number of results to return (1-10)")
|
| 30 |
+
topic_category: Optional[str] = Field(
|
| 31 |
+
default=None,
|
| 32 |
+
description="Specific topic category to filter results (dsa, web, python, etc.)"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class YouTubeSearchTool(BaseTool):
|
| 38 |
+
"""Tool for searching YouTube for educational videos related to academic topics"""
|
| 39 |
+
|
| 40 |
+
name: str = "YouTube Academic Search"
|
| 41 |
+
description: str = (
|
| 42 |
+
"Searches YouTube for high-quality educational videos related to computer science, "
|
| 43 |
+
"DSA, programming, and academic topics. Returns real video data with working URLs."
|
| 44 |
+
)
|
| 45 |
+
args_schema: Type[BaseModel] = YouTubeSearchInput
|
| 46 |
+
|
| 47 |
+
YOUTUBE_SEARCH_URL: str = "https://www.youtube.com/results" # Fixed: removed extra spaces
|
| 48 |
+
|
| 49 |
+
def __init__(self, **kwargs):
|
| 50 |
+
super().__init__(**kwargs)
|
| 51 |
+
# No API key needed
|
| 52 |
+
|
| 53 |
+
def _run(self, query: str, max_results: int = 5, topic_category: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 54 |
+
"""Execute the YouTube search with the given parameters"""
|
| 55 |
+
logger.info(f"Searching YouTube for: '{query}' (max_results={max_results}, category={topic_category})")
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
# Prepare search query
|
| 59 |
+
search_query = self._enhance_query(query, topic_category)
|
| 60 |
+
|
| 61 |
+
# Prepare request parameters
|
| 62 |
+
params = {
|
| 63 |
+
"search_query": search_query,
|
| 64 |
+
"sp": "EgIQAQ%3D%3D" # This parameter filters for videos only
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# Execute request with headers to mimic a browser
|
| 68 |
+
headers = {
|
| 69 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
| 70 |
+
"Accept-Language": "en-US,en;q=0.9",
|
| 71 |
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
| 72 |
+
"Connection": "keep-alive",
|
| 73 |
+
"Upgrade-Insecure-Requests": "1",
|
| 74 |
+
"Sec-Fetch-Dest": "document",
|
| 75 |
+
"Sec-Fetch-Mode": "navigate",
|
| 76 |
+
"Sec-Fetch-Site": "none",
|
| 77 |
+
"Sec-Fetch-User": "?1"
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
# Add a small delay to avoid being blocked
|
| 81 |
+
time.sleep(random.uniform(0.5, 1.5))
|
| 82 |
+
|
| 83 |
+
response = requests.get(
|
| 84 |
+
self.YOUTUBE_SEARCH_URL,
|
| 85 |
+
params=params,
|
| 86 |
+
headers=headers,
|
| 87 |
+
timeout=15,
|
| 88 |
+
cookies={"CONSENT": "YES+cb.20210328-17-p0.en+FX+100"}
|
| 89 |
+
)
|
| 90 |
+
response.raise_for_status()
|
| 91 |
+
|
| 92 |
+
# Parse the HTML response
|
| 93 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 94 |
+
|
| 95 |
+
# Extract initial data from the page
|
| 96 |
+
scripts = soup.find_all('script')
|
| 97 |
+
initial_data = None
|
| 98 |
+
|
| 99 |
+
for script in scripts:
|
| 100 |
+
if script.string and 'var ytInitialData' in script.string:
|
| 101 |
+
# Extract the JSON data from the script
|
| 102 |
+
start_index = script.string.find('var ytInitialData = ') + len('var ytInitialData = ')
|
| 103 |
+
end_index = script.string.find(';</script>', start_index)
|
| 104 |
+
if end_index == -1:
|
| 105 |
+
end_index = script.string.find(';', start_index)
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
# Handle possible JSONP responses
|
| 109 |
+
json_text = script.string[start_index:end_index].strip()
|
| 110 |
+
if json_text.endswith(')'):
|
| 111 |
+
json_text = json_text[:-1]
|
| 112 |
+
initial_data = json.loads(json_text)
|
| 113 |
+
break
|
| 114 |
+
except json.JSONDecodeError as e:
|
| 115 |
+
logger.debug(f"JSON decode error: {str(e)}")
|
| 116 |
+
continue
|
| 117 |
+
|
| 118 |
+
if not initial_data:
|
| 119 |
+
logger.warning("Could not extract initial data from YouTube page")
|
| 120 |
+
return self._get_fallback_videos(query, max_results, topic_category)
|
| 121 |
+
|
| 122 |
+
# Process results
|
| 123 |
+
videos = []
|
| 124 |
+
try:
|
| 125 |
+
# Navigate through the JSON structure to find video data
|
| 126 |
+
contents = initial_data['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents']
|
| 127 |
+
|
| 128 |
+
for section in contents:
|
| 129 |
+
if 'itemSectionRenderer' in section:
|
| 130 |
+
items = section['itemSectionRenderer']['contents']
|
| 131 |
+
|
| 132 |
+
for item in items:
|
| 133 |
+
if 'videoRenderer' in item:
|
| 134 |
+
video_data = item['videoRenderer']
|
| 135 |
+
|
| 136 |
+
# Extract video information
|
| 137 |
+
video_id = video_data.get('videoId', '')
|
| 138 |
+
if not video_id:
|
| 139 |
+
continue
|
| 140 |
+
|
| 141 |
+
# Get title - handle different possible structures
|
| 142 |
+
title = ""
|
| 143 |
+
if 'title' in video_data and 'runs' in video_data['title']:
|
| 144 |
+
title = video_data['title']['runs'][0].get('text', '')
|
| 145 |
+
elif 'title' in video_data and 'simpleText' in video_data['title']:
|
| 146 |
+
title = video_data['title'].get('simpleText', '')
|
| 147 |
+
|
| 148 |
+
# Get channel name
|
| 149 |
+
channel = ""
|
| 150 |
+
if 'ownerText' in video_data and 'runs' in video_data['ownerText']:
|
| 151 |
+
channel = video_data['ownerText']['runs'][0].get('text', '')
|
| 152 |
+
|
| 153 |
+
# Get description
|
| 154 |
+
description = ""
|
| 155 |
+
if 'descriptionSnippet' in video_data and 'runs' in video_data['descriptionSnippet']:
|
| 156 |
+
description = video_data['descriptionSnippet']['runs'][0].get('text', '')
|
| 157 |
+
|
| 158 |
+
# Get thumbnail URL
|
| 159 |
+
thumbnail_url = ""
|
| 160 |
+
if 'thumbnail' in video_data and 'thumbnails' in video_data['thumbnail']:
|
| 161 |
+
thumbnails = video_data['thumbnail']['thumbnails']
|
| 162 |
+
if thumbnails:
|
| 163 |
+
# Get the highest quality thumbnail
|
| 164 |
+
thumbnail_url = thumbnails[-1].get('url', '')
|
| 165 |
+
|
| 166 |
+
# Filter for relevance
|
| 167 |
+
if self._is_relevant_video(title, description, query, topic_category):
|
| 168 |
+
# FIXED: Removed extra spaces from URLs
|
| 169 |
+
videos.append({
|
| 170 |
+
"title": title,
|
| 171 |
+
"url": f"https://www.youtube.com/watch?v={video_id}",
|
| 172 |
+
"embed_url": f"https://www.youtube.com/embed/{video_id}",
|
| 173 |
+
"channel": channel,
|
| 174 |
+
"description": self._clean_description(description),
|
| 175 |
+
"thumbnail": thumbnail_url,
|
| 176 |
+
"category": self._determine_video_category(
|
| 177 |
+
title,
|
| 178 |
+
description,
|
| 179 |
+
query,
|
| 180 |
+
topic_category
|
| 181 |
+
)
|
| 182 |
+
})
|
| 183 |
+
|
| 184 |
+
if len(videos) >= max_results:
|
| 185 |
+
break
|
| 186 |
+
|
| 187 |
+
if len(videos) >= max_results:
|
| 188 |
+
break
|
| 189 |
+
except (KeyError, IndexError, TypeError) as e:
|
| 190 |
+
logger.error(f"Error parsing YouTube data: {str(e)}")
|
| 191 |
+
return self._get_fallback_videos(query, max_results, topic_category)
|
| 192 |
+
|
| 193 |
+
if not videos:
|
| 194 |
+
logger.warning(f"No relevant videos found for query: '{query}'")
|
| 195 |
+
return self._get_fallback_videos(query, max_results, topic_category)
|
| 196 |
+
|
| 197 |
+
logger.info(f"Found {len(videos)} relevant YouTube videos for query: '{query}'")
|
| 198 |
+
return videos
|
| 199 |
+
|
| 200 |
+
except requests.exceptions.RequestException as e:
|
| 201 |
+
logger.error(f"Network error during YouTube search: {str(e)}")
|
| 202 |
+
return self._get_fallback_videos(query, max_results, topic_category)
|
| 203 |
+
except Exception as e:
|
| 204 |
+
logger.exception(f"Unexpected error during YouTube search: {str(e)}")
|
| 205 |
+
return self._get_fallback_videos(query, max_results, topic_category)
|
| 206 |
+
|
| 207 |
+
async def _arun(self, query: str, max_results: int = 5, topic_category: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 208 |
+
"""Async version of the tool"""
|
| 209 |
+
return self._run(query, max_results, topic_category)
|
| 210 |
+
|
| 211 |
+
def _enhance_query(self, query: str, topic_category: Optional[str] = None) -> str:
|
| 212 |
+
"""Enhance the search query for better educational results"""
|
| 213 |
+
base_query = query
|
| 214 |
+
|
| 215 |
+
# Add educational terms based on topic
|
| 216 |
+
if topic_category:
|
| 217 |
+
topic_lower = topic_category.lower()
|
| 218 |
+
if "dsa" in topic_lower or "algorithm" in topic_lower or "data structure" in topic_lower:
|
| 219 |
+
base_query += " algorithm tutorial"
|
| 220 |
+
elif "web" in topic_lower or "development" in topic_lower:
|
| 221 |
+
base_query += " tutorial"
|
| 222 |
+
elif "python" in topic_lower or "programming" in topic_lower:
|
| 223 |
+
base_query += " programming tutorial"
|
| 224 |
+
elif "operating" in topic_lower or "os" in topic_lower:
|
| 225 |
+
base_query += " tutorial"
|
| 226 |
+
elif "machine learning" in topic_lower or "ml" in topic_lower:
|
| 227 |
+
base_query += " tutorial"
|
| 228 |
+
|
| 229 |
+
# Always add terms for high-quality educational content
|
| 230 |
+
enhanced_query = f"{base_query}"
|
| 231 |
+
|
| 232 |
+
logger.debug(f"Enhanced YouTube query: '{enhanced_query}'")
|
| 233 |
+
return enhanced_query
|
| 234 |
+
|
| 235 |
+
def _is_relevant_video(self, title: str, description: str, query: str, topic_category: Optional[str]) -> bool:
|
| 236 |
+
"""Determine if a video is relevant to the academic search"""
|
| 237 |
+
# Handle empty inputs
|
| 238 |
+
if not title:
|
| 239 |
+
return False
|
| 240 |
+
|
| 241 |
+
title_lower = title.lower()
|
| 242 |
+
description_lower = description.lower() if description else ""
|
| 243 |
+
query_lower = query.lower()
|
| 244 |
+
|
| 245 |
+
# Filter out irrelevant content
|
| 246 |
+
irrelevant_terms = [
|
| 247 |
+
"song", "music", "gaming", "funny", "meme", "challenge",
|
| 248 |
+
"vlog", "unboxing", "review", "top 10", "best of",
|
| 249 |
+
"live", "stream", "reaction", "cover", "remix"
|
| 250 |
+
]
|
| 251 |
+
|
| 252 |
+
for term in irrelevant_terms:
|
| 253 |
+
if term in title_lower or (description and term in description_lower):
|
| 254 |
+
return False
|
| 255 |
+
|
| 256 |
+
# Check for educational indicators - make this less strict
|
| 257 |
+
educational_indicators = [
|
| 258 |
+
"tutorial", "course", "lesson", "guide", "explained",
|
| 259 |
+
"how to", "learn", "beginner", "advanced", "lecture",
|
| 260 |
+
"class", "notes", "concepts", "explained", "fundamentals",
|
| 261 |
+
"introduction", "overview", "basics", "complete", "full"
|
| 262 |
+
]
|
| 263 |
+
|
| 264 |
+
has_educational_indicator = any(term in title_lower or (description and term in description_lower)
|
| 265 |
+
for term in educational_indicators)
|
| 266 |
+
|
| 267 |
+
# If we have a topic category, check for specific relevance
|
| 268 |
+
if topic_category:
|
| 269 |
+
topic_lower = topic_category.lower()
|
| 270 |
+
if "dsa" in topic_lower:
|
| 271 |
+
return has_educational_indicator or (
|
| 272 |
+
"algorithm" in title_lower or "data structure" in title_lower or
|
| 273 |
+
"dsa" in title_lower or "problem solving" in title_lower
|
| 274 |
+
)
|
| 275 |
+
elif "web" in topic_lower:
|
| 276 |
+
return has_educational_indicator or (
|
| 277 |
+
"web development" in title_lower or "frontend" in title_lower or
|
| 278 |
+
"backend" in title_lower or "full stack" in title_lower
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# If no educational indicators, check if the title contains the query
|
| 282 |
+
if not has_educational_indicator and query_lower in title_lower:
|
| 283 |
+
return True
|
| 284 |
+
|
| 285 |
+
return has_educational_indicator
|
| 286 |
+
|
| 287 |
+
def _clean_description(self, description: str) -> str:
|
| 288 |
+
"""Clean up the video description for display"""
|
| 289 |
+
if not description:
|
| 290 |
+
return ""
|
| 291 |
+
|
| 292 |
+
# Remove URLs
|
| 293 |
+
description = re.sub(r'http\S+', '', description)
|
| 294 |
+
# Remove excessive whitespace
|
| 295 |
+
description = re.sub(r'\s+', ' ', description).strip()
|
| 296 |
+
# Truncate if too long
|
| 297 |
+
if len(description) > 200:
|
| 298 |
+
description = description[:197] + "..."
|
| 299 |
+
return description
|
| 300 |
+
|
| 301 |
+
def _determine_video_category(self, title: str, description: str, query: str, topic_category: Optional[str]) -> str:
|
| 302 |
+
"""Determine the most appropriate category for the video"""
|
| 303 |
+
title_lower = title.lower()
|
| 304 |
+
description_lower = description.lower() if description else ""
|
| 305 |
+
|
| 306 |
+
# Use topic_category if provided and valid
|
| 307 |
+
if topic_category:
|
| 308 |
+
topic_lower = topic_category.lower()
|
| 309 |
+
if "dsa" in topic_lower or "algorithm" in topic_lower or "data structure" in topic_lower:
|
| 310 |
+
return "DSA"
|
| 311 |
+
elif "web" in topic_lower or "development" in topic_lower:
|
| 312 |
+
return "Web Development"
|
| 313 |
+
elif "python" in topic_lower or "programming" in topic_lower:
|
| 314 |
+
return "Programming"
|
| 315 |
+
elif "operating" in topic_lower or "os" in topic_lower:
|
| 316 |
+
return "Operating Systems"
|
| 317 |
+
elif "machine learning" in topic_lower or "ml" in topic_lower:
|
| 318 |
+
return "Machine Learning"
|
| 319 |
+
|
| 320 |
+
# Determine category from content
|
| 321 |
+
if "dsa" in title_lower or "algorithm" in title_lower or "data structure" in title_lower:
|
| 322 |
+
return "DSA"
|
| 323 |
+
elif "web development" in title_lower or "frontend" in title_lower or "backend" in title_lower:
|
| 324 |
+
return "Web Development"
|
| 325 |
+
elif "python" in title_lower or "programming" in title_lower or "coding" in title_lower:
|
| 326 |
+
return "Programming"
|
| 327 |
+
elif "operating system" in title_lower or "os" in title_lower:
|
| 328 |
+
return "Operating Systems"
|
| 329 |
+
elif "machine learning" in title_lower or "deep learning" in title_lower or "ai" in title_lower:
|
| 330 |
+
return "Machine Learning"
|
| 331 |
+
elif "database" in title_lower or "dbms" in title_lower:
|
| 332 |
+
return "Databases"
|
| 333 |
+
elif "network" in title_lower or "computer network" in title_lower:
|
| 334 |
+
return "Networking"
|
| 335 |
+
|
| 336 |
+
return "Computer Science"
|
| 337 |
+
|
| 338 |
+
def _get_fallback_videos(self, query: str, max_results: int, topic_category: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 339 |
+
"""Return high-quality fallback videos when scraping fails"""
|
| 340 |
+
logger.warning("Using fallback YouTube videos due to scraping issues")
|
| 341 |
+
|
| 342 |
+
# Check if we have specific videos for this query
|
| 343 |
+
query_lower = query.lower()
|
| 344 |
+
|
| 345 |
+
# Specific videos for common topics
|
| 346 |
+
if "binary search" in query_lower:
|
| 347 |
+
return [
|
| 348 |
+
{
|
| 349 |
+
"title": "Binary Search Algorithm Explained",
|
| 350 |
+
"url": "https://www.youtube.com/watch?v=j5uXyPJ0Pew",
|
| 351 |
+
"embed_url": "https://www.youtube.com/embed/j5uXyPJ0Pew",
|
| 352 |
+
"channel": "CS Dojo",
|
| 353 |
+
"description": "Clear explanation of binary search algorithm with examples",
|
| 354 |
+
"thumbnail": "https://i.ytimg.com/vi/j5uXyPJ0Pew/hqdefault.jpg",
|
| 355 |
+
"duration": "10 min",
|
| 356 |
+
"views": "500K+",
|
| 357 |
+
"category": "DSA"
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"title": "Binary Search Implementation in Python",
|
| 361 |
+
"url": "https://www.youtube.com/watch?v=zeEaz5J0w1c",
|
| 362 |
+
"embed_url": "https://www.youtube.com/embed/zeEaz5J0w1c",
|
| 363 |
+
"channel": "Programming with Mosh",
|
| 364 |
+
"description": "Step-by-step implementation of binary search in Python",
|
| 365 |
+
"thumbnail": "https://i.ytimg.com/vi/zeEaz5J0w1c/hqdefault.jpg",
|
| 366 |
+
"duration": "12 min",
|
| 367 |
+
"views": "300K+",
|
| 368 |
+
"category": "DSA"
|
| 369 |
+
}
|
| 370 |
+
]
|
| 371 |
+
elif "dynamic programming" in query_lower:
|
| 372 |
+
return [
|
| 373 |
+
{
|
| 374 |
+
"title": "Dynamic Programming - Learn to Solve Algorithmic Problems",
|
| 375 |
+
"url": "https://www.youtube.com/watch?v=oBt53YbR9Kk",
|
| 376 |
+
"embed_url": "https://www.youtube.com/embed/oBt53YbR9Kk",
|
| 377 |
+
"channel": "freeCodeCamp",
|
| 378 |
+
"description": "Complete guide to dynamic programming with examples",
|
| 379 |
+
"thumbnail": "https://i.ytimg.com/vi/oBt53YbR9Kk/hqdefault.jpg",
|
| 380 |
+
"duration": "45 min",
|
| 381 |
+
"views": "1M+",
|
| 382 |
+
"category": "DSA"
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
"title": "Dynamic Programming Tutorial",
|
| 386 |
+
"url": "https://www.youtube.com/watch?v=CB_N7A_a1qY",
|
| 387 |
+
"embed_url": "https://www.youtube.com/embed/CB_N7A_a1qY",
|
| 388 |
+
"channel": "Abdul Bari",
|
| 389 |
+
"description": "Comprehensive tutorial on dynamic programming concepts",
|
| 390 |
+
"thumbnail": "https://i.ytimg.com/vi/CB_N7A_a1qY/hqdefault.jpg",
|
| 391 |
+
"duration": "30 min",
|
| 392 |
+
"views": "800K+",
|
| 393 |
+
"category": "DSA"
|
| 394 |
+
}
|
| 395 |
+
]
|
| 396 |
+
elif "react" in query_lower:
|
| 397 |
+
return [
|
| 398 |
+
{
|
| 399 |
+
"title": "React JS Tutorial for Beginners",
|
| 400 |
+
"url": "https://www.youtube.com/watch?v=w7ejDZ8o_s8",
|
| 401 |
+
"embed_url": "https://www.youtube.com/embed/w7ejDZ8o_s8",
|
| 402 |
+
"channel": "Programming with Mosh",
|
| 403 |
+
"description": "Complete React tutorial for beginners",
|
| 404 |
+
"thumbnail": "https://i.ytimg.com/vi/w7ejDZ8o_s8/hqdefault.jpg",
|
| 405 |
+
"duration": "1 hour",
|
| 406 |
+
"views": "5M+",
|
| 407 |
+
"category": "Web Development"
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"title": "React Fundamentals",
|
| 411 |
+
"url": "https://www.youtube.com/watch?v=Ke90Tje7VS0",
|
| 412 |
+
"embed_url": "https://www.youtube.com/embed/Ke90Tje7VS0",
|
| 413 |
+
"channel": "freeCodeCamp",
|
| 414 |
+
"description": "Learn React fundamentals with hands-on examples",
|
| 415 |
+
"thumbnail": "https://i.ytimg.com/vi/Ke90Tje7VS0/hqdefault.jpg",
|
| 416 |
+
"duration": "2 hours",
|
| 417 |
+
"views": "2M+",
|
| 418 |
+
"category": "Web Development"
|
| 419 |
+
}
|
| 420 |
+
]
|
| 421 |
+
|
| 422 |
+
# Default educational videos by category
|
| 423 |
+
category_videos = {
|
| 424 |
+
"dsa": [
|
| 425 |
+
{
|
| 426 |
+
"title": "Data Structures and Algorithms - Full Course for Beginners",
|
| 427 |
+
"url": "https://www.youtube.com/watch?v=8hly31xKli0",
|
| 428 |
+
"embed_url": "https://www.youtube.com/embed/8hly31xKli0",
|
| 429 |
+
"channel": "freeCodeCamp",
|
| 430 |
+
"description": "Comprehensive DSA course covering all fundamental data structures and algorithms with practical examples",
|
| 431 |
+
"thumbnail": "https://i.ytimg.com/vi/8hly31xKli0/hqdefault.jpg",
|
| 432 |
+
"duration": "4+ hours",
|
| 433 |
+
"views": "2.5M+",
|
| 434 |
+
"category": "DSA"
|
| 435 |
+
}
|
| 436 |
+
],
|
| 437 |
+
"web": [
|
| 438 |
+
{
|
| 439 |
+
"title": "Web Development Tutorial for Beginners",
|
| 440 |
+
"url": "https://www.youtube.com/watch?v=ysyzdFV45ek",
|
| 441 |
+
"embed_url": "https://www.youtube.com/embed/ysyzdFV45ek",
|
| 442 |
+
"channel": "Traversy Media",
|
| 443 |
+
"description": "Complete guide to modern web development practices including HTML, CSS, and JavaScript",
|
| 444 |
+
"thumbnail": "https://i.ytimg.com/vi/ysyzdFV45ek/hqdefault.jpg",
|
| 445 |
+
"duration": "1+ hour",
|
| 446 |
+
"views": "3.5M+",
|
| 447 |
+
"category": "Web Development"
|
| 448 |
+
}
|
| 449 |
+
],
|
| 450 |
+
"programming": [
|
| 451 |
+
{
|
| 452 |
+
"title": "Python Programming Tutorial - Full Course",
|
| 453 |
+
"url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc",
|
| 454 |
+
"embed_url": "https://www.youtube.com/embed/_uQrJ0TkZlc",
|
| 455 |
+
"channel": "Programming with Mosh",
|
| 456 |
+
"description": "Learn Python programming from scratch with hands-on examples and projects",
|
| 457 |
+
"thumbnail": "https://i.ytimg.com/vi/_uQrJ0TkZlc/hqdefault.jpg",
|
| 458 |
+
"duration": "6+ hours",
|
| 459 |
+
"views": "5.2M+",
|
| 460 |
+
"category": "Programming"
|
| 461 |
+
}
|
| 462 |
+
]
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
# Determine which category to use
|
| 466 |
+
category_key = "programming" # Default category
|
| 467 |
+
|
| 468 |
+
if topic_category:
|
| 469 |
+
topic_lower = topic_category.lower()
|
| 470 |
+
if "dsa" in topic_lower or "algorithm" in topic_lower or "data structure" in topic_lower:
|
| 471 |
+
category_key = "dsa"
|
| 472 |
+
elif "web" in topic_lower or "development" in topic_lower:
|
| 473 |
+
category_key = "web"
|
| 474 |
+
elif "python" in topic_lower or "programming" in topic_lower or "coding" in topic_lower:
|
| 475 |
+
category_key = "programming"
|
| 476 |
+
|
| 477 |
+
# Return videos from the appropriate category, or default to programming
|
| 478 |
+
return category_videos.get(category_key, category_videos["programming"])[:max_results]
|