Yvonne Priscilla commited on
Commit
3253dc0
·
1 Parent(s): bf5a50c

update score calculation on server and update detail profile (get from backend instead read manually from db)

Browse files
src/app/api/agentic/route.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CalculateWeightPayload, WeightBody } from "@/types/calculate";
2
+ import { FilterFormValues } from "@/types/candidate-table";
3
+ import { cookies } from "next/headers";
4
+ import { NextRequest, NextResponse } from "next/server";
5
+
6
+ function toFilterBody(filters: FilterFormValues) {
7
+ return {
8
+ gpa_edu_1: filters.educations[0]?.gpa || null,
9
+ univ_edu_1: filters.educations[0]?.university || null,
10
+ major_edu_1: filters.educations[0]?.major || null,
11
+ gpa_edu_2: filters.educations[1]?.gpa || null,
12
+ univ_edu_2: filters.educations[1]?.university || null,
13
+ major_edu_2: filters.educations[1]?.major || null,
14
+ gpa_edu_3: filters.educations[2]?.gpa || null,
15
+ univ_edu_3: filters.educations[2]?.university || null,
16
+ major_edu_3: filters.educations[2]?.major || null,
17
+ domicile: filters.domicile || null,
18
+ yoe: filters.yoe || null,
19
+ hardskills: filters.hardskills?.length ? [filters.hardskills] : null,
20
+ softskills: filters.softskills?.length ? [filters.softskills] : null,
21
+ certifications: filters.certifications?.length ? [filters.certifications] : null,
22
+ business_domain: filters.businessDomain?.length ? [filters.businessDomain] : null,
23
+ }
24
+ }
25
+
26
+ function toWeightBody(value: CalculateWeightPayload): WeightBody {
27
+ const edu = (i: number) => value.education[i] ?? { university: 0, major: 0, gpa: 0 }
28
+ return {
29
+ univ_edu_1: edu(0).university / 100, major_edu_1: edu(0).major / 100, gpa_edu_1: edu(0).gpa / 100,
30
+ univ_edu_2: edu(1).university / 100, major_edu_2: edu(1).major / 100, gpa_edu_2: edu(1).gpa / 100,
31
+ univ_edu_3: edu(2).university / 100, major_edu_3: edu(2).major / 100, gpa_edu_3: edu(2).gpa / 100,
32
+ domicile: value.others.domicile / 100,
33
+ yoe: value.others.yearOfExperiences / 100,
34
+ hardskills: value.others.hardskills / 100,
35
+ softskills: value.others.softskills / 100,
36
+ certifications: value.others.certifications / 100,
37
+ business_domain: value.others.businessDomain / 100,
38
+ }
39
+ }
40
+
41
+ export async function POST(request: NextRequest) {
42
+ const cookieStore = await cookies();
43
+ const token = cookieStore.get('auth_token')?.value;
44
+ const body = await request.json()
45
+
46
+ const resCreateFilter = await fetch(
47
+ `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/create_filter`,
48
+ {
49
+ method: "POST",
50
+ headers: {
51
+ Authorization: `Bearer ${token}`,
52
+ "Content-Type": "application/json",
53
+ },
54
+ body: JSON.stringify(toFilterBody(body.filters)),
55
+ }
56
+ )
57
+
58
+ const dataFilter = await resCreateFilter.json()
59
+
60
+ const resCreateWeight = await fetch(
61
+ `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/create_weight?criteria_id=${dataFilter.criteria_id}`,
62
+ {
63
+ method: "POST",
64
+ headers: {
65
+ Authorization: `Bearer ${token}`,
66
+ "Content-Type": "application/json",
67
+ },
68
+ body: JSON.stringify(toWeightBody(body.weights)),
69
+ }
70
+ )
71
+
72
+ const dataWeight = await resCreateWeight.json()
73
+
74
+ const resCalculate = await fetch(
75
+ `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/agentic/calculate_score?weight_id=${dataWeight.data.weight_id}`,
76
+ {
77
+ method: "POST",
78
+ headers: {
79
+ Authorization: `Bearer ${token}`,
80
+ "Content-Type": "application/json",
81
+ }
82
+ }
83
+ )
84
+
85
+ return NextResponse.json(dataFilter, { status: resCalculate.status })
86
+ }
src/app/api/cv-profile/profile-detail/route.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cookies } from "next/headers";
2
+ import { NextResponse } from "next/server";
3
+
4
+ export async function GET(req: Request) {
5
+ try {
6
+ const cookieStore = await cookies();
7
+ const token = cookieStore.get("auth_token")?.value;
8
+
9
+ if (!token) {
10
+ return NextResponse.json(
11
+ { message: "Not authenticated" },
12
+ { status: 401 },
13
+ );
14
+ }
15
+
16
+ const { searchParams } = new URL(req.url);
17
+ const profile_id = searchParams.get("profile_id");
18
+
19
+ const response = await fetch(
20
+ `https://byteriot-candidateexplorer.hf.space/CandidateExplorer/profile/${profile_id}`,
21
+ {
22
+ headers: { Authorization: `Bearer ${token}` },
23
+ },
24
+ );
25
+
26
+ if (!response.ok) {
27
+ return NextResponse.json(
28
+ { message: 'Invalid token' },
29
+ { status: 401 }
30
+ );
31
+ }
32
+
33
+ const scoreCardData = await response.json();
34
+ return NextResponse.json(scoreCardData, { status: 200 });
35
+ } catch (error) {
36
+ console.error("Get Profile error:", error);
37
+ return NextResponse.json(
38
+ { message: "Failed to fetch profile" },
39
+ { status: 500 },
40
+ );
41
+ }
42
+ }
src/components/dashboard/detail-dialog.tsx CHANGED
@@ -1,7 +1,10 @@
1
  'use client';
