Added CareerCoach - adjusted CareerRoadmap.
This commit is contained in:
parent
5c4a280cf8
commit
86eda98bc5
@ -12,6 +12,7 @@ DB_USER=sqluser
|
|||||||
DB_NAME=user_profile_db
|
DB_NAME=user_profile_db
|
||||||
DB_PASSWORD=ps<g+2DO-eTb2mb5
|
DB_PASSWORD=ps<g+2DO-eTb2mb5
|
||||||
|
|
||||||
|
APTIVA_API_BASE=https://dev1.aptivaai.com/api
|
||||||
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
||||||
REACT_APP_ENV=production
|
REACT_APP_ENV=production
|
||||||
REACT_APP_OPENAI_API_KEY=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
|
REACT_APP_OPENAI_API_KEY=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
|
||||||
|
@ -12,6 +12,7 @@ DB_USER=sqluser
|
|||||||
DB_NAME=user_profile_db
|
DB_NAME=user_profile_db
|
||||||
DB_PASSWORD=ps<g+2DO-eTb2mb5
|
DB_PASSWORD=ps<g+2DO-eTb2mb5
|
||||||
|
|
||||||
|
APTIVA_API_BASE=https://dev1.aptivaai.com/api
|
||||||
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
||||||
REACT_APP_ENV=production
|
REACT_APP_ENV=production
|
||||||
REACT_APP_OPENAI_API_KEY=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
|
REACT_APP_OPENAI_API_KEY=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
|
||||||
|
@ -30,6 +30,7 @@ const app = express();
|
|||||||
const PORT = process.env.PREMIUM_PORT || 5002;
|
const PORT = process.env.PREMIUM_PORT || 5002;
|
||||||
const { getDocument } = pkg;
|
const { getDocument } = pkg;
|
||||||
|
|
||||||
|
|
||||||
// 1) Create a MySQL pool using your environment variables
|
// 1) Create a MySQL pool using your environment variables
|
||||||
const pool = mysql.createPool({
|
const pool = mysql.createPool({
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
@ -364,14 +365,22 @@ app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res)
|
|||||||
|
|
||||||
// 4) Construct ChatGPT messages
|
// 4) Construct ChatGPT messages
|
||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `
|
content: `
|
||||||
You are an expert career & financial coach.
|
You are an expert career & financial coach.
|
||||||
Today's date: ${isoToday}.
|
Today's date: ${isoToday}.
|
||||||
Short-term means any date up to ${isoShortTermLimit} (within 6 months).
|
Short-term means any date up to ${isoShortTermLimit} (within 6 months).
|
||||||
Long-term means a date between ${isoOneYearFromNow} and ${isoThreeYearsFromNow} (1-3 years).
|
Long-term means a date between ${isoOneYearFromNow} and ${isoThreeYearsFromNow} (1-3 years).
|
||||||
All milestone dates must be strictly >= ${isoToday}. Titles must be <= 5 words.
|
All milestone dates must be strictly >= ${isoToday}. Titles must be <= 5 words.
|
||||||
|
|
||||||
|
IMPORTANT RESTRICTIONS:
|
||||||
|
- NEVER suggest specific investments in cryptocurrency, stocks, or other speculative financial instruments.
|
||||||
|
- NEVER provide specific investment advice without appropriate risk disclosures.
|
||||||
|
- NEVER provide legal, medical, or psychological advice.
|
||||||
|
- ALWAYS promote responsible and low-risk financial planning strategies.
|
||||||
|
- Emphasize skills enhancement, networking, and education as primary pathways to financial success.
|
||||||
|
|
||||||
Respond ONLY in the requested JSON format.`
|
Respond ONLY in the requested JSON format.`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -380,7 +389,7 @@ Respond ONLY in the requested JSON format.`
|
|||||||
Here is the user's current situation:
|
Here is the user's current situation:
|
||||||
${summaryText}
|
${summaryText}
|
||||||
|
|
||||||
Please provide exactly 3 short-term (within 6 months) and 2 long-term (1–3 years) milestones. Avoid any previously suggested milestones.
|
Please provide exactly 2 short-term (within 6 months) and 1 long-term (1–3 years) milestones. Avoid any previously suggested milestones.
|
||||||
Each milestone must have:
|
Each milestone must have:
|
||||||
- "title" (up to 5 words)
|
- "title" (up to 5 words)
|
||||||
- "date" in YYYY-MM-DD format (>= ${isoToday})
|
- "date" in YYYY-MM-DD format (>= ${isoToday})
|
||||||
@ -429,11 +438,9 @@ function buildUserSummary({
|
|||||||
userProfile = {},
|
userProfile = {},
|
||||||
scenarioRow = {},
|
scenarioRow = {},
|
||||||
financialProfile = {},
|
financialProfile = {},
|
||||||
collegeProfile = {}
|
collegeProfile = {},
|
||||||
|
aiRisk = null
|
||||||
}) {
|
}) {
|
||||||
// Provide a short multiline string about the user's finances, goals, etc.
|
|
||||||
// but avoid referencing scenarioRow.start_date
|
|
||||||
// e.g.:
|
|
||||||
const location = `${userProfile.state || 'Unknown State'}, ${userProfile.area || 'N/A'}`;
|
const location = `${userProfile.state || 'Unknown State'}, ${userProfile.area || 'N/A'}`;
|
||||||
const careerName = scenarioRow.career_name || 'Unknown';
|
const careerName = scenarioRow.career_name || 'Unknown';
|
||||||
const careerGoals = scenarioRow.career_goals || 'No goals specified';
|
const careerGoals = scenarioRow.career_goals || 'No goals specified';
|
||||||
@ -446,7 +453,13 @@ function buildUserSummary({
|
|||||||
const retirementSavings = financialProfile.retirement_savings || 0;
|
const retirementSavings = financialProfile.retirement_savings || 0;
|
||||||
const emergencyFund = financialProfile.emergency_fund || 0;
|
const emergencyFund = financialProfile.emergency_fund || 0;
|
||||||
|
|
||||||
// And similarly for collegeProfile if needed, ignoring start_date
|
let riskText = '';
|
||||||
|
if (aiRisk?.riskLevel) {
|
||||||
|
riskText = `
|
||||||
|
AI Automation Risk: ${aiRisk.riskLevel}
|
||||||
|
Reasoning: ${aiRisk.reasoning}`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
User Location: ${location}
|
User Location: ${location}
|
||||||
Career Name: ${careerName}
|
Career Name: ${careerName}
|
||||||
@ -460,9 +473,317 @@ Financial:
|
|||||||
- Monthly Debt: \$${monthlyDebt}
|
- Monthly Debt: \$${monthlyDebt}
|
||||||
- Retirement Savings: \$${retirementSavings}
|
- Retirement Savings: \$${retirementSavings}
|
||||||
- Emergency Fund: \$${emergencyFund}
|
- Emergency Fund: \$${emergencyFund}
|
||||||
|
|
||||||
|
${riskText}
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Example: ai/chat with correct milestone-saving logic
|
||||||
|
app.post('/api/premium/ai/chat', authenticatePremiumUser, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
userProfile = {},
|
||||||
|
scenarioRow = {},
|
||||||
|
financialProfile = {},
|
||||||
|
collegeProfile = {},
|
||||||
|
chatHistory = []
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 1. Helper Functions
|
||||||
|
// ------------------------------------------------
|
||||||
|
|
||||||
|
// A. Build a "where you are now" vs. "where you want to go" message
|
||||||
|
function buildStatusSituationMessage(status, situation, careerName) {
|
||||||
|
const sStatus = (status || "").toLowerCase(); // e.g. "planned", "current", "exploring"
|
||||||
|
const sSituation = (situation || "").toLowerCase(); // e.g. "planning", "preparing", "enhancing", "retirement"
|
||||||
|
|
||||||
|
// "Where you are now"
|
||||||
|
let nowPart = "";
|
||||||
|
switch (sStatus) {
|
||||||
|
case "planned":
|
||||||
|
nowPart = `It appears you're looking ahead to a possible future in ${careerName}.`;
|
||||||
|
break;
|
||||||
|
case "current":
|
||||||
|
nowPart = `It appears you're already working in the ${careerName} field.`;
|
||||||
|
break;
|
||||||
|
case "exploring":
|
||||||
|
nowPart = `It appears you're exploring how ${careerName} might fit your future plans.`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nowPart = `I don’t have a clear picture of where you stand currently with ${careerName}.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Where you’d like to go next"
|
||||||
|
let nextPart = "";
|
||||||
|
switch (sSituation) {
|
||||||
|
case "planning":
|
||||||
|
nextPart = `You're aiming to clarify your strategy for moving into this field.`;
|
||||||
|
break;
|
||||||
|
case "preparing":
|
||||||
|
nextPart = `You're actively developing the skills you need to step into ${careerName}.`;
|
||||||
|
break;
|
||||||
|
case "enhancing":
|
||||||
|
nextPart = `You’d like to deepen or expand your responsibilities within ${careerName}.`;
|
||||||
|
break;
|
||||||
|
case "retirement":
|
||||||
|
nextPart = `You're considering how to transition toward retirement in this role.`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nextPart = `I'm not entirely sure of your next direction.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedDescription = `${nowPart} ${nextPart}`.trim();
|
||||||
|
|
||||||
|
// Add a friendly note about how there's no "wrong" answer
|
||||||
|
const friendlyNote = `
|
||||||
|
No worries if these selections feel a bit overlapping or if you're just exploring.
|
||||||
|
One portion highlights where you currently see yourself, and the other points to where you'd like to go.
|
||||||
|
Feel free to refine these whenever you want, or just continue as is.
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
return `${combinedDescription}\n\n${friendlyNote}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. Build a user summary that references all available info
|
||||||
|
function buildUserSummary({ userProfile, scenarioRow, financialProfile, collegeProfile, aiRisk }) {
|
||||||
|
// For illustration; adjust to your actual data fields.
|
||||||
|
const userName = userProfile.first_name || "N/A";
|
||||||
|
const location = userProfile.location || "N/A";
|
||||||
|
const userGoals = userProfile.goals || []; // maybe an array
|
||||||
|
|
||||||
|
const careerName = scenarioRow?.career_name || "this career";
|
||||||
|
const socCode = scenarioRow?.soc_code || "N/A";
|
||||||
|
const jobDescription = scenarioRow?.job_description || "No description";
|
||||||
|
const tasksList = scenarioRow?.tasks?.length
|
||||||
|
? scenarioRow.tasks.join(", ")
|
||||||
|
: "No tasks info";
|
||||||
|
|
||||||
|
const income = financialProfile?.income ? `$${financialProfile.income}` : "N/A";
|
||||||
|
const debt = financialProfile?.debt ? `$${financialProfile.debt}` : "N/A";
|
||||||
|
const savings = financialProfile?.savings ? `$${financialProfile.savings}` : "N/A";
|
||||||
|
|
||||||
|
const major = collegeProfile?.major || "N/A";
|
||||||
|
const creditsCompleted = collegeProfile?.credits_completed || 0;
|
||||||
|
const graduationDate = collegeProfile?.expected_graduation || "Unknown";
|
||||||
|
|
||||||
|
const aiRiskReport = aiRisk?.report || "No AI risk info provided.";
|
||||||
|
|
||||||
|
return `
|
||||||
|
[USER PROFILE]
|
||||||
|
- Name: ${userName}
|
||||||
|
- Location: ${location}
|
||||||
|
- Goals: ${userGoals.length ? userGoals.join(", ") : "Not specified"}
|
||||||
|
|
||||||
|
[TARGET CAREER]
|
||||||
|
- Career Name: ${careerName}
|
||||||
|
- SOC Code: ${socCode}
|
||||||
|
- Job Description: ${jobDescription}
|
||||||
|
- Typical Tasks: ${tasksList}
|
||||||
|
|
||||||
|
[FINANCIAL PROFILE]
|
||||||
|
- Income: ${income}
|
||||||
|
- Debt: ${debt}
|
||||||
|
- Savings: ${savings}
|
||||||
|
|
||||||
|
[COLLEGE / EDUCATION]
|
||||||
|
- Major: ${major}
|
||||||
|
- Credits Completed: ${creditsCompleted}
|
||||||
|
- Expected Graduation Date: ${graduationDate}
|
||||||
|
|
||||||
|
[AI RISK ANALYSIS]
|
||||||
|
${aiRiskReport}
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example environment config
|
||||||
|
const MILESTONE_API_URL = process.env.APTIVA_INTERNAL_API
|
||||||
|
? `${process.env.APTIVA_INTERNAL_API}/premium/milestone`
|
||||||
|
: "http://localhost:5002/api/premium/milestone";
|
||||||
|
|
||||||
|
const IMPACT_API_URL = process.env.APTIVA_INTERNAL_API
|
||||||
|
? `${process.env.APTIVA_INTERNAL_API}/premium/milestone-impacts`
|
||||||
|
: "http://localhost:5002/api/premium/milestone-impacts";
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 2. AI Risk Fetch
|
||||||
|
// ------------------------------------------------
|
||||||
|
const apiBase = process.env.APTIVA_INTERNAL_API || "http://localhost:5002/api";
|
||||||
|
let aiRisk = null;
|
||||||
|
try {
|
||||||
|
const aiRiskRes = await fetch(`${apiBase}/premium/ai-risk-analysis`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
socCode: scenarioRow?.soc_code,
|
||||||
|
careerName: scenarioRow?.career_name,
|
||||||
|
jobDescription: scenarioRow?.job_description,
|
||||||
|
tasks: scenarioRow?.tasks || []
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (aiRiskRes.ok) {
|
||||||
|
aiRisk = await aiRiskRes.json();
|
||||||
|
} else {
|
||||||
|
console.warn("AI risk fetch failed with status:", aiRiskRes.status);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching AI risk analysis:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 3. Build Status + Situation text
|
||||||
|
// ------------------------------------------------
|
||||||
|
const { status: userStatus } = scenarioRow;
|
||||||
|
const { career_situation: userSituation } = userProfile;
|
||||||
|
const careerName = scenarioRow?.career_name || "this career";
|
||||||
|
|
||||||
|
const combinedStatusSituation = buildStatusSituationMessage(
|
||||||
|
userStatus,
|
||||||
|
userSituation,
|
||||||
|
careerName
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 4. Build Additional Context Summary
|
||||||
|
// ------------------------------------------------
|
||||||
|
const summaryText = buildUserSummary({
|
||||||
|
userProfile,
|
||||||
|
scenarioRow,
|
||||||
|
financialProfile,
|
||||||
|
collegeProfile,
|
||||||
|
aiRisk
|
||||||
|
});
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 5. Construct System-Level Prompts
|
||||||
|
// ------------------------------------------------
|
||||||
|
const systemPromptIntro = `
|
||||||
|
You are Jess, a professional career coach working inside AptivaAI.
|
||||||
|
|
||||||
|
The user has already provided detailed information about their situation, career goals, finances, education, and more.
|
||||||
|
Your job is to leverage *all* this context to provide specific, empathetic, and helpful advice.
|
||||||
|
|
||||||
|
Do not re-ask for the details below unless you need clarifications.
|
||||||
|
Reflect and use the user's actual data. Avoid purely generic responses.
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const systemPromptStatusSituation = `
|
||||||
|
[CURRENT AND NEXT STEP OVERVIEW]
|
||||||
|
${combinedStatusSituation}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const systemPromptDetailedContext = `
|
||||||
|
[DETAILED USER PROFILE & CONTEXT]
|
||||||
|
${summaryText}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// Build up the final messages array
|
||||||
|
const messagesToSend = [
|
||||||
|
{ role: "system", content: systemPromptIntro },
|
||||||
|
{ role: "system", content: systemPromptStatusSituation },
|
||||||
|
{ role: "system", content: systemPromptDetailedContext },
|
||||||
|
...chatHistory // includes user and assistant messages so far
|
||||||
|
];
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 6. Call GPT
|
||||||
|
// ------------------------------------------------
|
||||||
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: "gpt-4",
|
||||||
|
messages: messagesToSend,
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 600
|
||||||
|
});
|
||||||
|
|
||||||
|
let coachResponse = completion?.choices?.[0]?.message?.content?.trim();
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 7. Detect and Possibly Save Milestones
|
||||||
|
// ------------------------------------------------
|
||||||
|
let milestones = [];
|
||||||
|
let isMilestoneFormat = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
milestones = JSON.parse(coachResponse);
|
||||||
|
isMilestoneFormat = Array.isArray(milestones);
|
||||||
|
} catch (e) {
|
||||||
|
isMilestoneFormat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMilestoneFormat && milestones.length) {
|
||||||
|
// 7a. Prepare data for milestone creation
|
||||||
|
const rawForMilestonesEndpoint = milestones.map(m => {
|
||||||
|
return {
|
||||||
|
title: m.title,
|
||||||
|
description: m.description,
|
||||||
|
date: m.date,
|
||||||
|
career_profile_id: scenarioRow?.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7b. Bulk-create milestones
|
||||||
|
const mileRes = await fetch(MILESTONE_API_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ milestones: rawForMilestonesEndpoint })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mileRes.ok) {
|
||||||
|
console.error("Failed to save milestones =>", mileRes.status);
|
||||||
|
coachResponse = "I prepared milestones, but couldn't save them right now. Please try again later.";
|
||||||
|
} else {
|
||||||
|
// 7c. newly created milestones with IDs
|
||||||
|
const createdMils = await mileRes.json();
|
||||||
|
|
||||||
|
// 7d. For each milestone, if it has "impacts", create them
|
||||||
|
for (let i = 0; i < milestones.length; i++) {
|
||||||
|
const originalMilestone = milestones[i];
|
||||||
|
const newMilestone = createdMils[i];
|
||||||
|
|
||||||
|
if (Array.isArray(originalMilestone.impacts) && originalMilestone.impacts.length) {
|
||||||
|
for (const imp of originalMilestone.impacts) {
|
||||||
|
const impactPayload = {
|
||||||
|
milestone_id: newMilestone.id,
|
||||||
|
impact_type: imp.impact_type,
|
||||||
|
direction: imp.direction || 'subtract',
|
||||||
|
amount: imp.amount || 0,
|
||||||
|
start_date: imp.start_date || null,
|
||||||
|
end_date: imp.end_date || null
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const impactRes = await fetch(IMPACT_API_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(impactPayload)
|
||||||
|
});
|
||||||
|
if (!impactRes.ok) {
|
||||||
|
console.error(`Failed to create impact for milestone ${newMilestone.id}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error creating impact for milestone ${newMilestone.id}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coachResponse = "I've created some actionable milestones for you (with financial impacts). You can view them in your roadmap!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------
|
||||||
|
// 8. Send JSON Response
|
||||||
|
// ------------------------------------------------
|
||||||
|
res.json({ reply: coachResponse });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in /api/premium/ai/chat =>", err);
|
||||||
|
res.status(500).json({ error: "Failed to generate conversational response." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/***************************************************
|
/***************************************************
|
||||||
AI MILESTONE CONVERSION ENDPOINT
|
AI MILESTONE CONVERSION ENDPOINT
|
||||||
****************************************************/
|
****************************************************/
|
||||||
|
@ -257,7 +257,7 @@ function App() {
|
|||||||
to="/career-roadmap"
|
to="/career-roadmap"
|
||||||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||||||
>
|
>
|
||||||
Career Roadmap
|
Roadmap & AI Career Coach
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/resume-optimizer"
|
to="/resume-optimizer"
|
||||||
|
275
src/components/CareerCoach.js
Normal file
275
src/components/CareerCoach.js
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import authFetch from "../utils/authFetch.js";
|
||||||
|
|
||||||
|
export default function CareerCoach({
|
||||||
|
userProfile,
|
||||||
|
financialProfile,
|
||||||
|
scenarioRow,
|
||||||
|
collegeProfile,
|
||||||
|
onMilestonesCreated,
|
||||||
|
}) {
|
||||||
|
const [messages, setMessages] = useState([]);
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const chatContainerRef = useRef(null);
|
||||||
|
const [hasSentMessage, setHasSentMessage] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatContainerRef.current) {
|
||||||
|
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [messages, hasSentMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const introMessage = generatePersonalizedIntro();
|
||||||
|
setMessages([introMessage]);
|
||||||
|
}, [scenarioRow, userProfile]);
|
||||||
|
|
||||||
|
function buildStatusSituationMessage(status, situation, careerName) {
|
||||||
|
const sStatus = (status || "").toLowerCase();
|
||||||
|
// e.g. "planned", "current", "exploring", etc.
|
||||||
|
|
||||||
|
const sSituation = (situation || "").toLowerCase();
|
||||||
|
// e.g. "planning", "preparing", "enhancing", "retirement"
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// "Where you are right now"
|
||||||
|
// -----------------------------
|
||||||
|
let nowPart = "";
|
||||||
|
switch (sStatus) {
|
||||||
|
case "planned":
|
||||||
|
nowPart = `It sounds like you're looking ahead to a possible future in ${careerName}.`;
|
||||||
|
break;
|
||||||
|
case "current":
|
||||||
|
nowPart = `It sounds like you're already in the ${careerName} field.`;
|
||||||
|
break;
|
||||||
|
case "exploring":
|
||||||
|
nowPart = `It sounds like you're still exploring how ${careerName} might fit your plans.`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nowPart = `I don’t have much info on your current involvement with ${careerName}.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// "Where you’d like to go next"
|
||||||
|
// -----------------------------
|
||||||
|
let nextPart = "";
|
||||||
|
switch (sSituation) {
|
||||||
|
case "planning":
|
||||||
|
nextPart = `You're aiming to figure out your strategy for moving into this field.`;
|
||||||
|
break;
|
||||||
|
case "preparing":
|
||||||
|
nextPart = `You're actively developing the skills you need to step into ${careerName}.`;
|
||||||
|
break;
|
||||||
|
case "enhancing":
|
||||||
|
nextPart = `You’d like to deepen or broaden your responsibilities within ${careerName}.`;
|
||||||
|
break;
|
||||||
|
case "retirement":
|
||||||
|
nextPart = `You're contemplating how to transition toward retirement in this field.`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nextPart = `I'm not entirely sure of your next direction.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Combine the descriptive text
|
||||||
|
// -----------------------------
|
||||||
|
const combinedDescription = `${nowPart} ${nextPart}`.trim();
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Optional “friendly note” if they seem to span different phases
|
||||||
|
// but we do *not* treat it as a mismatch or error.
|
||||||
|
// -----------------------------
|
||||||
|
let friendlyNote = `
|
||||||
|
Feel free to use AptivaAI however it best suits you—there’s no "wrong" answer.
|
||||||
|
One part highlights your current situation, the other indicates what you're aiming for next.
|
||||||
|
Some folks aren't working and want premium-level guidance, and that's where we shine.
|
||||||
|
We can refine details anytime or just jump straight to what you're most interested in exploring now!
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
// You could conditionally show the friendlyNote only if you detect certain combos,
|
||||||
|
// OR you can show it unconditionally to make the user comfortable.
|
||||||
|
|
||||||
|
// For maximum inclusivity, we can always show it.
|
||||||
|
|
||||||
|
return `${combinedDescription}\n\n${friendlyNote}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatePersonalizedIntro = () => {
|
||||||
|
const careerName = scenarioRow?.career_name || null;
|
||||||
|
const goalsText = scenarioRow?.career_goals?.trim() || null;
|
||||||
|
const riskLevel = scenarioRow?.riskLevel;
|
||||||
|
const riskReasoning = scenarioRow?.riskReasoning;
|
||||||
|
|
||||||
|
const userSituation = userProfile?.career_situation?.toLowerCase();
|
||||||
|
const userStatus = scenarioRow?.status?.toLowerCase();
|
||||||
|
|
||||||
|
const combinedMessage = buildStatusSituationMessage(userStatus, userSituation, careerName);
|
||||||
|
|
||||||
|
const interestInventoryMessage = userProfile?.riasec
|
||||||
|
? `With your Interest Inventory profile (${userProfile.riasec}), I can tailor suggestions more precisely.`
|
||||||
|
: `If you complete the Interest Inventory, I’ll be able to offer more targeted suggestions based on your interests.`;
|
||||||
|
|
||||||
|
const riskMessage =
|
||||||
|
riskLevel && riskReasoning
|
||||||
|
? `Note: This role has a <strong>${riskLevel}</strong> automation risk over the next 10 years. ${riskReasoning}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const goalsMessage = goalsText
|
||||||
|
? `Your goals include:<br />${goalsText
|
||||||
|
.split(/\d+\.\s?/)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((goal) => `• ${goal.trim()}`)
|
||||||
|
.join("<br />")}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const missingProfileFields = [];
|
||||||
|
if (!careerName) missingProfileFields.push("career choice");
|
||||||
|
if (!goalsText) missingProfileFields.push("career goals");
|
||||||
|
if (!userSituation) missingProfileFields.push("career phase");
|
||||||
|
|
||||||
|
let advisoryMessage = "";
|
||||||
|
if (missingProfileFields.length > 0) {
|
||||||
|
advisoryMessage = `<br /><br /><em>If you provide ${
|
||||||
|
missingProfileFields.length > 1
|
||||||
|
? "a few more details"
|
||||||
|
: "this information"
|
||||||
|
}, I’ll be able to offer more tailored and precise advice.</em>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: "assistant",
|
||||||
|
content: `
|
||||||
|
Hi! ${combinedMessage}<br /><br />
|
||||||
|
${goalsMessage ? goalsMessage + "<br /><br />" : ""}
|
||||||
|
${interestInventoryMessage}<br /><br />
|
||||||
|
${riskMessage}<br />
|
||||||
|
${advisoryMessage}<br />
|
||||||
|
I'm here to support you with personalized coaching. What would you like to focus on today?
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleSendMessage = async () => {
|
||||||
|
if (!input.trim() || loading) return;
|
||||||
|
|
||||||
|
const userMessage = { role: "user", content: input.trim() };
|
||||||
|
const updatedMessages = [...messages, userMessage];
|
||||||
|
|
||||||
|
setMessages(updatedMessages);
|
||||||
|
setInput("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
userProfile,
|
||||||
|
financialProfile,
|
||||||
|
scenarioRow,
|
||||||
|
collegeProfile,
|
||||||
|
chatHistory: updatedMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await authFetch("/api/premium/ai/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("AI request failed");
|
||||||
|
|
||||||
|
const { reply } = await res.json();
|
||||||
|
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ role: "assistant", content: reply },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
reply.includes("created those milestones") ||
|
||||||
|
reply.includes("milestones for you")
|
||||||
|
) {
|
||||||
|
onMilestonesCreated?.();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("CareerCoach error:", err);
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: "Sorry, something went wrong. Please try again.",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
setHasSentMessage(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border rounded-lg shadow bg-white p-6 mb-6">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4">Career Coach</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={chatContainerRef}
|
||||||
|
className="overflow-y-auto border rounded mb-4 space-y-2"
|
||||||
|
style={{
|
||||||
|
maxHeight: "320px",
|
||||||
|
minHeight: "200px",
|
||||||
|
padding: "1rem",
|
||||||
|
scrollBehavior: "smooth",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.map((msg, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`rounded p-2 ${
|
||||||
|
msg.role === "user"
|
||||||
|
? "bg-blue-100 text-blue-800 self-end"
|
||||||
|
: "bg-gray-200 text-gray-800 self-start"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: msg.content.replace(/\n/g, "<br />"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{loading && (
|
||||||
|
<div className="text-sm text-gray-500 italic">Coach is typing...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="flex-grow border rounded py-2 px-3"
|
||||||
|
value={input}
|
||||||
|
placeholder="Ask your Career Coach..."
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`rounded px-4 py-2 ${
|
||||||
|
loading
|
||||||
|
? "bg-gray-300 text-gray-600 cursor-not-allowed"
|
||||||
|
: "bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
|
}`}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -18,9 +18,10 @@ import authFetch from '../utils/authFetch.js';
|
|||||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||||
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
|
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
|
||||||
import { getFullStateName } from '../utils/stateUtils.js';
|
import { getFullStateName } from '../utils/stateUtils.js';
|
||||||
|
import CareerCoach from "./CareerCoach.js";
|
||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import ScenarioEditModal from './ScenarioEditModal.js';
|
import ScenarioEditModal from './ScenarioEditModal.js';
|
||||||
|
import parseAIJson from "../utils/parseAIJson.js"; // your shared parser
|
||||||
|
|
||||||
import './CareerRoadmap.css';
|
import './CareerRoadmap.css';
|
||||||
import './MilestoneTimeline.css';
|
import './MilestoneTimeline.css';
|
||||||
@ -233,43 +234,15 @@ function getYearsInCareer(startDateString) {
|
|||||||
return Math.floor(diffYears).toString();
|
return Math.floor(diffYears).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* parseAiJson
|
|
||||||
* If ChatGPT returns a fenced code block like:
|
|
||||||
* ```json
|
|
||||||
* [ { ... }, ... ]
|
|
||||||
* ```
|
|
||||||
* we extract that JSON. Otherwise, we parse the raw string.
|
|
||||||
*/
|
|
||||||
function parseAiJson(rawText) {
|
|
||||||
const fencedRegex = /```json\s*([\s\S]*?)\s*```/i;
|
|
||||||
const match = rawText.match(fencedRegex);
|
|
||||||
if (match) {
|
|
||||||
const jsonStr = match[1].trim();
|
|
||||||
const arr = JSON.parse(jsonStr);
|
|
||||||
// Add an "id" for each milestone
|
|
||||||
arr.forEach((m) => {
|
|
||||||
m.id = crypto.randomUUID();
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
} else {
|
|
||||||
// fallback if no fences
|
|
||||||
const arr = JSON.parse(rawText);
|
|
||||||
arr.forEach((m) => {
|
|
||||||
m.id = crypto.randomUUID();
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const apiURL = process.env.REACT_APP_API_URL;
|
const apiURL = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'MONTE_CARLO'
|
const [interestStrategy, setInterestStrategy] = useState('FLAT'); // 'NONE' | 'FLAT' | 'MONTE_CARLO'
|
||||||
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06); // 6% default
|
const [flatAnnualRate, setFlatAnnualRate] = useState(0.06);
|
||||||
const [randomRangeMin, setRandomRangeMin] = useState(-0.02); // -3% monthly
|
const [randomRangeMin, setRandomRangeMin] = useState(-0.02);
|
||||||
const [randomRangeMax, setRandomRangeMax] = useState(0.02); // 8% monthly
|
const [randomRangeMax, setRandomRangeMax] = useState(0.02);
|
||||||
|
|
||||||
// Basic states
|
// Basic states
|
||||||
const [userProfile, setUserProfile] = useState(null);
|
const [userProfile, setUserProfile] = useState(null);
|
||||||
@ -818,7 +791,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const rawText = data.recommendations || '';
|
const rawText = data.recommendations || '';
|
||||||
const arr = parseAiJson(rawText);
|
const arr = parseAIJson(rawText);
|
||||||
|
|
||||||
setRecommendations(arr);
|
setRecommendations(arr);
|
||||||
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
|
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
|
||||||
@ -838,53 +811,6 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleToggle(recId) {
|
|
||||||
setSelectedIds((prev) => {
|
|
||||||
if (prev.includes(recId)) {
|
|
||||||
return prev.filter((x) => x !== recId);
|
|
||||||
} else {
|
|
||||||
return [...prev, recId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreateSelectedMilestones() {
|
|
||||||
if (!careerProfileId) return;
|
|
||||||
const confirm = window.confirm('Create the selected AI suggestions as milestones?');
|
|
||||||
if (!confirm) return;
|
|
||||||
|
|
||||||
const selectedRecs = recommendations.filter((r) => selectedIds.includes(r.id));
|
|
||||||
if (!selectedRecs.length) return;
|
|
||||||
|
|
||||||
// Use the AI-suggested date:
|
|
||||||
const payload = selectedRecs.map((rec) => ({
|
|
||||||
title: rec.title,
|
|
||||||
description: rec.description || '',
|
|
||||||
date: rec.date, // <-- use AI's date, not today's date
|
|
||||||
career_profile_id: careerProfileId
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await authFetch('/api/premium/milestone', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ milestones: payload })
|
|
||||||
});
|
|
||||||
if (!r.ok) throw new Error('Failed to create new milestones');
|
|
||||||
|
|
||||||
// re-run projection to see them in the chart
|
|
||||||
await buildProjection();
|
|
||||||
|
|
||||||
// optionally clear
|
|
||||||
alert('Milestones created successfully!');
|
|
||||||
setSelectedIds([]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error creating milestones =>', err);
|
|
||||||
alert('Error saving new AI milestones.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSimulationYearsChange(e) {
|
function handleSimulationYearsChange(e) {
|
||||||
setSimulationYearsInput(e.target.value);
|
setSimulationYearsInput(e.target.value);
|
||||||
}
|
}
|
||||||
@ -895,13 +821,26 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
|
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-4">
|
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-4">
|
||||||
<h2 className="text-2xl font-bold mb-4">Where Am I Now?</h2>
|
|
||||||
|
{/* 0) New CareerCoach at the top */}
|
||||||
|
<CareerCoach
|
||||||
|
userProfile={userProfile}
|
||||||
|
financialProfile={financialProfile}
|
||||||
|
scenarioRow={scenarioRow}
|
||||||
|
collegeProfile={collegeProfile}
|
||||||
|
onMilestonesCreated={() => {
|
||||||
|
/* refresh or reload logic here */
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 1) Then your "Where Am I Now?" */}
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Where you are now and where you are going.</h2>
|
||||||
|
|
||||||
{/* 1) Career */}
|
{/* 1) Career */}
|
||||||
<div className="bg-white p-4 rounded shadow mb-4 flex flex-col justify-center items-center min-h-[80px]">
|
<div className="bg-white p-4 rounded shadow mb-4 flex flex-col justify-center items-center min-h-[80px]">
|
||||||
<p>
|
<p>
|
||||||
<strong>Current Career:</strong>{' '}
|
<strong>Target Career:</strong>{' '}
|
||||||
{scenarioRow?.career_name || '(Select a career)'}
|
{scenarioRow?.career_name || '(Select a career)'}
|
||||||
</p>
|
</p>
|
||||||
{yearsInCareer && (
|
{yearsInCareer && (
|
||||||
@ -915,7 +854,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
{/* 2) Salary Benchmarks */}
|
{/* 2) Salary Benchmarks */}
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
{salaryData?.regional && (
|
{salaryData?.regional && (
|
||||||
<div className="bg-white p-4 rounded shadow w-full md:w-1/2">
|
<div className="bg-white p-4 rounded shadow w-full h-auto overflow-none">
|
||||||
<h4 className="font-medium mb-2">
|
<h4 className="font-medium mb-2">
|
||||||
Regional Salary Data ({userArea || 'U.S.'})
|
Regional Salary Data ({userArea || 'U.S.'})
|
||||||
</h4>
|
</h4>
|
||||||
@ -948,7 +887,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{salaryData?.national && (
|
{salaryData?.national && (
|
||||||
<div className="bg-white p-4 rounded shadow w-full md:w-1/2">
|
<div className="bg-white p-4 rounded shadow w-full h-auto overflow-none">
|
||||||
<h4 className="font-medium mb-2">National Salary Data</h4>
|
<h4 className="font-medium mb-2">National Salary Data</h4>
|
||||||
<p>
|
<p>
|
||||||
10th percentile:{' '}
|
10th percentile:{' '}
|
||||||
@ -979,7 +918,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3) Economic Projections */}
|
{/* 3) Economic Projections */}
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4 h-auto overflow-none">
|
||||||
{economicProjections?.state && (
|
{economicProjections?.state && (
|
||||||
<EconomicProjectionsBar data={economicProjections.state} />
|
<EconomicProjectionsBar data={economicProjections.state} />
|
||||||
)}
|
)}
|
||||||
@ -993,13 +932,13 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 4) Career Goals */}
|
{/* 4) Career Goals
|
||||||
<div className="bg-white p-4 rounded shadow">
|
<div className="bg-white p-4 rounded shadow">
|
||||||
<h3 className="text-lg font-semibold mb-2">Your Career Goals</h3>
|
<h3 className="text-lg font-semibold mb-2">Your Career Goals</h3>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
{scenarioRow?.career_goals || 'No career goals entered yet.'}
|
{scenarioRow?.career_goals || 'No career goals entered yet.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>*/}
|
||||||
|
|
||||||
{/* 5) Financial Projection */}
|
{/* 5) Financial Projection */}
|
||||||
<div className="bg-white p-4 rounded shadow">
|
<div className="bg-white p-4 rounded shadow">
|
||||||
@ -1117,42 +1056,42 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 7) AI Next Steps */}
|
{/* 7) AI Next Steps */}
|
||||||
<div className="bg-white p-4 rounded shadow mt-4">
|
{/* <div className="bg-white p-4 rounded shadow mt-4">
|
||||||
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
||||||
{buttonLabel}
|
{buttonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{aiLoading && <p>Generating your next steps…</p>}
|
{aiLoading && <p>Generating your next steps…</p>}
|
||||||
|
|
||||||
{/* If we have structured recs, show checkboxes */}
|
{/* If we have structured recs, show checkboxes
|
||||||
{recommendations.length > 0 && (
|
{recommendations.length > 0 && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<h3 className="font-semibold">Select the Advice You Want to Keep</h3>
|
<h3 className="font-semibold">Select the Advice You Want to Keep</h3>
|
||||||
<ul className="mt-2 space-y-2">
|
<ul className="mt-2 space-y-2">
|
||||||
{recommendations.map((m) => (
|
{recommendations.map((m) => (
|
||||||
<li key={m.id} className="flex items-start gap-2">
|
<li key={m.id} className="flex items-start gap-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedIds.includes(m.id)}
|
checked={selectedIds.includes(m.id)}
|
||||||
onChange={() => handleToggle(m.id)}
|
onChange={() => handleToggle(m.id)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col text-left">
|
<div className="flex flex-col text-left">
|
||||||
<strong>{m.title}</strong>
|
<strong>{m.title}</strong>
|
||||||
<span>{m.date}</span>
|
<span>{m.date}</span>
|
||||||
<p className="text-sm">{m.description}</p>
|
<p className="text-sm">{m.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{selectedIds.length > 0 && (
|
{selectedIds.length > 0 && (
|
||||||
<Button className="mt-3" onClick={handleCreateSelectedMilestones}>
|
<Button className="mt-3" onClick={handleCreateSelectedMilestones}>
|
||||||
Create Milestones from Selected
|
Create Milestones from Selected
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -69,8 +69,11 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block font-medium">
|
<label className="block font-medium">
|
||||||
Are you currently working or earning any income (even part-time)?
|
Are you currently earning any income — even part-time or outside your intended career path?
|
||||||
</label>
|
</label>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
(We ask this to understand your financial picture. This won’t affect how we track your progress toward your target career.)
|
||||||
|
</p>
|
||||||
<select
|
<select
|
||||||
value={currentlyWorking}
|
value={currentlyWorking}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -87,7 +90,13 @@ const CareerOnboarding = ({ nextStep, prevStep, data, setData }) => {
|
|||||||
|
|
||||||
{/* 2) Replace old local “Search for Career” with <CareerSearch/> */}
|
{/* 2) Replace old local “Search for Career” with <CareerSearch/> */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="font-medium">Search for Career</h3>
|
<h3 className="font-medium">
|
||||||
|
What career are you planning to pursue?
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
This should be your <strong>target career path</strong> — whether it’s a new goal or the one you're already in.
|
||||||
|
</p>
|
||||||
|
|
||||||
<CareerSearch onCareerSelected={handleCareerSelected} />
|
<CareerSearch onCareerSelected={handleCareerSelected} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
29
src/utils/parseAIJson.js
Normal file
29
src/utils/parseAIJson.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* parseAiJson
|
||||||
|
* Attempts to extract a JSON array or object from a string that may include
|
||||||
|
* extra text or a fenced code block (```json ... ```).
|
||||||
|
*
|
||||||
|
* @param {string} rawText - The raw string from the AI response.
|
||||||
|
* @returns {any} - The parsed JSON (object or array).
|
||||||
|
* @throws Will throw an error if parsing fails.
|
||||||
|
*/
|
||||||
|
function parseAIJson(rawText) {
|
||||||
|
if (!rawText || typeof rawText !== 'string') {
|
||||||
|
throw new Error('No valid text provided for parseAiJson.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Look for a fenced code block with "```json" ... "```"
|
||||||
|
const fencedRegex = /```json\s*([\s\S]*?)\s*```/i;
|
||||||
|
const match = rawText.match(fencedRegex);
|
||||||
|
if (match && match[1]) {
|
||||||
|
// parse the fenced code block
|
||||||
|
const jsonStr = match[1].trim();
|
||||||
|
return JSON.parse(jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: try parsing the entire string directly
|
||||||
|
// Sometimes the AI might return just a raw JSON array without fences
|
||||||
|
return JSON.parse(rawText);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parseAIJson;
|
Loading…
Reference in New Issue
Block a user