Halfway through adding AI Risk Analysis. GPT has completely forgotten what we're doing.
This commit is contained in:
parent
6298eedaba
commit
c72e680f2b
@ -157,6 +157,47 @@ try {
|
|||||||
console.error('Error reading economicproj.json:', err);
|
console.error('Error reading economicproj.json:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//AI At Risk helpers
|
||||||
|
async function getRiskAnalysisFromDB(socCode) {
|
||||||
|
const row = await userProfileDb.get(
|
||||||
|
`SELECT * FROM ai_risk_analysis WHERE soc_code = ?`,
|
||||||
|
[socCode]
|
||||||
|
);
|
||||||
|
return row || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to upsert a row
|
||||||
|
async function storeRiskAnalysisInDB({
|
||||||
|
socCode,
|
||||||
|
careerName,
|
||||||
|
jobDescription,
|
||||||
|
tasks,
|
||||||
|
riskLevel,
|
||||||
|
reasoning,
|
||||||
|
}) {
|
||||||
|
// We'll use INSERT OR REPLACE so that if a row with the same soc_code
|
||||||
|
// already exists, it gets replaced (acts like an upsert).
|
||||||
|
const sql = `
|
||||||
|
INSERT OR REPLACE INTO ai_risk_analysis (
|
||||||
|
soc_code,
|
||||||
|
career_name,
|
||||||
|
job_description,
|
||||||
|
tasks,
|
||||||
|
risk_level,
|
||||||
|
reasoning,
|
||||||
|
created_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`;
|
||||||
|
await userProfileDb.run(sql, [
|
||||||
|
socCode,
|
||||||
|
careerName || '',
|
||||||
|
jobDescription || '',
|
||||||
|
tasks || '',
|
||||||
|
riskLevel || '',
|
||||||
|
reasoning || '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* O*Net routes, CIP routes, distance routes, etc.
|
* O*Net routes, CIP routes, distance routes, etc.
|
||||||
**************************************************/
|
**************************************************/
|
||||||
@ -735,6 +776,68 @@ app.get('/api/user-profile/:id', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
* AI RISK ASSESSMENT ENDPOINT READ
|
||||||
|
****************************************************/
|
||||||
|
app.get('/api/ai-risk/:socCode', async (req, res) => {
|
||||||
|
const { socCode } = req.params;
|
||||||
|
try {
|
||||||
|
const row = await getRiskAnalysisFromDB(socCode);
|
||||||
|
if (!row) {
|
||||||
|
return res.status(404).json({ error: 'Not found' });
|
||||||
|
}
|
||||||
|
// Return full data or partial, up to you:
|
||||||
|
res.json({
|
||||||
|
socCode: row.soc_code,
|
||||||
|
careerName: row.career_name,
|
||||||
|
jobDescription: row.job_description,
|
||||||
|
tasks: row.tasks,
|
||||||
|
riskLevel: row.risk_level,
|
||||||
|
reasoning: row.reasoning,
|
||||||
|
created_at: row.created_at,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching AI risk:', err);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
* AI RISK ASSESSMENT ENDPOINT WRITE
|
||||||
|
****************************************************/
|
||||||
|
app.post('/api/ai-risk', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
socCode,
|
||||||
|
careerName,
|
||||||
|
jobDescription,
|
||||||
|
tasks,
|
||||||
|
riskLevel,
|
||||||
|
reasoning,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!socCode) {
|
||||||
|
return res.status(400).json({ error: 'socCode is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store / upsert row
|
||||||
|
await storeRiskAnalysisInDB({
|
||||||
|
socCode,
|
||||||
|
careerName,
|
||||||
|
jobDescription,
|
||||||
|
tasks,
|
||||||
|
riskLevel,
|
||||||
|
reasoning,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({ message: 'AI Risk Analysis stored successfully' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error storing AI risk data:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to store AI risk data.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* Start the Express server
|
* Start the Express server
|
||||||
**************************************************/
|
**************************************************/
|
||||||
|
@ -584,6 +584,92 @@ app.post('/api/premium/milestone/convert-ai', authenticatePremiumUser, async (re
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/***************************************************
|
||||||
|
AI CAREER RISK ANALYSIS ENDPOINT
|
||||||
|
****************************************************/
|
||||||
|
// server3.js
|
||||||
|
app.post('/api/premium/ai-risk-analysis', authenticatePremiumUser, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
socCode,
|
||||||
|
careerName,
|
||||||
|
jobDescription,
|
||||||
|
tasks = []
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!socCode) {
|
||||||
|
return res.status(400).json({ error: 'socCode is required.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Check if we already have it
|
||||||
|
const cached = await getCachedRiskAnalysis(socCode);
|
||||||
|
if (cached) {
|
||||||
|
return res.json({
|
||||||
|
socCode: cached.soc_code,
|
||||||
|
careerName: cached.career_name,
|
||||||
|
jobDescription: cached.job_description,
|
||||||
|
tasks: cached.tasks ? JSON.parse(cached.tasks) : [],
|
||||||
|
riskLevel: cached.risk_level,
|
||||||
|
reasoning: cached.reasoning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) If missing, call GPT-3.5 to generate analysis
|
||||||
|
const prompt = `
|
||||||
|
The user has a career named: ${careerName}
|
||||||
|
Description: ${jobDescription}
|
||||||
|
Tasks: ${tasks.join('; ')}
|
||||||
|
|
||||||
|
Provide AI automation risk analysis for the next 10 years.
|
||||||
|
Return JSON in exactly this format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"riskLevel": "Low|Moderate|High",
|
||||||
|
"reasoning": "Short explanation (< 50 words)."
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: 'gpt-3.5-turbo',
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
temperature: 0.3,
|
||||||
|
max_tokens: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const aiText = completion?.choices?.[0]?.message?.content?.trim() || '';
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(aiText);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing AI JSON:', err);
|
||||||
|
return res.status(500).json({ error: 'Invalid AI JSON response.' });
|
||||||
|
}
|
||||||
|
const { riskLevel, reasoning } = parsed;
|
||||||
|
|
||||||
|
// 3) Store in DB
|
||||||
|
await cacheRiskAnalysis({
|
||||||
|
socCode,
|
||||||
|
careerName,
|
||||||
|
jobDescription,
|
||||||
|
tasks,
|
||||||
|
riskLevel,
|
||||||
|
reasoning
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4) Return the new analysis
|
||||||
|
res.json({
|
||||||
|
socCode,
|
||||||
|
careerName,
|
||||||
|
jobDescription,
|
||||||
|
tasks,
|
||||||
|
riskLevel,
|
||||||
|
reasoning
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in /api/premium/ai-risk-analysis:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to generate AI risk analysis.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
MILESTONE ENDPOINTS
|
MILESTONE ENDPOINTS
|
||||||
|
@ -4,11 +4,40 @@ import axios from 'axios';
|
|||||||
const apiUrl = process.env.REACT_APP_API_URL;
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||||
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [aiRisk, setAiRisk] = useState(null); // <-- store AI risk data
|
||||||
|
const [loadingRisk, setLoadingRisk] = useState(false);
|
||||||
|
|
||||||
console.log('CareerModal props:', { career, careerDetails});
|
// Fetch AI risk whenever we have a valid SOC code from `career.code`
|
||||||
|
useEffect(() => {
|
||||||
|
if (!career?.code) return;
|
||||||
|
|
||||||
|
// e.g. "15-1231.00" => strip down to "15-1231" if needed
|
||||||
|
const strippedSoc = career.code.split('.')[0];
|
||||||
|
setLoadingRisk(true);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`${apiUrl}/ai-risk/${strippedSoc}`)
|
||||||
|
.then((res) => {
|
||||||
|
setAiRisk(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err?.response?.status === 404) {
|
||||||
|
// no AI risk data
|
||||||
|
setAiRisk(null);
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching AI risk:', err);
|
||||||
|
setError('Failed to load AI risk analysis.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingRisk(false);
|
||||||
|
});
|
||||||
|
}, [career, apiUrl]);
|
||||||
|
|
||||||
|
console.log('CareerModal props:', { career, careerDetails, aiRisk });
|
||||||
|
|
||||||
|
// Handle your normal careerDetails loading logic
|
||||||
if (careerDetails?.error) {
|
if (careerDetails?.error) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
||||||
@ -23,19 +52,22 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!careerDetails?.salaryData) {
|
if (!careerDetails?.salaryData) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
<p className="text-lg text-gray-700">Loading career details...</p>
|
<p className="text-lg text-gray-700">Loading career details...</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
if (error) return <div>{error}</div>;
|
|
||||||
|
|
||||||
|
if (error) return <div>{error}</div>;
|
||||||
|
|
||||||
|
// Helper for "stability" rating
|
||||||
const calculateStabilityRating = (salaryData) => {
|
const calculateStabilityRating = (salaryData) => {
|
||||||
const medianSalaryObj = salaryData.find(s => s.percentile === 'Median');
|
const medianSalaryObj = salaryData.find((s) => s.percentile === 'Median');
|
||||||
const medianSalary = medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0;
|
const medianSalary =
|
||||||
|
medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0;
|
||||||
|
|
||||||
if (medianSalary >= 90000) return 5;
|
if (medianSalary >= 90000) return 5;
|
||||||
if (medianSalary >= 70000) return 4;
|
if (medianSalary >= 70000) return 4;
|
||||||
@ -44,29 +76,50 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50">
|
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50">
|
||||||
<div className="bg-white rounded-lg shadow-lg w-full max-w-5xl p-6 m-4 max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg shadow-lg w-full max-w-5xl p-6 m-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-4 pb-2 border-b">
|
{/* Title row */}
|
||||||
<h2 className="text-2xl font-bold text-blue-600">{careerDetails.title}</h2>
|
<div className="flex justify-between items-center mb-4 pb-2 border-b">
|
||||||
<div className="flex gap-2">
|
<div>
|
||||||
<button
|
<h2 className="text-2xl font-bold text-blue-600">
|
||||||
onClick={() => {
|
{careerDetails.title}
|
||||||
const stabilityRating = calculateStabilityRating(careerDetails.salaryData);
|
</h2>
|
||||||
addCareerToList({
|
{/* AI RISK SECTION */}
|
||||||
...careerDetails,
|
{loadingRisk ? (
|
||||||
ratings: {
|
<p className="text-sm text-gray-500 mt-1">Loading AI risk...</p>
|
||||||
stability: stabilityRating
|
) : aiRisk ? (
|
||||||
}
|
<div className="text-sm text-gray-500 mt-1">
|
||||||
});
|
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
|
||||||
}}
|
<br />
|
||||||
className="text-white bg-green-500 hover:bg-green-600 rounded px-3 py-1"
|
<span>{aiRisk.reasoning}</span>
|
||||||
>
|
</div>
|
||||||
Add to Comparison
|
) : (
|
||||||
</button>
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
No AI risk data available
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const stabilityRating = calculateStabilityRating(
|
||||||
|
careerDetails.salaryData
|
||||||
|
);
|
||||||
|
addCareerToList({
|
||||||
|
...careerDetails,
|
||||||
|
ratings: {
|
||||||
|
stability: stabilityRating,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="text-white bg-green-500 hover:bg-green-600 rounded px-3 py-1"
|
||||||
|
>
|
||||||
|
Add to Comparison
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
@ -83,7 +136,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
<p className="text-gray-700">{careerDetails.jobDescription}</p>
|
<p className="text-gray-700">{careerDetails.jobDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tasks (full width) */}
|
{/* Tasks */}
|
||||||
<div className="mb-4 border-t pt-3">
|
<div className="mb-4 border-t pt-3">
|
||||||
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
|
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
|
||||||
<ul className="list-disc pl-5 space-y-1">
|
<ul className="list-disc pl-5 space-y-1">
|
||||||
@ -93,7 +146,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Salary and Economic Projections side-by-side */}
|
{/* Salary & Projections side-by-side */}
|
||||||
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
|
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
|
||||||
{/* Salary Data */}
|
{/* Salary Data */}
|
||||||
<div className="md:w-1/2 overflow-x-auto">
|
<div className="md:w-1/2 overflow-x-auto">
|
||||||
@ -110,8 +163,12 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
{careerDetails.salaryData.map((s, i) => (
|
{careerDetails.salaryData.map((s, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td className="px-3 py-2 border-b">{s.percentile}</td>
|
<td className="px-3 py-2 border-b">{s.percentile}</td>
|
||||||
<td className="px-3 py-2 border-b">${s.regionalSalary.toLocaleString()}</td>
|
<td className="px-3 py-2 border-b">
|
||||||
<td className="px-3 py-2 border-b">${s.nationalSalary.toLocaleString()}</td>
|
${s.regionalSalary.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 border-b">
|
||||||
|
${s.nationalSalary.toLocaleString()}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -121,27 +178,25 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
{/* Economic Projections */}
|
{/* Economic Projections */}
|
||||||
<div className="md:w-1/2 overflow-x-auto">
|
<div className="md:w-1/2 overflow-x-auto">
|
||||||
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
|
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
|
||||||
|
|
||||||
<table className="w-full text-left border border-gray-300 rounded">
|
<table className="w-full text-left border border-gray-300 rounded">
|
||||||
<thead className="bg-gray-100">
|
<thead className="bg-gray-100">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 border-b"></th>
|
<th className="px-3 py-2 border-b"></th>
|
||||||
{/* If we have state data, show a column for it */}
|
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<th className="px-3 py-2 border-b">
|
<th className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.area}
|
{careerDetails.economicProjections.state.area}
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{/* If we have national data, show a column for it */}
|
|
||||||
{careerDetails.economicProjections.national && (
|
{careerDetails.economicProjections.national && (
|
||||||
<th className="px-3 py-2 border-b">National</th>
|
<th className="px-3 py-2 border-b">National</th>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{/* Row for Current Jobs */}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 py-2 border-b font-semibold">Current Jobs</td>
|
<td className="px-3 py-2 border-b font-semibold">
|
||||||
|
Current Jobs
|
||||||
|
</td>
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<td className="px-3 py-2 border-b">
|
<td className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.base.toLocaleString()}
|
{careerDetails.economicProjections.state.base.toLocaleString()}
|
||||||
@ -153,10 +208,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{/* Row for Jobs in 10 yrs */}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 py-2 border-b font-semibold">Jobs in 10 yrs</td>
|
<td className="px-3 py-2 border-b font-semibold">
|
||||||
|
Jobs in 10 yrs
|
||||||
|
</td>
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<td className="px-3 py-2 border-b">
|
<td className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
{careerDetails.economicProjections.state.projection.toLocaleString()}
|
||||||
@ -168,8 +223,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{/* Row for Growth % */}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
@ -183,10 +236,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{/* Row for Annual Openings */}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className="px-3 py-2 border-b font-semibold">Annual Openings</td>
|
<td className="px-3 py-2 border-b font-semibold">
|
||||||
|
Annual Openings
|
||||||
|
</td>
|
||||||
{careerDetails.economicProjections.state && (
|
{careerDetails.economicProjections.state && (
|
||||||
<td className="px-3 py-2 border-b">
|
<td className="px-3 py-2 border-b">
|
||||||
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
|
||||||
@ -201,7 +254,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user