2
 
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
 
4
  import { Candidate } from '@/types/candidate-table';
 
 
5
  import { Download } from 'lucide-react';
6
  import { Badge } from '../ui/badge';
7
 
@@ -11,107 +14,165 @@ interface DetailDialogProps {
11
  candidate?: Candidate | null;
12
  }
13
 
14
- export function DetailDialog({ open, onOpenChange, candidate }: DetailDialogProps) {
15
- if (!candidate) return null;
 
 
 
 
 
 
 
 
16
 
17
- const InfoItem = ({ label, value }: { label: string; value: React.ReactNode }) => (
18
  <div>
19
  <h3 className="text-sm text-gray-500">{label}</h3>
20
- <div className="text-base">{value || "-"}</div>
21
  </div>
22
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  return (
25
  <Dialog open={open} onOpenChange={onOpenChange}>
26
  <DialogContent className="sm:max-w-2xl">
 
27
  <DialogHeader>
28
- <DialogTitle>Profile Candidates</DialogTitle>
29
  </DialogHeader>
30
 
31
- <div className="grid grid-cols-2 gap-4">
32
- <InfoItem label="Full Name" value={candidate.fullname} />
33
- <InfoItem label="Domicile" value={candidate.domicile} />
34
- <InfoItem label="Year of Experience" value={candidate.yoe ? `${candidate.yoe} year${candidate.yoe !== 1 ? "s" : ""}` : "-"} />
35
- <InfoItem label="Filename" value={candidate.filename} />
36
-
37
- {/* Education 1 */}
38
- {candidate.univ_edu_1 && (
39
- <InfoItem label="University 1" value={candidate.univ_edu_1} />
40
- )}
41
- {candidate.major_edu_1 && (
42
- <InfoItem label="Major 1" value={candidate.major_edu_1} />
43
- )}
44
- {candidate.gpa_edu_1 && (
45
- <InfoItem label="GPA 1" value={candidate.gpa_edu_1} />
46
- )}
47
-
48
- {/* Education 2 */}
49
- {candidate.univ_edu_2 && (
50
- <InfoItem label="University 2" value={candidate.univ_edu_2} />
51
- )}
52
- {candidate.major_edu_2 && (
53
- <InfoItem label="Major 2" value={candidate.major_edu_2} />
54
- )}
55
- {candidate.gpa_edu_2 && (
56
- <InfoItem label="GPA 2" value={candidate.gpa_edu_2} />
57
- )}
58
-
59
- {/* Education 3 */}
60
- {candidate.univ_edu_3 && (
61
- <InfoItem label="University 3" value={candidate.univ_edu_3} />
62
- )}
63
- {candidate.major_edu_3 && (
64
- <InfoItem label="Major 3" value={candidate.major_edu_3} />
65
- )}
66
- {candidate.gpa_edu_3 && (
67
- <InfoItem label="GPA 3" value={candidate.gpa_edu_3} />
68
- )}
69
-
70
- {/* Array fields */}
71
- <div className="col-span-2">
72
- <h3 className="text-sm text-gray-500 mb-1">Hardskills</h3>
73
- <div className="flex flex-wrap gap-2">
74
- {candidate.hardskills?.length > 0
75
- ? candidate.hardskills.map((s) => <Badge variant="outline" key={s}>{s}</Badge>)
76
- : "-"}
77
- </div>
78
  </div>
 
79
 
80
- <div className="col-span-2">
81
- <h3 className="text-sm text-gray-500 mb-1">Softskills</h3>
82
- <div className="flex flex-wrap gap-2">
83
- {candidate.softskills?.length > 0
84
- ? candidate.softskills.map((s) => <Badge variant="outline" key={s}>{s}</Badge>)
85
- : "-"}
86
- </div>
87
- </div>
88
 
89
- <div className="col-span-2">
90
- <h3 className="text-sm text-gray-500 mb-1">Certifications</h3>
91
- <div className="flex flex-wrap gap-2">
92
- {candidate.certifications?.length > 0
93
- ? candidate.certifications.map((s) => <Badge variant="outline" key={s}>{s}</Badge>)
94
- : "-"}
95
- </div>
96
- </div>
97
 
98
- <div className="col-span-2">
99
- <h3 className="text-sm text-gray-500 mb-1">Business Domain</h3>
100
- <div className="flex flex-wrap gap-2">
101
- {candidate.business_domain?.length > 0
102
- ? candidate.business_domain.map((s) => <Badge variant="outline" key={s}>{s}</Badge>)
103
- : "-"}
104
- </div>
105
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- <div>
108
- <h3 className="text-sm text-gray-500">CV</h3>
109
- <button className="mt-1 flex items-center gap-1 text-sm text-green-600 hover:underline">
110
- <Download className="size-4" />
111
- {candidate.filename}
112
- </button>
113
  </div>
114
- </div>
 
115
  </DialogContent>
116
  </Dialog>
117
  );
 
1
  'use client';
2
 
3
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
4
+ import { authFetch } from '@/lib/api';
5
  import { Candidate } from '@/types/candidate-table';
6
+ import { Profile } from '@/types/profile';
7
+ import { useQuery } from '@tanstack/react-query';
8
  import { Download } from 'lucide-react';
9
  import { Badge } from '../ui/badge';
10
 
 
14
  candidate?: Candidate | null;
15
  }
16
 
17
+ // Helper: check if value is meaningful
18
+ const isValidValue = (value: any) => {
19
+ if (value === null || value === undefined) return false
20
+ if (typeof value === "string" && value.trim() === "") return false
21
+ if (typeof value === "number" && value === 0) return false
22
+ return true
23
+ }
24
+
25
+ const InfoItem = ({ label, value }: { label: string; value: React.ReactNode }) => {
26
+ if (!isValidValue(value)) return null
27
 
28
+ return (
29
  <div>
30
  <h3 className="text-sm text-gray-500">{label}</h3>
31
+ <div className="text-base">{value}</div>
32
  </div>
33
  )
34
+ }
35
+
36
+ export function DetailDialog({ open, onOpenChange, candidate }: DetailDialogProps) {
37
+
38
+ const fetchProfile = async (profileId: string): Promise<Profile> => {
39
+ const res = await authFetch(`/api/cv-profile/profile-detail?profile_id=${profileId}`)
40
+ if (!res.ok) throw new Error("Failed to fetch profile")
41
+ return res.json()
42
+ }
43
+
44
+ const { data, isLoading } = useQuery({
45
+ queryKey: ["profile-detail", candidate?.profile_id],
46
+ queryFn: () => fetchProfile(candidate!.profile_id),
47
+ enabled: open && !!candidate?.profile_id,
48
+ staleTime: 0,
49
+ refetchOnWindowFocus: false,
50
+ })
51
+
52
+ const handleDownload = () => {
53
+ if (!data?.url) return
54
+ window.open(data.url, '_blank')
55
+ }
56
 
57
  return (
58
  <Dialog open={open} onOpenChange={onOpenChange}>
59
  <DialogContent className="sm:max-w-2xl">
60
+
61
  <DialogHeader>
62
+ <DialogTitle>Profile Candidate</DialogTitle>
63
  </DialogHeader>
64
 
65
+ {/* ===================== */}
66
+ {/* 🔹 LOADING SKELETON */}
67
+ {/* ===================== */}
68
+ {isLoading && (
69
+ <div className="grid grid-cols-2 gap-4 animate-pulse">
70
+ {Array.from({ length: 10 }).map((_, i) => (
71
+ <div key={i}>
72
+ <div className="h-3 bg-gray-200 rounded w-24 mb-2" />
73
+ <div className="h-4 bg-gray-200 rounded w-full" />
74
+ </div>
75
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  </div>
77
+ )}
78
 
79
+ {/* ===================== */}
80
+ {/* 🔹 CONTENT */}
81
+ {/* ===================== */}
82
+ {!isLoading && data && (
83
+ <div className="grid grid-cols-2 gap-4">
 
 
 
84
 
85
+ <InfoItem label="Full Name" value={data.fullname} />
86
+ <InfoItem label="Domicile" value={data.domicile} />
 
 
 
 
 
 
87
 
88
+ <InfoItem
89
+ label="Year of Experience"
90
+ value={
91
+ isValidValue(data.yoe)
92
+ ? `${data.yoe} year${data.yoe !== 1 ? "s" : ""}`
93
+ : null
94
+ }
95
+ />
96
+
97
+ <InfoItem label="Filename" value={data.filename} />
98
+
99
+ {/* Education 1 */}
100
+ <InfoItem label="University 1" value={data.univ_edu_1} />
101
+ <InfoItem label="Major 1" value={data.major_edu_1} />
102
+ <InfoItem label="GPA 1" value={data.gpa_edu_1} />
103
+
104
+ {/* Education 2 */}
105
+ <InfoItem label="University 2" value={data.univ_edu_2} />
106
+ <InfoItem label="Major 2" value={data.major_edu_2} />
107
+ <InfoItem label="GPA 2" value={data.gpa_edu_2} />
108
+
109
+ {/* Education 3 */}
110
+ <InfoItem label="University 3" value={data.univ_edu_3} />
111
+ <InfoItem label="Major 3" value={data.major_edu_3} />
112
+ <InfoItem label="GPA 3" value={data.gpa_edu_3} />
113
+
114
+ {/* Arrays */}
115
+ {data.hardskills?.length > 0 && (
116
+ <div className="col-span-2">
117
+ <h3 className="text-sm text-gray-500 mb-1">Hardskills</h3>
118
+ <div className="flex flex-wrap gap-2">
119
+ {data.hardskills.map((s) => (
120
+ <Badge variant="outline" key={s}>{s}</Badge>
121
+ ))}
122
+ </div>
123
+ </div>
124
+ )}
125
+
126
+ {data.softskills?.length > 0 && (
127
+ <div className="col-span-2">
128
+ <h3 className="text-sm text-gray-500 mb-1">Softskills</h3>
129
+ <div className="flex flex-wrap gap-2">
130
+ {data.softskills.map((s) => (
131
+ <Badge variant="outline" key={s}>{s}</Badge>
132
+ ))}
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {data.certifications?.length > 0 && (
138
+ <div className="col-span-2">
139
+ <h3 className="text-sm text-gray-500 mb-1">Certifications</h3>
140
+ <div className="flex flex-wrap gap-2">
141
+ {data.certifications.map((s) => (
142
+ <Badge variant="outline" key={s}>{s}</Badge>
143
+ ))}
144
+ </div>
145
+ </div>
146
+ )}
147
+
148
+ {data.business_domain?.length > 0 && (
149
+ <div className="col-span-2">
150
+ <h3 className="text-sm text-gray-500 mb-1">Business Domain</h3>
151
+ <div className="flex flex-wrap gap-2">
152
+ {data.business_domain.map((s) => (
153
+ <Badge variant="outline" key={s}>{s}</Badge>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ )}
158
+
159
+ {/* Download */}
160
+ {isValidValue(data.url) && (
161
+ <div>
162
+ <h3 className="text-sm text-gray-500">CV</h3>
163
+ <button
164
+ className="mt-1 flex items-center gap-1 text-sm text-green-600 hover:underline"
165
+ onClick={handleDownload}
166
+ >
167
+ <Download className="size-4" />
168
+ {data.filename}
169
+ </button>
170
+ </div>
171
+ )}
172
 
 
 
 
 
 
 
173
  </div>
174
+ )}
175
+
176
  </DialogContent>
177
  </Dialog>
178
  );
src/lib/scoring-service.ts CHANGED
@@ -53,17 +53,9 @@ export async function createAndCalculateScore(
53
  filters: FilterFormValues,
54
  weights: CalculateWeightPayload
55
  ): Promise<{ criteriaId: string }> {
56
- // Step 1 create filter → get criteria_id
57
- const filterData = await postJSON(`${BASE_URL}/create_filter`, toFilterBody(filters))
58
-
59
- // Step 2 — create weight → get weight_id + criteria_id
60
- const weightData = await postJSON(
61
- `${BASE_URL}/create_weight?criteria_id=${filterData.criteria_id}`,
62
- toWeightBody(weights)
63
- )
64
-
65
- // Step 3 — trigger score calculation
66
- await postJSON(`${BASE_URL}/calculate_score?weight_id=${weightData.data.weight_id}`)
67
-
68
- return { criteriaId: weightData.data.criteria_id }
69
  }
 
53
  filters: FilterFormValues,
54
  weights: CalculateWeightPayload
55
  ): Promise<{ criteriaId: string }> {
56
+ const res = await postJSON("/api/agentic", {
57
+ filters,
58
+ weights
59
+ })
60
+ return { criteriaId: res.criteria_id }
 
 
 
 
 
 
 
 
61
  }
src/types/profile.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Profile = {
2
+ profile_id: string
3
+ fullname: string
4
+
5
+ gpa_edu_1: number
6
+ univ_edu_1: string
7
+ major_edu_1: string
8
+
9
+ gpa_edu_2: number
10
+ univ_edu_2: string
11
+ major_edu_2: string
12
+
13
+ gpa_edu_3: number
14
+ univ_edu_3: string
15
+ major_edu_3: string
16
+
17
+ domicile: string
18
+ yoe: number
19
+
20
+ hardskills: string[]
21
+ softskills: string[]
22
+ certifications: string[]
23
+ business_domain: string[]
24
+
25
+ filename: string
26
+ file_id: string
27
+
28
+ created_at: string // ISO date string
29
+ url: string
30
+ }