CareerRoadmap Supportbot context add.
This commit is contained in:
parent
ec0ce1fce8
commit
0635b60792
@ -220,6 +220,45 @@ export default function chatFreeEndpoint(
|
||||
system += INTEREST_PLAYBOOK + CAREER_EXPLORER_FEATURES;
|
||||
}
|
||||
|
||||
/* ---- EducationalProgramsPage ------------------------------------ */
|
||||
if (pageContext === "EducationalProgramsPage") {
|
||||
/* snapshot already comes over the wire from the front-end */
|
||||
const {
|
||||
careerCtx = {}, // { socCode, careerTitle, cipCodes[] }
|
||||
ksaCtx = {}, // { total, topKnow[], topSkill[] }
|
||||
filterCtx = {}, // { sortBy, maxTuition, maxDistance, inStateOnly }
|
||||
schoolCtx = {} // { count }
|
||||
} = snapshot || {};
|
||||
|
||||
system += `
|
||||
### Current context: Educational Programs
|
||||
${careerCtx.socCode
|
||||
? `Career : ${careerCtx.careerTitle} (SOC ${careerCtx.socCode})
|
||||
CIP codes : ${careerCtx.cipCodes?.join(", ") || "n/a"}`
|
||||
: "No career selected"}
|
||||
|
||||
${ksaCtx.total
|
||||
? `KSA summary : ${ksaCtx.total} items
|
||||
• Top Knowledge : ${ksaCtx.topKnow?.join(", ") || "—"}
|
||||
• Top Skills : ${ksaCtx.topSkill?.join(", ") || "—"}`
|
||||
: ""}
|
||||
|
||||
Filters in effect
|
||||
• Sort by : ${filterCtx.sortBy || "tuition"}
|
||||
• Max tuition : $${filterCtx.maxTuition ?? "—"}
|
||||
• Max distance : ${filterCtx.maxDistance ?? "—"} mi
|
||||
• In-state only : ${filterCtx.inStateOnly ? "yes" : "no"}
|
||||
|
||||
Matching schools : ${schoolCtx.count ?? 0}
|
||||
${Array.isArray(schoolCtx.sample) && schoolCtx.sample.length
|
||||
? `Sample (top ${schoolCtx.sample.length})
|
||||
${schoolCtx.sample
|
||||
.map((s,i)=>`${i+1}. ${s.name} – $${s.inState||"?"} in-state, ${s.distance!==null? s.distance+" mi":"distance n/a"}`)
|
||||
.join("\n")}`
|
||||
: ""}
|
||||
(Remember: you can’t click—just explain the steps.)`;
|
||||
}
|
||||
|
||||
/* 2) Append task catalogue so the bot can describe valid actions */
|
||||
const isPremium = req.user?.plan_type === "premium";
|
||||
system +=
|
||||
@ -251,6 +290,39 @@ export default function chatFreeEndpoint(
|
||||
`Key tasks: ${tasks.slice(0,5).join("; ")}\n`;
|
||||
}
|
||||
|
||||
/* ---- CareerRoadmap extras ---- */
|
||||
if (pageContext === "CareerRoadmap" && snapshot) {
|
||||
const { careerCtx={}, salaryCtx={}, econCtx={}, roadmapCtx={} } = snapshot;
|
||||
|
||||
system += "\n\n### Current context: Career Road-map\n";
|
||||
|
||||
if (careerCtx.title) {
|
||||
system += `Career : ${careerCtx.title} (SOC ${careerCtx.socCode||'n/a'})\n`;
|
||||
} else {
|
||||
system += "No career selected yet\n";
|
||||
}
|
||||
|
||||
if (salaryCtx.userSalary) {
|
||||
system += `Salary : $${salaryCtx.userSalary.toLocaleString()} (you)\n`;
|
||||
if (salaryCtx.regionalMedian)
|
||||
system += ` • Regional median : $${salaryCtx.regionalMedian.toLocaleString()}\n`;
|
||||
if (salaryCtx.nationalMedian)
|
||||
system += ` • National median : $${salaryCtx.nationalMedian.toLocaleString()}\n`;
|
||||
}
|
||||
|
||||
if (econCtx.stateGrowth || econCtx.nationalGrowth) {
|
||||
system += "Growth outlook\n";
|
||||
if (econCtx.stateGrowth !== null)
|
||||
system += ` • State : ${econCtx.stateGrowth}%\n`;
|
||||
if (econCtx.nationalGrowth !== null)
|
||||
system += ` • Nation : ${econCtx.nationalGrowth}%\n`;
|
||||
}
|
||||
|
||||
system += `Road-map : ${roadmapCtx.done}/${roadmapCtx.milestones} milestones complete, ` +
|
||||
`${roadmapCtx.yearsAhead} y horizon\n`;
|
||||
|
||||
system += "(Explain steps only; never click for the user.)";
|
||||
}
|
||||
/* ── Build tool list for this request ────────────────────── */
|
||||
const tools = [...SUPPORT_TOOLS]; // guidance mode → support-only;
|
||||
let messages = [
|
||||
|
@ -556,6 +556,7 @@ const uiToolHandlers = useMemo(() => {
|
||||
|
||||
<ChatDrawer
|
||||
pageContext={pageContext}
|
||||
title="Help & Support"
|
||||
snapshot={chatSnapshot}
|
||||
uiToolHandlers={uiToolHandlers}
|
||||
/>
|
||||
|
@ -42,5 +42,12 @@
|
||||
{ "id": "EP-07", "label": "Max Distance filter" },
|
||||
{ "id": "EP-08", "label": "In-State Only checkbox" },
|
||||
{ "id": "EP-09", "label": "Select School button" }
|
||||
]
|
||||
],
|
||||
"CareerRoadmap": [
|
||||
{ "id":"CR-01", "label":"Edit simulation inputs" },
|
||||
{ "id":"CR-02", "label":"Zoom / reset chart" },
|
||||
{ "id":"CR-03", "label":"Add new milestone" },
|
||||
{ "id":"CR-04", "label":"Edit existing milestone" },
|
||||
{ "id":"CR-05", "label": "Choose retirement-interest model" }
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback, useContext } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Line, Bar } from 'react-chartjs-2';
|
||||
import { format } from 'date-fns'; // ⬅ install if not already
|
||||
@ -28,6 +28,7 @@ import { simulateFinancialProjection } from '../utils/FinancialProjectionService
|
||||
import parseFloatOrZero from '../utils/ParseFloatorZero.js';
|
||||
import { getFullStateName } from '../utils/stateUtils.js';
|
||||
import CareerCoach from "./CareerCoach.js";
|
||||
import ChatCtx from '../contexts/ChatCtx.js';
|
||||
|
||||
import { Button } from './ui/button.js';
|
||||
import { Pencil } from 'lucide-react';
|
||||
@ -390,10 +391,9 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
||||
const [buttonDisabled, setButtonDisabled] = useState(false);
|
||||
const [aiRisk, setAiRisk] = useState(null);
|
||||
|
||||
const {
|
||||
projectionData: initProjData = [],
|
||||
loanPayoffMonth: initLoanMonth = null
|
||||
} = location.state || {};
|
||||
const { setChatSnapshot } = useContext(ChatCtx);
|
||||
|
||||
|
||||
|
||||
const reloadScenarioAndCollege = useCallback(async () => {
|
||||
if (!careerProfileId) return;
|
||||
@ -623,6 +623,39 @@ useEffect(() => {
|
||||
randomRangeMax
|
||||
]);
|
||||
|
||||
/**
|
||||
* Snapshot for the Support-bot: only UI state, no domain data
|
||||
*/
|
||||
const uiSnap = useMemo(() => ({
|
||||
page : 'CareerRoadmap',
|
||||
|
||||
panels: {
|
||||
careerCoachLoaded : !!scenarioRow?.career_name,
|
||||
salaryBenchmarks : !!salaryData,
|
||||
econProjections : !!economicProjections,
|
||||
financialProjection : !!projectionData.length,
|
||||
milestonesPanel : !!scenarioMilestones.length,
|
||||
editScenarioModalUp : showEditModal,
|
||||
drawerOpen : drawerOpen
|
||||
},
|
||||
|
||||
counts: {
|
||||
milestonesTotal : scenarioMilestones.length,
|
||||
milestonesDone : scenarioMilestones.filter(m => m.completed).length,
|
||||
yearsSimulated : simulationYears
|
||||
}
|
||||
}), [
|
||||
selectedCareer,
|
||||
salaryData, economicProjections,
|
||||
projectionData.length,
|
||||
scenarioMilestones, showEditModal, drawerOpen,
|
||||
simulationYears
|
||||
]);
|
||||
|
||||
/* push the snapshot to the chat context */
|
||||
useEffect(() => setChatSnapshot(uiSnap), [uiSnap, setChatSnapshot]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (recommendations.length > 0) {
|
||||
localStorage.setItem('aiRecommendations', JSON.stringify(recommendations));
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState, useContext } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
import { ONET_DEFINITIONS } from './definitions.js';
|
||||
import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js';
|
||||
import ChatCtx from '../contexts/ChatCtx.js';
|
||||
|
||||
// Helper to combine IM and LV for each KSA
|
||||
function combineIMandLV(rows) {
|
||||
@ -89,6 +90,8 @@ function EducationalProgramsPage() {
|
||||
|
||||
const [showSearch, setShowSearch] = useState(true);
|
||||
|
||||
const { setChatSnapshot } = useContext(ChatCtx);
|
||||
|
||||
// If user picks a new career from CareerSearch
|
||||
const handleCareerSelected = (foundObj) => {
|
||||
setCareerTitle(foundObj.title || '');
|
||||
@ -352,6 +355,40 @@ useEffect(() => {
|
||||
return result;
|
||||
}, [schools, inStateOnly, userState, maxTuition, maxDistance, sortBy]);
|
||||
|
||||
|
||||
const TOP_N = 8; // ← tweak here
|
||||
const topSchools = filteredAndSortedSchools.slice(0, TOP_N).map(s => ({
|
||||
name : s.INSTNM,
|
||||
inState : Number(s['In_state cost'] || 0),
|
||||
outState : Number(s['Out_state cost'] || 0),
|
||||
distance : s.distance ? Number(s.distance) : null,
|
||||
degree : s.CREDDESC,
|
||||
website : s.Website
|
||||
}));
|
||||
|
||||
const snapshot = useMemo(() => ({
|
||||
careerCtx : socCode ? { socCode, careerTitle, cipCodes } : null,
|
||||
ksaCtx : ksaForCareer.length ? {
|
||||
total : ksaForCareer.length,
|
||||
topKnow : ksaForCareer.filter(k => k.ksa_type === 'Knowledge')
|
||||
.slice(0,3).map(k => k.elementName),
|
||||
topSkill : ksaForCareer.filter(k => k.ksa_type === 'Skill')
|
||||
.slice(0,3).map(k => k.elementName)
|
||||
} : null,
|
||||
filterCtx : { sortBy, maxTuition, maxDistance, inStateOnly },
|
||||
schoolCtx : { count : filteredAndSortedSchools.length, sample : topSchools }
|
||||
}), [
|
||||
socCode, careerTitle, cipCodes,
|
||||
ksaForCareer, sortBy, maxTuition,
|
||||
maxDistance, inStateOnly,
|
||||
filteredAndSortedSchools
|
||||
]);
|
||||
|
||||
|
||||
useEffect(() => { setChatSnapshot(snapshot); },
|
||||
[snapshot, setChatSnapshot]);
|
||||
|
||||
|
||||
// Render a single KSA row
|
||||
function renderKsaRow(k, idx, careerTitle) {
|
||||
const elementName = k.elementName;
|
||||
|
Loading…
Reference in New Issue
Block a user