akshit7093 commited on
Commit
f4552a1
·
1 Parent(s): 8098153

Changes before Firebase Studio auto-run

Browse files
.idx/dev.nix CHANGED
@@ -22,11 +22,16 @@
22
  enable = true;
23
  previews = {
24
  web = {
25
- command = [ "./devserver.sh" ];
26
- env = { PORT = "$PORT"; };
 
 
 
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 (Final Version)
2
 
3
  import json
4
  import os
5
  import re
6
  import time
7
  from datetime import datetime
 
8
 
9
- # Import the scraper functions and classes from your existing files
 
 
 
 
 
 
 
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 you want to fetch data for.
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
- print(f"Starting data aggregation for {len(STUDENTS_TO_FETCH)} student(s)...")
201
 
202
  for student in STUDENTS_TO_FETCH:
203
  enrollment_no = student.get("enrollment_no")
204
  if not enrollment_no:
205
- print("Skipping entry due to missing enrollment number.")
206
  continue
207
 
208
- print(f"\nFetching & Cleaning data for Enrollment No: {enrollment_no}")
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
- print(" - Processing IPU data...")
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
- print(" > Success.")
230
  else:
231
- raise Exception("Failed to process IPU data.")
232
  except Exception as e:
233
  student_record["errors"]["ipu"] = str(e)
234
- print(f" > FAILED: {e}")
235
 
236
  if student.get("leetcode_user"):
237
  try:
238
- print(f" - Processing LeetCode data for '{student['leetcode_user']}'...")
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
- print(" > Success.")
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
- print(f" > FAILED: {e}")
248
 
249
  if student.get("github_user"):
250
  try:
251
- print(f" - Processing GitHub data for '{student['github_user']}'...")
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
- print(" > Success.")
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
- print(f" > FAILED: {e}")
261
 
262
  if student.get("codeforces_user"):
263
  try:
264
- print(f" - Processing Codeforces data for '{student['codeforces_user']}'...")
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
- print(" > Success.")
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
- print(f" > FAILED: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- print(f"\n✅ Final cleaning complete. Data saved to '{OUTPUT_FILE}'.")
283
  except Exception as e:
284
- print(f"\n❌ Error saving final JSON file: {e}")
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 stronger instructions)
2
 
3
  from langchain.prompts import PromptTemplate
4
- from langchain_core.pydantic_v1 import BaseModel, Field
5
- from typing import List
6
 
7
- # --- Pydantic Models for Structured Report Output (No changes here) ---
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. Analyze the provided data thoroughly.
40
- 2. Adhere strictly to the scoring rubric below to evaluate the student.
41
- 3. You MUST output a single, valid JSON object that conforms to the schema provided.
42
- 4. DO NOT output any text, explanation, or markdown before or after the JSON object. Your entire response must be only the JSON.
 
 
 
 
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
- # api_rag_system.py (Updated with Robust Parsing)
2
-
3
  from langchain_google_genai import ChatGoogleGenerativeAI
4
- from langchain_core.output_parsers import JsonOutputParser # <-- CHANGED IMPORT
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-2.5-pro",
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
- return list(set(sources)) if sources else ["academic_profile", "coding_profiles"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # The output from this chain will be a dictionary that matches the Pydantic model
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 {"error": "Failed to generate a valid report from the LLM."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- targeted_context["coding_profiles"] = student_profile.get("coding_profiles")
 
 
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
- pip
2
- autopep8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  Flask==3.0.3
 
 
 
 
 
 
 
 
 
 
4
  gunicorn==22.0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  Werkzeug==3.0.6
6
- requests
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
- <!-- CSS is now embedded directly in the HTML -->
9
- <style>
10
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
11
-
12
- :root {
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
- <header>
215
- <h1>AI Student Profile Analyzer</h1>
216
- <p>Select a student to generate a detailed performance report or ask questions.</p>
217
- </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- <div class="controls">
220
- <select id="student-selector">
221
- <option value="">-- Select a Student --</option>
222
- </select>
223
- <button id="generate-report-btn" disabled>Generate Full Report</button>
224
- </div>
225
 
226
- <div id="loading-spinner" class="spinner hidden"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- <!-- Report Display Area -->
229
- <div id="report-container" class="hidden">
230
- <h2 id="report-title"></h2>
231
- <div id="report-content">
232
- <div class="report-section summary">
233
- <h3>HR Summary</h3>
234
- <p id="summary-text"></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  </div>
236
- <div class="report-section scores">
237
- <h3>Detailed Scores</h3>
238
- <div id="scores-grid"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  </div>
240
- <div class="report-section analysis">
241
- <h3>Analysis</h3>
242
- <div id="analysis-content">
243
- <h4>Strengths</h4>
244
- <ul id="strengths-list"></ul>
245
- <h4>Weaknesses</h4>
246
- <ul id="weaknesses-list"></ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  </div>
248
  </div>
249
- <div class="report-section advice">
250
- <h3>Actionable Advice</h3>
251
- <ul id="advice-list"></ul>
 
 
 
 
 
 
 
 
252
  </div>
253
- </div>
254
- </div>
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
- <!-- JavaScript is now embedded directly in the HTML -->
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]