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 +86 -0
- src/app/api/cv-profile/profile-detail/route.ts +42 -0
- src/components/dashboard/detail-dialog.tsx +144 -83
- src/lib/scoring-service.ts +5 -13
- src/types/profile.ts +30 -0
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 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
<div>
|
| 19 |
<h3 className="text-sm text-gray-500">{label}</h3>
|
| 20 |
-
<div className="text-base">{value
|
| 21 |
</div>
|
| 22 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
return (
|
| 25 |
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 26 |
<DialogContent className="sm:max-w-2xl">
|
|
|
|
| 27 |
<DialogHeader>
|
| 28 |
-
<DialogTitle>Profile
|
| 29 |
</DialogHeader>
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
<
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 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 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
: "-"}
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
|
| 89 |
-
|
| 90 |
-
<
|
| 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 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 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 |
+
}
|