team99tech commited on
Commit
de21b45
·
1 Parent(s): 26714b4

added minor1 changes2

Browse files
backend/agents/__pycache__/interviewer.cpython-312.pyc CHANGED
Binary files a/backend/agents/__pycache__/interviewer.cpython-312.pyc and b/backend/agents/__pycache__/interviewer.cpython-312.pyc differ
 
backend/agents/__pycache__/roadmap_builder.cpython-312.pyc ADDED
Binary file (3.15 kB). View file
 
backend/agents/interviewer.py CHANGED
@@ -31,9 +31,9 @@ def get_difficulty(question_number: int, current_score: float) -> str:
31
  if question_number == 1:
32
  return "conceptual"
33
  if question_number == 2:
34
- return "applied" if current_score > 0.5 else "conceptual"
35
  if question_number >= 3:
36
- return "scenario" if current_score > 0.6 else "applied"
37
  return "conceptual"
38
 
39
  def generate_question(
 
31
  if question_number == 1:
32
  return "conceptual"
33
  if question_number == 2:
34
+ return "applied"
35
  if question_number >= 3:
36
+ return "debugging"
37
  return "conceptual"
38
 
39
  def generate_question(
backend/agents/roadmap_builder.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from langchain_groq import ChatGroq
4
+ from langchain_core.messages import SystemMessage, HumanMessage
5
+ from models.schemas import SkillScore
6
+
7
+ GROQ_MODEL = "llama-3.3-70b-versatile"
8
+
9
+ ROADMAP_SYSTEM = """You are a senior technical mentor.
10
+ Given a list of skills where a candidate has a gap, and their target domain, generate a personalized 3-tier roadmap for EACH skill.
11
+
12
+ Tier 1: Core concepts and fundamentals
13
+ Tier 2: Intermediate integration and application
14
+ Tier 3: Advanced / Role-specific scenario
15
+
16
+ For each tier, provide a highly specific, customized 'mini_project' description (2-3 sentences) that is directly relevant to their domain. DO NOT use generic phrases.
17
+ Also provide a 1-sentence 'why' explaining why this step is critical for overcoming their specific gap.
18
+
19
+ Return valid JSON in this exact format:
20
+ {
21
+ "skill_id_1": [
22
+ {
23
+ "tier": 1,
24
+ "mini_project": "Build a simple API using FastAPI and Pydantic to validate user inputs.",
25
+ "why": "This establishes the fundamental routing and validation concepts."
26
+ },
27
+ {
28
+ "tier": 2,
29
+ "mini_project": "...",
30
+ "why": "..."
31
+ },
32
+ {
33
+ "tier": 3,
34
+ "mini_project": "...",
35
+ "why": "..."
36
+ }
37
+ ]
38
+ }
39
+ """
40
+
41
+ def generate_custom_roadmap_data(skill_scores: list[SkillScore], domain: str) -> dict:
42
+ gap_skills = [s for s in skill_scores if s.gap_level in ("high_gap", "medium_gap")]
43
+ if not gap_skills:
44
+ return {}
45
+
46
+ prompt = f"Domain: {domain}\nGap Skills to analyze:\n"
47
+ for s in gap_skills:
48
+ prompt += f"- ID: {s.skill_id} | Label: {s.label} | Gap: {s.gap_level} | Final Score: {s.final_score:.2f}\n"
49
+
50
+ llm = ChatGroq(
51
+ model=GROQ_MODEL,
52
+ api_key=os.getenv("GROQ_API_KEY", "dummy"),
53
+ temperature=0.4,
54
+ )
55
+
56
+ messages = [
57
+ SystemMessage(content=ROADMAP_SYSTEM),
58
+ HumanMessage(content=prompt)
59
+ ]
60
+
61
+ try:
62
+ response = llm.invoke(messages)
63
+ content = response.content.strip()
64
+ if content.startswith("```json"):
65
+ content = content[7:]
66
+ if content.endswith("```"):
67
+ content = content[:-3]
68
+ return json.loads(content)
69
+ except Exception as e:
70
+ print("Roadmap builder error:", e)
71
+ return {}
backend/output/__pycache__/roadmap_generator.cpython-312.pyc CHANGED
Binary files a/backend/output/__pycache__/roadmap_generator.cpython-312.pyc and b/backend/output/__pycache__/roadmap_generator.cpython-312.pyc differ
 
backend/output/roadmap_generator.py CHANGED
@@ -2,11 +2,13 @@ import json
2
  from pathlib import Path
3
  from models.schemas import RoadmapWeek, SkillScore
4
  from knowledge_graph.graph_engine import get_engine
 
5
 
6
  def generate_roadmap(
7
  skill_scores: list[SkillScore],
8
  graph_paths: dict[str, list[str]],
9
  hours_per_day: float = 2.0,
 
10
  ) -> list[RoadmapWeek]:
11
  engine = get_engine()
12
  resources_path = Path("backend/knowledge_graph/resources.json")
@@ -20,6 +22,9 @@ def generate_roadmap(
20
  base_hours_per_week = hours_per_day * 5
21
  week_counter = 1
22
 
 
 
 
23
  # Sort skills: high_gap first, then medium_gap
24
  gap_skills = [s for s in skill_scores if s.gap_level in ("high_gap", "medium_gap")]
25
  gap_skills.sort(key=lambda s: 0 if s.gap_level == "high_gap" else 1)
@@ -27,63 +32,35 @@ def generate_roadmap(
27
  for skill in gap_skills:
28
  skill_id = skill.skill_id
29
  path = graph_paths.get(skill_id, [])
30
- path_str = " → ".join(path)
31
 
32
  resources = []
33
  if skill_id in resources_db:
34
  resources = resources_db[skill_id].get("courses", [])
35
 
36
- why_msg = "Direct learning path"
37
- if len(path) > 1:
38
- source = path[0]
39
- target = path[-1]
40
- hops = len(path) - 1
41
- has_edges = True if engine.graph.has_node(source) and engine.graph.has_node(target) else False
42
- if has_edges:
43
- edge_type = "path"
44
- try:
45
- if hops == 1:
46
- edge_type = engine.graph[source][target]["type"]
47
- except:
48
- pass
49
- why_msg = f"You already know {source} → {target} is {edge_type} ({hops} hop)"
50
- else:
51
- why_msg = f"Building up from {source} to {target}"
52
-
53
- roadmap.append(RoadmapWeek(
54
- week=week_counter,
55
- skill_id=skill_id,
56
- label=skill.label,
57
- tier=1,
58
- resources=resources,
59
- mini_project=f"Build a basic script using {skill.label} core features",
60
- graph_path=path,
61
- why=why_msg
62
- ))
63
- week_counter += 1
64
-
65
- roadmap.append(RoadmapWeek(
66
- week=week_counter,
67
- skill_id=skill_id,
68
- label=skill.label,
69
- tier=2,
70
- resources=[],
71
- mini_project=f"Integrate {skill.label} into a broader project",
72
- graph_path=path,
73
- why=why_msg
74
- ))
75
- week_counter += 1
76
 
77
- roadmap.append(RoadmapWeek(
78
- week=week_counter,
79
- skill_id=skill_id,
80
- label=skill.label,
81
- tier=3,
82
- resources=[],
83
- mini_project=f"Solve a role-specific scenario with {skill.label}",
84
- graph_path=path,
85
- why=why_msg
86
- ))
87
- week_counter += 1
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  return roadmap
 
2
  from pathlib import Path
3
  from models.schemas import RoadmapWeek, SkillScore
4
  from knowledge_graph.graph_engine import get_engine
5
+ from agents.roadmap_builder import generate_custom_roadmap_data
6
 
7
  def generate_roadmap(
8
  skill_scores: list[SkillScore],
9
  graph_paths: dict[str, list[str]],
10
  hours_per_day: float = 2.0,
11
+ domain: str = "general",
12
  ) -> list[RoadmapWeek]:
13
  engine = get_engine()
14
  resources_path = Path("backend/knowledge_graph/resources.json")
 
22
  base_hours_per_week = hours_per_day * 5
23
  week_counter = 1
24
 
25
+ # 1. Fetch custom LLM analysis
26
+ custom_data = generate_custom_roadmap_data(skill_scores, domain)
27
+
28
  # Sort skills: high_gap first, then medium_gap
29
  gap_skills = [s for s in skill_scores if s.gap_level in ("high_gap", "medium_gap")]
30
  gap_skills.sort(key=lambda s: 0 if s.gap_level == "high_gap" else 1)
 
32
  for skill in gap_skills:
33
  skill_id = skill.skill_id
34
  path = graph_paths.get(skill_id, [])
 
35
 
36
  resources = []
37
  if skill_id in resources_db:
38
  resources = resources_db[skill_id].get("courses", [])
39
 
40
+ custom_tiers = custom_data.get(skill_id, [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ for tier in [1, 2, 3]:
43
+ # Default values
44
+ mini_project = f"Practice and implement {skill.label} concepts."
45
+ why_msg = f"Essential for mastering {skill.label}."
46
+
47
+ # Use LLM generated data if available
48
+ for ct in custom_tiers:
49
+ if ct.get("tier") == tier:
50
+ mini_project = ct.get("mini_project", mini_project)
51
+ why_msg = ct.get("why", why_msg)
52
+ break
53
+
54
+ roadmap.append(RoadmapWeek(
55
+ week=week_counter,
56
+ skill_id=skill_id,
57
+ label=skill.label,
58
+ tier=tier,
59
+ resources=resources if tier == 1 else [], # Only show resources in Tier 1
60
+ mini_project=mini_project,
61
+ graph_path=path,
62
+ why=why_msg
63
+ ))
64
+ week_counter += 1
65
 
66
  return roadmap
backend/routers/__pycache__/roadmap.cpython-312.pyc CHANGED
Binary files a/backend/routers/__pycache__/roadmap.cpython-312.pyc and b/backend/routers/__pycache__/roadmap.cpython-312.pyc differ
 
backend/routers/roadmap.py CHANGED
@@ -21,7 +21,8 @@ async def get_roadmap(assessment_id: str, hours_per_day: float = 2.0):
21
  roadmap = generate_roadmap(
22
  st.get("skill_scores", []),
23
  st.get("graph_paths", {}),
24
- hours_per_day
 
25
  )
26
 
27
  return AssessmentResult(
 
21
  roadmap = generate_roadmap(
22
  st.get("skill_scores", []),
23
  st.get("graph_paths", {}),
24
+ hours_per_day,
25
+ st["extraction"].domain
26
  )
27
 
28
  return AssessmentResult(
frontend/app/candidate/job/[id]/page.tsx DELETED
@@ -1,140 +0,0 @@
1
- "use client"
2
- import { useEffect, useState } from "react"
3
- import { useParams, useRouter } from "next/navigation"
4
- import { createClient } from "@/utils/supabase/client"
5
- import { useStore } from "@/lib/store"
6
- import { startAssessment } from "@/lib/api"
7
- import UploadZone from "@/components/upload/UploadZone"
8
-
9
- export default function CandidateJobPage() {
10
- const params = useParams()
11
- const id = typeof params.id === "string" ? params.id : null
12
- const router = useRouter()
13
- const supabase = createClient()
14
-
15
- const { hoursPerDay, setHoursPerDay, setJdText, resumeText, setResumeText, setAssessmentId, setExtraction } = useStore()
16
-
17
- const [job, setJob] = useState<any>(null)
18
- const [loading, setLoading] = useState(true)
19
- const [starting, setStarting] = useState(false)
20
- const [error, setError] = useState("")
21
-
22
- useEffect(() => {
23
- if (!id) return
24
- fetchJob()
25
- }, [id])
26
-
27
- const fetchJob = async () => {
28
- const { data: { user } } = await supabase.auth.getUser()
29
- if (!user) {
30
- router.push('/login')
31
- return
32
- }
33
-
34
- const { data } = await supabase.from('jobs').select('*, profiles(email, full_name)').eq('id', id).single()
35
- if (data) {
36
- setJob(data)
37
- setJdText(data.jd_text)
38
- }
39
- setLoading(false)
40
- }
41
-
42
- const handleBegin = async () => {
43
- if (!job || !resumeText) return
44
- setStarting(true)
45
- setError("")
46
-
47
- try {
48
- const { data: { user } } = await supabase.auth.getUser()
49
- if (!user) throw new Error("Not authenticated")
50
-
51
- // 1. Create Assessment Row in Supabase
52
- const { data: assessment, error: dbError } = await supabase.from('assessments').insert({
53
- job_id: job.id,
54
- candidate_id: user.id,
55
- resume_text: resumeText,
56
- status: 'in_progress'
57
- }).select().single()
58
-
59
- if (dbError) throw dbError
60
-
61
- // 2. Start Python Backend Assessment with that same Supabase ID
62
- const res = await startAssessment(job.jd_text, resumeText, hoursPerDay, assessment.id)
63
-
64
- // 3. Save to Zustand store and navigate
65
- setAssessmentId(assessment.id)
66
- setExtraction(res.extraction)
67
- router.push(`/assess/${assessment.id}`)
68
-
69
- } catch (err: any) {
70
- setError(err.message)
71
- setStarting(false)
72
- }
73
- }
74
-
75
- if (loading || !job) return <div className="min-h-screen bg-bg flex items-center justify-center font-mono text-accent animate-pulse">Loading Job...</div>
76
-
77
- return (
78
- <div className="min-h-screen bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
79
- <div className="max-w-4xl mx-auto space-y-8">
80
- <button onClick={() => router.push('/candidate')} className="text-sm font-mono text-muted hover:text-accent transition-colors">
81
- ← Back to Portal
82
- </button>
83
-
84
- <header className="border-b border-border pb-6">
85
- <h1 className="text-3xl font-bold font-mono tracking-tighter mb-2 text-accent">{job.title}</h1>
86
- <p className="text-muted font-mono text-sm">Posted by: {job.profiles?.full_name || job.profiles?.email}</p>
87
- </header>
88
-
89
- {/* Job Description View */}
90
- <section className="bg-surface border border-border shadow-sm p-8 rounded-3xl space-y-4 shadow-xl hover:shadow-[0_0_30px_rgba(0,240,255,0.05)] transition-shadow duration-500">
91
- <h2 className="text-xl font-mono text-accent flex items-center">
92
- <span className="w-8 h-px bg-accent/50 mr-4"></span>
93
- Job Description
94
- </h2>
95
- <div className="bg-background/80 border border-border rounded-xl p-6 max-h-[300px] overflow-y-auto font-mono text-sm text-muted whitespace-pre-wrap leading-relaxed">
96
- {job.jd_text}
97
- </div>
98
- </section>
99
-
100
- <div className="bg-surface border border-border shadow-sm p-8 rounded-3xl space-y-8 shadow-xl">
101
- <div>
102
- <h2 className="text-xl font-mono text-text mb-4 flex items-center">
103
- <span className="w-8 h-px bg-accent/50 mr-4"></span>
104
- Apply & Test
105
- </h2>
106
- <p className="text-muted text-sm mb-6">
107
- Upload your resume below to begin. Our AI will instantly evaluate your resume against this job description and conduct a dynamic technical interview.
108
- </p>
109
-
110
- <div className="max-w-lg">
111
- <UploadZone label="Your Candidate Resume (PDF)" endpoint="resume" onUpload={setResumeText} />
112
- </div>
113
- </div>
114
-
115
- <div className="max-w-lg bg-background/50 border border-border p-5 rounded-xl">
116
- <label className="flex justify-between text-sm font-mono text-muted mb-4">
117
- <span>Availability (hours/day)</span>
118
- <span className="text-accent font-bold">{hoursPerDay}h</span>
119
- </label>
120
- <input
121
- type="range" min="1" max="8" step="0.5"
122
- value={hoursPerDay} onChange={(e) => setHoursPerDay(parseFloat(e.target.value))}
123
- className="w-full h-1.5 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-accent transition-all hover:h-2"
124
- />
125
- </div>
126
-
127
- {error && <div className="text-red-500 font-mono text-sm">{error}</div>}
128
-
129
- <button
130
- onClick={handleBegin}
131
- disabled={!resumeText || starting}
132
- className="bg-accent text-white px-8 py-4 rounded-xl font-mono font-bold text-lg hover:scale-[1.02] shadow-[0_4px_14px_0_rgb(37,87,167,0.39)] disabled:opacity-50 disabled:hover:scale-100 transition-all"
133
- >
134
- {starting ? "Initializing Engine..." : "Begin AI Interview →"}
135
- </button>
136
- </div>
137
- </div>
138
- </div>
139
- )
140
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/candidate/page.tsx DELETED
@@ -1,145 +0,0 @@
1
- "use client"
2
- import { useEffect, useState } from "react"
3
- import { useRouter } from "next/navigation"
4
- import { createClient } from "@/utils/supabase/client"
5
-
6
- export default function CandidateDashboard() {
7
- const router = useRouter()
8
- const supabase = createClient()
9
- const [jobs, setJobs] = useState<any[]>([])
10
- const [loading, setLoading] = useState(true)
11
- const [activeTab, setActiveTab] = useState<'opportunities' | 'profile'>('opportunities')
12
- const [profileData, setProfileData] = useState<any>(null)
13
- const [fullName, setFullName] = useState("")
14
- const [saving, setSaving] = useState(false)
15
-
16
- useEffect(() => {
17
- fetchJobs()
18
- }, [])
19
-
20
- const fetchJobs = async () => {
21
- const { data: { user } } = await supabase.auth.getUser()
22
- if (!user) {
23
- router.push('/login')
24
- return
25
- }
26
-
27
- const { data: profile, error } = await supabase.from('profiles').select('*').eq('id', user.id).single()
28
-
29
- if (error || !profile) {
30
- await supabase.auth.signOut()
31
- router.push('/login')
32
- return
33
- }
34
-
35
- if (profile.role !== 'candidate') {
36
- router.push('/employer')
37
- return
38
- }
39
-
40
- setProfileData(profile)
41
- setFullName(profile.full_name || "")
42
-
43
- // Fetch all jobs
44
- const { data } = await supabase.from('jobs').select('*, profiles(email, full_name)').order('created_at', { ascending: false })
45
- if (data) setJobs(data)
46
- setLoading(false)
47
- }
48
-
49
- const saveProfile = async () => {
50
- if (!profileData) return
51
- setSaving(true)
52
- const { error } = await supabase.from('profiles').update({ full_name: fullName }).eq('id', profileData.id)
53
- if (error) {
54
- alert("Error saving profile. Make sure to run the SQL command to add the full_name column!")
55
- } else {
56
- setProfileData({ ...profileData, full_name: fullName })
57
- alert("Profile updated successfully!")
58
- }
59
- setSaving(false)
60
- }
61
-
62
- const handleLogout = async () => {
63
- await supabase.auth.signOut()
64
- router.push('/')
65
- }
66
-
67
- if (loading) return <div className="min-h-screen bg-bg flex items-center justify-center font-mono text-accent animate-pulse">Loading Opportunities...</div>
68
-
69
- return (
70
- <div className="min-h-screen bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
71
- <div className="max-w-4xl mx-auto space-y-12">
72
- <header className="flex flex-col gap-6 border-b border-border pb-6">
73
- <div className="flex justify-between items-end">
74
- <div>
75
- <h1 className="text-4xl font-bold font-mono tracking-tighter mb-2">Candidate <span className="text-accent">Portal</span></h1>
76
- <p className="text-muted font-mono">Discover roles and prove your skills</p>
77
- </div>
78
- <button onClick={handleLogout} className="text-sm font-mono text-muted hover:text-red-400 transition-colors">Log Out</button>
79
- </div>
80
- <div className="flex gap-4">
81
- <button onClick={() => setActiveTab('opportunities')} className={`font-mono pb-2 border-b-2 transition-colors ${activeTab === 'opportunities' ? 'border-accent text-accent' : 'border-transparent text-muted hover:text-text'}`}>Opportunities</button>
82
- <button onClick={() => setActiveTab('profile')} className={`font-mono pb-2 border-b-2 transition-colors ${activeTab === 'profile' ? 'border-accent text-accent' : 'border-transparent text-muted hover:text-text'}`}>My Profile</button>
83
- </div>
84
- </header>
85
-
86
- {activeTab === 'opportunities' ? (
87
- <section className="space-y-6">
88
- <h2 className="text-2xl font-mono text-text">Open Positions</h2>
89
-
90
- {jobs.length === 0 ? (
91
- <div className="border border-dashed border-border rounded-2xl p-12 text-center text-muted font-mono">
92
- No open positions at the moment. Check back soon!
93
- </div>
94
- ) : (
95
- <div className="grid grid-cols-1 gap-4">
96
- {jobs.map(job => (
97
- <div key={job.id} className="bg-surface border border-border shadow-sm p-6 rounded-2xl flex flex-col md:flex-row justify-between items-start md:items-center gap-4 hover:border-accent/50 transition-colors">
98
- <div>
99
- <h3 className="text-xl font-mono text-accent mb-2">{job.title}</h3>
100
- <p className="text-xs font-mono text-muted">Posted by: {job.profiles?.full_name || job.profiles?.email}</p>
101
- <p className="text-xs font-mono text-muted mt-1">{new Date(job.created_at).toLocaleDateString()}</p>
102
- </div>
103
- <button
104
- onClick={() => router.push(`/candidate/job/${job.id}`)}
105
- className="bg-accent/10 text-accent border border-accent/30 px-6 py-2 rounded-lg font-mono font-bold hover:bg-accent hover:text-background transition-all"
106
- >
107
- Apply & Assess →
108
- </button>
109
- </div>
110
- ))}
111
- </div>
112
- )}
113
- </section>
114
- ) : (
115
- <section className="bg-surface border border-border p-8 rounded-3xl max-w-lg">
116
- <h2 className="text-2xl font-mono text-text mb-6">Profile Settings</h2>
117
- <div className="space-y-6">
118
- <div>
119
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">Account Email</label>
120
- <input type="text" disabled value={profileData?.email || ""} className="w-full bg-background border border-border rounded-xl p-3 text-sm opacity-50 cursor-not-allowed" />
121
- </div>
122
- <div>
123
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">Full Name</label>
124
- <input
125
- type="text"
126
- value={fullName}
127
- onChange={(e) => setFullName(e.target.value)}
128
- className="w-full bg-background border border-border rounded-xl p-3 text-sm focus:outline-none focus:border-accent"
129
- placeholder="John Doe"
130
- />
131
- </div>
132
- <button
133
- onClick={saveProfile}
134
- disabled={saving}
135
- className="w-full bg-accent text-white px-6 py-3 rounded-xl font-mono font-bold hover:scale-[1.02] shadow-md transition-all disabled:opacity-50"
136
- >
137
- {saving ? "Saving..." : "Save Profile"}
138
- </button>
139
- </div>
140
- </section>
141
- )}
142
- </div>
143
- </div>
144
- )
145
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/employer/job/[id]/page.tsx DELETED
@@ -1,121 +0,0 @@
1
- "use client"
2
- import { useEffect, useState } from "react"
3
- import { useParams, useRouter } from "next/navigation"
4
- import { createClient } from "@/utils/supabase/client"
5
-
6
- export default function EmployerJobPage() {
7
- const params = useParams()
8
- const id = typeof params.id === "string" ? params.id : null
9
- const router = useRouter()
10
- const supabase = createClient()
11
-
12
- const [job, setJob] = useState<any>(null)
13
- const [assessments, setAssessments] = useState<any[]>([])
14
- const [loading, setLoading] = useState(true)
15
-
16
- useEffect(() => {
17
- if (!id) return
18
- fetchJobAndCandidates()
19
- }, [id])
20
-
21
- const fetchJobAndCandidates = async () => {
22
- const { data: { user } } = await supabase.auth.getUser()
23
- if (!user) {
24
- router.push('/login')
25
- return
26
- }
27
-
28
- // Fetch job details
29
- const { data: jobData } = await supabase.from('jobs').select('*').eq('id', id).single()
30
- if (jobData) setJob(jobData)
31
-
32
- // Fetch assessments for this job
33
- const { data: assessmentData } = await supabase.from('assessments')
34
- .select('id, status, created_at, result_data, profiles(email, full_name)')
35
- .eq('job_id', id)
36
-
37
- if (assessmentData) {
38
- const mapped = assessmentData.map((a: any) => {
39
- let score = 0;
40
- if (a.result_data && a.result_data.skill_scores && a.result_data.skill_scores.length > 0) {
41
- score = Math.round((a.result_data.skill_scores.reduce((sum: number, s: any) => sum + s.final_score, 0) / a.result_data.skill_scores.length) * 100)
42
- }
43
- return { ...a, score }
44
- })
45
- // Sort by score descending
46
- mapped.sort((a, b) => b.score - a.score)
47
- setAssessments(mapped)
48
- }
49
-
50
- setLoading(false)
51
- }
52
-
53
- if (loading || !job) return <div className="min-h-screen bg-bg flex items-center justify-center font-mono text-accent animate-pulse">Loading Job Details...</div>
54
-
55
- return (
56
- <div className="min-h-screen bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
57
- <div className="max-w-5xl mx-auto space-y-8">
58
- <button onClick={() => router.push('/employer')} className="text-sm font-mono text-muted hover:text-accent transition-colors">
59
- ← Back to Dashboard
60
- </button>
61
-
62
- <header className="border-b border-border pb-6 flex justify-between items-end">
63
- <div>
64
- <h1 className="text-3xl font-bold font-mono tracking-tighter mb-2 text-accent">{job.title}</h1>
65
- <p className="text-muted font-mono text-sm">Posted on {new Date(job.created_at).toLocaleDateString()}</p>
66
- </div>
67
- <div className="bg-surface/50 border border-border px-4 py-2 rounded-lg">
68
- <span className="font-mono text-xl text-text font-bold">{assessments.length}</span>
69
- <span className="text-muted text-xs uppercase ml-2">Candidates</span>
70
- </div>
71
- </header>
72
-
73
- <section className="space-y-6">
74
- <h2 className="text-2xl font-mono text-text">Candidate Assessments</h2>
75
-
76
- {assessments.length === 0 ? (
77
- <div className="border border-dashed border-border rounded-2xl p-12 text-center text-muted font-mono bg-surface/50">
78
- No candidates have applied to this job yet.
79
- </div>
80
- ) : (
81
- <div className="grid grid-cols-1 gap-4">
82
- {assessments.map((assessment) => (
83
- <div key={assessment.id} className="bg-surface border border-border shadow-sm p-6 rounded-2xl flex flex-col md:flex-row justify-between items-start md:items-center gap-4 hover:border-accent/50 transition-colors">
84
- <div>
85
- <h3 className="text-lg font-mono text-text mb-1">
86
- {assessment.profiles?.full_name ? assessment.profiles.full_name : assessment.profiles?.email}
87
- {assessment.profiles?.full_name && <span className="text-sm text-muted ml-2">({assessment.profiles?.email})</span>}
88
- </h3>
89
- <div className="flex items-center gap-3">
90
- <span className={`text-xs font-mono px-2 py-1 rounded-md border ${
91
- assessment.status === 'completed'
92
- ? 'border-green-500/50 text-green-400 bg-green-500/10'
93
- : 'border-yellow-500/50 text-yellow-400 bg-yellow-500/10'
94
- }`}>
95
- {assessment.status === 'completed' ? 'COMPLETED' : 'IN PROGRESS'}
96
- </span>
97
- {assessment.status === 'completed' && (
98
- <span className="text-xs font-mono px-2 py-1 rounded-md border border-accent/50 text-accent bg-accent/10">
99
- SCORE: {assessment.score}/100
100
- </span>
101
- )}
102
- <span className="text-xs text-muted font-mono">{new Date(assessment.created_at).toLocaleString()}</span>
103
- </div>
104
- </div>
105
-
106
- <button
107
- disabled={assessment.status !== 'completed'}
108
- onClick={() => router.push(`/results/${assessment.id}`)}
109
- className="bg-background border border-border text-text px-6 py-2 rounded-lg font-mono font-bold hover:border-accent hover:text-accent transition-all disabled:opacity-50 disabled:hover:border-border disabled:hover:text-text"
110
- >
111
- {assessment.status === 'completed' ? "View Results →" : "Awaiting Completion"}
112
- </button>
113
- </div>
114
- ))}
115
- </div>
116
- )}
117
- </section>
118
- </div>
119
- </div>
120
- )
121
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/employer/page.tsx DELETED
@@ -1,222 +0,0 @@
1
- "use client"
2
- import { useEffect, useState } from "react"
3
- import { useRouter } from "next/navigation"
4
- import { createClient } from "@/utils/supabase/client"
5
- import UploadZone from "@/components/upload/UploadZone"
6
-
7
- export default function EmployerDashboard() {
8
- const router = useRouter()
9
- const supabase = createClient()
10
- const [jobs, setJobs] = useState<any[]>([])
11
- const [loading, setLoading] = useState(true)
12
- const [jdText, setJdText] = useState("")
13
- const [jobTitle, setJobTitle] = useState("")
14
- const [creating, setCreating] = useState(false)
15
- const [activeTab, setActiveTab] = useState<'dashboard' | 'profile'>('dashboard')
16
- const [profileData, setProfileData] = useState<any>(null)
17
- const [companyName, setCompanyName] = useState("")
18
- const [saving, setSaving] = useState(false)
19
-
20
- useEffect(() => {
21
- fetchJobs()
22
- }, [])
23
-
24
- const fetchJobs = async () => {
25
- const { data: { user } } = await supabase.auth.getUser()
26
- if (!user) {
27
- router.push('/login')
28
- return
29
- }
30
-
31
- const { data: profile, error } = await supabase.from('profiles').select('*').eq('id', user.id).single()
32
-
33
- if (error || !profile) {
34
- await supabase.auth.signOut()
35
- router.push('/login')
36
- return
37
- }
38
-
39
- if (profile.role !== 'employer') {
40
- router.push('/candidate')
41
- return
42
- }
43
-
44
- setProfileData(profile)
45
- setCompanyName(profile.full_name || "")
46
-
47
- const { data } = await supabase.from('jobs').select('*').order('created_at', { ascending: false })
48
- if (data) setJobs(data)
49
- setLoading(false)
50
- }
51
-
52
- const saveProfile = async () => {
53
- if (!profileData) return
54
- setSaving(true)
55
- const { error } = await supabase.from('profiles').update({ full_name: companyName }).eq('id', profileData.id)
56
- if (error) {
57
- alert("Error saving profile. Make sure to run the SQL command to add the full_name column!")
58
- } else {
59
- setProfileData({ ...profileData, full_name: companyName })
60
- alert("Company Profile updated successfully!")
61
- }
62
- setSaving(false)
63
- }
64
-
65
- const handleCreateJob = async () => {
66
- if (!jobTitle || !jdText) return
67
- setCreating(true)
68
-
69
- const { data: { user } } = await supabase.auth.getUser()
70
- if (!user) return
71
-
72
- const { error } = await supabase.from('jobs').insert({
73
- employer_id: user.id,
74
- title: jobTitle,
75
- jd_text: jdText
76
- })
77
-
78
- setCreating(false)
79
- if (!error) {
80
- setJobTitle("")
81
- setJdText("")
82
- fetchJobs()
83
- }
84
- }
85
-
86
- const handleDeleteJob = async (e: React.MouseEvent, jobId: string) => {
87
- e.stopPropagation()
88
- if (!confirm("Are you sure you want to delete this job posting? All candidate assessments for this job will also be deleted permanently.")) return
89
-
90
- const { error } = await supabase.from('jobs').delete().eq('id', jobId)
91
- if (error) {
92
- alert("Failed to delete job")
93
- } else {
94
- fetchJobs()
95
- }
96
- }
97
-
98
- const handleLogout = async () => {
99
- await supabase.auth.signOut()
100
- router.push('/')
101
- }
102
-
103
- if (loading) return <div className="min-h-screen bg-bg flex items-center justify-center font-mono text-accent animate-pulse">Loading Dashboard...</div>
104
-
105
- return (
106
- <div className="min-h-screen bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
107
- <div className="max-w-6xl mx-auto space-y-12">
108
- <header className="flex flex-col gap-6 border-b border-border pb-6">
109
- <div className="flex justify-between items-end">
110
- <div>
111
- <h1 className="text-4xl font-bold font-mono tracking-tighter mb-2">Employer <span className="text-accent">Portal</span></h1>
112
- <p className="text-muted font-mono">Manage jobs and review candidate assessments</p>
113
- </div>
114
- <button onClick={handleLogout} className="text-sm font-mono text-muted hover:text-red-400 transition-colors">Log Out</button>
115
- </div>
116
- <div className="flex gap-4">
117
- <button onClick={() => setActiveTab('dashboard')} className={`font-mono pb-2 border-b-2 transition-colors ${activeTab === 'dashboard' ? 'border-accent text-accent' : 'border-transparent text-muted hover:text-text'}`}>Dashboard</button>
118
- <button onClick={() => setActiveTab('profile')} className={`font-mono pb-2 border-b-2 transition-colors ${activeTab === 'profile' ? 'border-accent text-accent' : 'border-transparent text-muted hover:text-text'}`}>Company Profile</button>
119
- </div>
120
- </header>
121
-
122
- {activeTab === 'dashboard' ? (
123
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
124
- <div className="lg:col-span-1 space-y-6">
125
- <div className="bg-surface border border-border shadow-sm p-6 rounded-2xl">
126
- <h2 className="text-xl font-mono text-accent mb-6">Post New Job</h2>
127
-
128
- <div className="space-y-4">
129
- <div>
130
- <label className="block text-xs font-mono text-muted mb-2">Job Title</label>
131
- <input
132
- type="text"
133
- value={jobTitle}
134
- onChange={(e) => setJobTitle(e.target.value)}
135
- className="w-full bg-background border border-border rounded-xl p-3 text-sm focus:border-accent outline-none"
136
- placeholder="e.g. Senior Frontend Engineer"
137
- />
138
- </div>
139
-
140
- <div className="pt-2">
141
- <UploadZone label="Job Description (PDF)" endpoint="jd" onUpload={setJdText} />
142
- </div>
143
-
144
- <button
145
- onClick={handleCreateJob}
146
- disabled={creating || !jobTitle || !jdText}
147
- className="w-full bg-accent text-background font-bold font-mono py-3 rounded-xl hover:brightness-110 disabled:opacity-50 mt-4"
148
- >
149
- {creating ? "Posting..." : "Create Job"}
150
- </button>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <div className="lg:col-span-2 space-y-6">
156
- <h2 className="text-2xl font-mono text-text">Your Active Jobs</h2>
157
-
158
- {jobs.length === 0 ? (
159
- <div className="border border-dashed border-border rounded-2xl p-12 text-center text-muted font-mono">
160
- No jobs posted yet. Post your first job to start receiving candidates!
161
- </div>
162
- ) : (
163
- <div className="space-y-4">
164
- {jobs.map(job => (
165
- <div key={job.id} className="bg-surface border border-border p-6 rounded-2xl hover:border-accent/50 transition-colors flex justify-between items-center group cursor-pointer" onClick={() => router.push(`/employer/job/${job.id}`)}>
166
- <div>
167
- <h3 className="text-lg font-mono text-accent mb-1">{job.title}</h3>
168
- <p className="text-xs text-muted font-mono">Posted {new Date(job.created_at).toLocaleDateString()}</p>
169
- </div>
170
- <div className="flex items-center gap-4">
171
- <div className="text-muted group-hover:text-accent group-hover:-translate-x-1 transition-all">
172
- View Candidates →
173
- </div>
174
- <button
175
- onClick={(e) => handleDeleteJob(e, job.id)}
176
- className="text-muted hover:text-red-500 p-2 rounded-lg hover:bg-red-50 transition-all"
177
- title="Delete Job"
178
- >
179
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
180
- </button>
181
- </div>
182
- </div>
183
- ))}
184
- </div>
185
- )}
186
- </div>
187
- </div>
188
- ) : (
189
- <section className="bg-surface border border-border shadow-sm p-8 rounded-3xl max-w-lg shadow-xl">
190
- <h2 className="text-2xl font-mono text-accent mb-6 flex items-center">
191
- <span className="w-8 h-px bg-accent/50 mr-4"></span>
192
- Company Details
193
- </h2>
194
- <div className="space-y-6">
195
- <div>
196
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">Account Email</label>
197
- <input type="text" disabled value={profileData?.email || ""} className="w-full bg-background border border-border rounded-xl p-3 text-sm opacity-50 cursor-not-allowed" />
198
- </div>
199
- <div>
200
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">Company Name</label>
201
- <input
202
- type="text"
203
- value={companyName}
204
- onChange={(e) => setCompanyName(e.target.value)}
205
- className="w-full bg-background border border-border rounded-xl p-3 text-sm focus:outline-none focus:border-accent text-text"
206
- placeholder="Acme Corp"
207
- />
208
- </div>
209
- <button
210
- onClick={saveProfile}
211
- disabled={saving}
212
- className="w-full bg-accent text-white px-6 py-3 rounded-xl font-mono font-bold hover:scale-[1.02] shadow-sm hover:shadow-md transition-all disabled:opacity-50"
213
- >
214
- {saving ? "Saving..." : "Save Company Profile"}
215
- </button>
216
- </div>
217
- </section>
218
- )}
219
- </div>
220
- </div>
221
- )
222
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/login/page.tsx DELETED
@@ -1,148 +0,0 @@
1
- "use client"
2
- import { useState } from "react"
3
- import { useRouter } from "next/navigation"
4
- import { createClient } from "@/utils/supabase/client"
5
-
6
- export default function LoginPage() {
7
- const router = useRouter()
8
- const supabase = createClient()
9
-
10
- const [isLogin, setIsLogin] = useState(true)
11
- const [email, setEmail] = useState("")
12
- const [password, setPassword] = useState("")
13
- const [role, setRole] = useState("candidate")
14
- const [loading, setLoading] = useState(false)
15
- const [error, setError] = useState("")
16
-
17
- const handleAuth = async (e: React.FormEvent) => {
18
- e.preventDefault()
19
- setLoading(true)
20
- setError("")
21
-
22
- // Strict check for allowed email domains
23
- const allowedDomains = ["@gmail.com", "@yahoo.com", "@outlook.com", "@hotmail.com", "@icloud.com"]
24
- const hasValidDomain = allowedDomains.some(domain => email.toLowerCase().endsWith(domain))
25
-
26
- if (!hasValidDomain) {
27
- setError("Please use a standard email provider (e.g., @gmail.com, @yahoo.com)")
28
- setLoading(false)
29
- return
30
- }
31
-
32
- if (isLogin) {
33
- const { data, error } = await supabase.auth.signInWithPassword({ email, password })
34
- if (error) {
35
- setError(error.message)
36
- setLoading(false)
37
- return
38
- }
39
-
40
- // Fetch role
41
- const { data: profile } = await supabase.from('profiles').select('role').eq('id', data.user.id).single()
42
- if (profile?.role === 'employer') {
43
- router.push('/employer')
44
- } else {
45
- router.push('/candidate')
46
- }
47
- } else {
48
- const { data, error } = await supabase.auth.signUp({ email, password })
49
- if (error) {
50
- setError(error.message)
51
- setLoading(false)
52
- return
53
- }
54
- if (data.user) {
55
- const { error: profileError } = await supabase.from('profiles').insert({
56
- id: data.user.id,
57
- email,
58
- role
59
- })
60
- if (profileError) {
61
- setError(profileError.message)
62
- setLoading(false)
63
- return
64
- }
65
- if (role === 'employer') {
66
- router.push('/employer')
67
- } else {
68
- router.push('/candidate')
69
- }
70
- }
71
- }
72
- }
73
-
74
- return (
75
- <div className="min-h-screen relative bg-bg text-text flex items-center justify-center p-6 overflow-hidden">
76
- <div className="relative z-10 max-w-md w-full bg-surface border border-border shadow-md p-8 rounded-3xl shadow-xl">
77
- <h1 className="text-4xl font-bold font-mono text-center tracking-tighter mb-2">
78
- SkillForge<span className="text-accent">.ai</span>
79
- </h1>
80
- <h2 className="text-lg text-center text-muted font-mono mb-8">{isLogin ? "Access your portal" : "Create your account"}</h2>
81
-
82
- {error && <div className="bg-red-500/10 border border-red-500/50 text-red-500 p-4 rounded-xl mb-6 text-sm font-mono">{error}</div>}
83
-
84
- <form onSubmit={handleAuth} className="space-y-5">
85
- <div>
86
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">Email Address</label>
87
- <input
88
- type="email"
89
- required
90
- value={email}
91
- onChange={e => setEmail(e.target.value)}
92
- className="w-full bg-background border border-border rounded-xl p-3 text-sm focus:outline-none focus:border-accent transition-colors"
93
- placeholder="you@example.com"
94
- />
95
- </div>
96
- <div>
97
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">Password</label>
98
- <input
99
- type="password"
100
- required
101
- value={password}
102
- onChange={e => setPassword(e.target.value)}
103
- className="w-full bg-background border border-border rounded-xl p-3 text-sm focus:outline-none focus:border-accent transition-colors"
104
- placeholder="••••••••"
105
- />
106
- </div>
107
-
108
- {!isLogin && (
109
- <div>
110
- <label className="block text-xs uppercase tracking-wider font-mono text-muted mb-2">I am a...</label>
111
- <div className="flex gap-4">
112
- <button
113
- type="button"
114
- onClick={() => setRole("candidate")}
115
- className={`flex-1 p-3 rounded-xl border font-mono transition-all duration-300 ${role === "candidate" ? "border-accent bg-accent/10 text-accent shadow-sm" : "border-border bg-background text-muted hover:border-border"}`}
116
- >
117
- Candidate
118
- </button>
119
- <button
120
- type="button"
121
- onClick={() => setRole("employer")}
122
- className={`flex-1 p-3 rounded-xl border font-mono transition-all duration-300 ${role === "employer" ? "border-accent bg-accent/10 text-accent shadow-sm" : "border-border bg-background text-muted hover:border-border"}`}
123
- >
124
- Employer
125
- </button>
126
- </div>
127
- </div>
128
- )}
129
-
130
- <button
131
- type="submit"
132
- disabled={loading}
133
- className="w-full bg-accent text-white font-bold font-mono py-4 rounded-xl hover:scale-[1.02] shadow-md disabled:opacity-50 disabled:hover:scale-100 transition-all mt-4"
134
- >
135
- {loading ? "Authenticating..." : (isLogin ? "Secure Login →" : "Create Account →")}
136
- </button>
137
- </form>
138
-
139
- <div className="text-center mt-8 text-muted text-sm font-mono">
140
- {isLogin ? "Don't have an account? " : "Already have an account? "}
141
- <button onClick={() => setIsLogin(!isLogin)} className="text-accent hover:underline">
142
- {isLogin ? "Sign Up" : "Log In"}
143
- </button>
144
- </div>
145
- </div>
146
- </div>
147
- )
148
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/app/page.tsx CHANGED
@@ -1,42 +1,86 @@
1
  "use client"
 
2
  import { useRouter } from "next/navigation"
 
 
 
3
 
4
  export default function LandingPage() {
5
  const router = useRouter()
 
 
6
 
7
- return (
8
- <main className="min-h-screen relative bg-bg text-text flex items-center justify-center p-6 overflow-hidden">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- <div className="relative z-10 w-full max-w-4xl flex flex-col items-center text-center">
11
- <h1 className="text-5xl md:text-7xl font-bold font-mono text-text mb-4 tracking-tighter">SkillForge<span className="text-accent">.ai</span></h1>
12
- <p className="text-xl text-muted mb-12 font-mono max-w-2xl">
13
- The next-generation technical assessment platform. Move beyond basic resume parsing and test real capability.
14
- </p>
 
 
 
 
15
 
16
- <div className="flex flex-col md:flex-row gap-6 w-full max-w-2xl">
17
- <div className="flex-1 bg-surface border border-border shadow-sm p-8 rounded-3xl flex flex-col items-center">
18
- <h2 className="text-2xl font-mono text-accent mb-4">For Employers</h2>
19
- <p className="text-muted text-sm mb-8 text-center">Post jobs, set requirements, and let AI conduct the first-round technical interviews for you.</p>
20
- <button
21
- onClick={() => router.push('/login')}
22
- className="w-full bg-background border border-border text-text px-6 py-3 rounded-xl font-mono hover:border-accent hover:text-accent transition-colors"
23
- >
24
- Employer Portal
25
- </button>
 
 
 
 
 
26
  </div>
 
27
 
28
- <div className="flex-1 bg-surface border border-border shadow-sm p-8 rounded-3xl flex flex-col items-center">
29
- <h2 className="text-2xl font-mono text-accent mb-4">For Candidates</h2>
30
- <p className="text-muted text-sm mb-8 text-center">Apply to open roles, take interactive AI-driven technical assessments, and prove your skills.</p>
31
- <button
32
- onClick={() => router.push('/login')}
33
- className="w-full bg-accent text-white px-6 py-3 rounded-xl font-mono font-bold hover:scale-[1.02] shadow-md transition-all"
34
- >
35
- Candidate Portal
36
- </button>
 
 
37
  </div>
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
  </div>
40
- </main>
41
  )
42
  }
 
1
  "use client"
2
+ import { useState } from "react"
3
  import { useRouter } from "next/navigation"
4
+ import { useStore } from "@/lib/store"
5
+ import { startAssessment } from "@/lib/api"
6
+ import UploadZone from "@/components/upload/UploadZone"
7
 
8
  export default function LandingPage() {
9
  const router = useRouter()
10
+
11
+ const { hoursPerDay, setHoursPerDay, setJdText, resumeText, setResumeText, setAssessmentId, setExtraction } = useStore()
12
 
13
+ const [jdTextLocal, setJdTextLocal] = useState("")
14
+ const [starting, setStarting] = useState(false)
15
+ const [error, setError] = useState("")
16
+
17
+ const handleBegin = async () => {
18
+ if (!jdTextLocal || !resumeText) return
19
+ setStarting(true)
20
+ setError("")
21
+
22
+ try {
23
+ // 1. Start Python Backend Assessment
24
+ const res = await startAssessment(jdTextLocal, resumeText, hoursPerDay)
25
+
26
+ // 2. Save to Zustand store and navigate
27
+ setJdText(jdTextLocal)
28
+ setAssessmentId(res.assessment_id)
29
+ setExtraction(res.extraction)
30
+ router.push(`/assess/${res.assessment_id}`)
31
 
32
+ } catch (err: any) {
33
+ setError(err.message)
34
+ setStarting(false)
35
+ }
36
+ }
37
+
38
+ return (
39
+ <div className="min-h-screen bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
40
+ <div className="max-w-4xl mx-auto space-y-8">
41
 
42
+ <header className="border-b border-border pb-6 text-center">
43
+ <h1 className="text-4xl md:text-5xl font-bold font-mono tracking-tighter mb-4 text-accent">SkillForge</h1>
44
+ <p className="text-muted font-mono text-sm">Upload a Job Description and a Candidate Resume to begin the AI assessment.</p>
45
+ </header>
46
+
47
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
48
+ <div className="bg-surface border border-border shadow-sm p-6 rounded-3xl">
49
+ <h2 className="text-lg font-mono text-text mb-4">1. Job Description</h2>
50
+ <UploadZone label="Upload JD (PDF)" endpoint="jd" onUpload={setJdTextLocal} />
51
+ {jdTextLocal && <div className="mt-4 text-xs text-green-600 font-mono">✓ JD Extracted</div>}
52
+ </div>
53
+ <div className="bg-surface border border-border shadow-sm p-6 rounded-3xl">
54
+ <h2 className="text-lg font-mono text-text mb-4">2. Candidate Resume</h2>
55
+ <UploadZone label="Upload Resume (PDF)" endpoint="resume" onUpload={setResumeText} />
56
+ {resumeText && <div className="mt-4 text-xs text-green-600 font-mono">✓ Resume Extracted</div>}
57
  </div>
58
+ </div>
59
 
60
+ <div className="bg-surface border border-border shadow-sm p-8 rounded-3xl space-y-8 max-w-lg mx-auto">
61
+ <div className="bg-gray-50 border border-border p-5 rounded-xl">
62
+ <label className="flex justify-between text-sm font-mono text-muted mb-4">
63
+ <span>Availability (hours/day)</span>
64
+ <span className="text-accent font-bold">{hoursPerDay}h</span>
65
+ </label>
66
+ <input
67
+ type="range" min="1" max="8" step="0.5"
68
+ value={hoursPerDay} onChange={(e) => setHoursPerDay(parseFloat(e.target.value))}
69
+ className="w-full h-1.5 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-accent transition-all hover:h-2"
70
+ />
71
  </div>
72
+
73
+ {error && <div className="text-red-500 font-mono text-sm text-center">{error}</div>}
74
+
75
+ <button
76
+ onClick={handleBegin}
77
+ disabled={!resumeText || !jdTextLocal || starting}
78
+ className="w-full bg-accent text-white px-8 py-4 rounded-xl font-mono font-bold text-lg hover:scale-[1.02] shadow-sm disabled:opacity-50 disabled:hover:scale-100 transition-all"
79
+ >
80
+ {starting ? "Initializing Engine..." : "Begin AI Interview →"}
81
+ </button>
82
  </div>
83
  </div>
84
+ </div>
85
  )
86
  }
frontend/app/results/[id]/page.tsx CHANGED
@@ -3,7 +3,6 @@ import { useEffect, useState } from "react"
3
  import { useParams, useRouter } from "next/navigation"
4
  import { useStore } from "@/lib/store"
5
  import { getRoadmap } from "@/lib/api"
6
- import { createClient } from "@/utils/supabase/client"
7
  import SkillHeatmap from "@/components/results/SkillHeatmap"
8
  import RoadmapTimeline from "@/components/results/RoadmapTimeline"
9
  import GraphPathViz from "@/components/results/GraphPathViz"
@@ -12,60 +11,28 @@ export default function ResultsPage() {
12
  const params = useParams()
13
  const id = typeof params.id === "string" ? params.id : null
14
  const router = useRouter()
15
- const { hoursPerDay, setHoursPerDay, result, setResult, resetAssessment } = useStore()
16
  const [mounted, setMounted] = useState(false)
17
  const [error, setError] = useState("")
18
- const [resumeText, setResumeText] = useState("")
19
- const [role, setRole] = useState("")
20
- const [candidateInfo, setCandidateInfo] = useState<{name: string, email: string} | null>(null)
21
 
22
  useEffect(() => {
23
  setMounted(true)
24
  if (!id) return
25
- const loadResults = async () => {
26
- const supabase = createClient()
27
-
28
- const { data: { user } } = await supabase.auth.getUser()
29
- if (user) {
30
- const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single()
31
- if (profile) setRole(profile.role)
32
- }
33
-
34
- // 1. Try to load completed result from Supabase
35
- const { data: assessment } = await supabase.from('assessments').select('status, result_data, resume_text, profiles(full_name, email)').eq('id', id).single()
36
-
37
- if (assessment) {
38
- setResumeText(assessment.resume_text || "")
39
- if (assessment.profiles) {
40
- setCandidateInfo({
41
- name: assessment.profiles.full_name || "",
42
- email: assessment.profiles.email || ""
43
- })
44
- }
45
- if (assessment.status === 'completed' && assessment.result_data) {
46
- setResult(assessment.result_data)
47
- return
48
  }
49
  }
50
-
51
- // 2. If not completed or not in Supabase, try to fetch from Python backend (live computation)
52
- try {
53
- const res = await getRoadmap(id, hoursPerDay)
54
- setResult(res)
55
-
56
- // 3. Save the newly computed result to Supabase
57
- await supabase.from('assessments').update({
58
- status: 'completed',
59
- result_data: res
60
- }).eq('id', id)
61
- } catch (err) {
62
- console.error("Failed to load roadmap:", err)
63
- setError("This assessment session was interrupted or wiped from memory before completion.")
64
- }
65
  }
66
-
67
- loadResults()
68
- }, [id, hoursPerDay, setResult, router])
69
 
70
  if (error) {
71
  return (
@@ -89,31 +56,6 @@ export default function ResultsPage() {
89
  ? Math.round((result.skill_scores.reduce((sum: number, s: any) => sum + s.final_score, 0) / result.skill_scores.length) * 100)
90
  : 0;
91
 
92
- if (role === 'candidate') {
93
- return (
94
- <div className="min-h-screen flex flex-col items-center justify-center bg-bg font-sans text-center p-6 space-y-6">
95
- <div className="bg-surface border border-border p-12 rounded-3xl shadow-xl max-w-lg w-full flex flex-col items-center">
96
- <div className="w-20 h-20 bg-green-500/10 text-green-500 rounded-full flex items-center justify-center mb-6">
97
- <svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7"></path></svg>
98
- </div>
99
- <h1 className="text-3xl font-bold text-text mb-4">Assessment Submitted</h1>
100
- <p className="text-muted mb-8">
101
- Your technical assessment has been successfully completed and securely transmitted to the employer. They will review your results and contact you soon.
102
- </p>
103
- <button
104
- onClick={() => {
105
- resetAssessment()
106
- router.push('/candidate')
107
- }}
108
- className="w-full bg-accent text-white font-bold py-4 rounded-xl hover:scale-[1.02] shadow-md transition-all"
109
- >
110
- Return to Candidate Portal
111
- </button>
112
- </div>
113
- </div>
114
- )
115
- }
116
-
117
  return (
118
  <div className="min-h-screen relative bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
119
 
@@ -124,11 +66,6 @@ export default function ResultsPage() {
124
  <h1 className="text-4xl md:text-5xl font-bold font-mono tracking-tighter mb-3">
125
  <span className="text-accent">Assessment</span> Results
126
  </h1>
127
- {candidateInfo && (
128
- <p className="text-lg font-mono text-text mb-2">
129
- Candidate: <span className="font-bold">{candidateInfo.name || candidateInfo.email}</span>
130
- </p>
131
- )}
132
  <p className="text-muted font-mono flex items-center">
133
  <span className="w-2 h-2 rounded-full bg-accent mr-3 animate-pulse"></span>
134
  Profile: {result.extraction.seniority_level} {result.extraction.domain}
@@ -141,9 +78,9 @@ export default function ResultsPage() {
141
  <span className="text-accent font-bold">{hoursPerDay}h/day</span>
142
  </label>
143
  <input
144
- type="range" min="1" max="8" step="0.5"
145
  value={hoursPerDay} onChange={(e) => setHoursPerDay(parseFloat(e.target.value))}
146
- className="w-full h-1.5 bg-gray-300 rounded-lg appearance-none cursor-pointer accent-accent transition-all hover:h-2"
147
  />
148
  </div>
149
  </header>
 
3
  import { useParams, useRouter } from "next/navigation"
4
  import { useStore } from "@/lib/store"
5
  import { getRoadmap } from "@/lib/api"
 
6
  import SkillHeatmap from "@/components/results/SkillHeatmap"
7
  import RoadmapTimeline from "@/components/results/RoadmapTimeline"
8
  import GraphPathViz from "@/components/results/GraphPathViz"
 
11
  const params = useParams()
12
  const id = typeof params.id === "string" ? params.id : null
13
  const router = useRouter()
14
+ const { hoursPerDay, setHoursPerDay, result, setResult, resetAssessment, resumeText } = useStore()
15
  const [mounted, setMounted] = useState(false)
16
  const [error, setError] = useState("")
 
 
 
17
 
18
  useEffect(() => {
19
  setMounted(true)
20
  if (!id) return
21
+
22
+ // Only fetch if we don't have results for this ID
23
+ if (!result || result.skill_scores === undefined) {
24
+ const loadResults = async () => {
25
+ try {
26
+ const res = await getRoadmap(id, hoursPerDay)
27
+ setResult(res)
28
+ } catch (err) {
29
+ console.error("Failed to load roadmap:", err)
30
+ setError("This assessment session was interrupted or wiped from memory before completion.")
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
  }
33
+ loadResults()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
+ }, [id, hoursPerDay, result, setResult])
 
 
36
 
37
  if (error) {
38
  return (
 
56
  ? Math.round((result.skill_scores.reduce((sum: number, s: any) => sum + s.final_score, 0) / result.skill_scores.length) * 100)
57
  : 0;
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  return (
60
  <div className="min-h-screen relative bg-bg text-text p-6 md:p-12 font-sans overflow-x-hidden">
61
 
 
66
  <h1 className="text-4xl md:text-5xl font-bold font-mono tracking-tighter mb-3">
67
  <span className="text-accent">Assessment</span> Results
68
  </h1>
 
 
 
 
 
69
  <p className="text-muted font-mono flex items-center">
70
  <span className="w-2 h-2 rounded-full bg-accent mr-3 animate-pulse"></span>
71
  Profile: {result.extraction.seniority_level} {result.extraction.domain}
 
78
  <span className="text-accent font-bold">{hoursPerDay}h/day</span>
79
  </label>
80
  <input
81
+ type="range" min="1" max="8" step="0.5" disabled
82
  value={hoursPerDay} onChange={(e) => setHoursPerDay(parseFloat(e.target.value))}
83
+ className="w-full h-1.5 bg-gray-300 rounded-lg appearance-none cursor-not-allowed accent-accent opacity-70"
84
  />
85
  </div>
86
  </header>