From 0635b60792c64348596fc674550a0779fe47b74d Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 7 Jul 2025 16:55:34 +0000 Subject: [PATCH] CareerRoadmap Supportbot context add. --- backend/utils/chatFreeEndpoint.js | 72 +++++++++++++++++++++++ src/App.js | 1 + src/ai/agent_support_reference.json | 9 ++- src/components/CareerRoadmap.js | 43 ++++++++++++-- src/components/EducationalProgramsPage.js | 39 +++++++++++- 5 files changed, 157 insertions(+), 7 deletions(-) diff --git a/backend/utils/chatFreeEndpoint.js b/backend/utils/chatFreeEndpoint.js index bd2dc9c..db192b0 100644 --- a/backend/utils/chatFreeEndpoint.js +++ b/backend/utils/chatFreeEndpoint.js @@ -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 = [ diff --git a/src/App.js b/src/App.js index c4fbcfd..e0ba509 100644 --- a/src/App.js +++ b/src/App.js @@ -556,6 +556,7 @@ const uiToolHandlers = useMemo(() => { diff --git a/src/ai/agent_support_reference.json b/src/ai/agent_support_reference.json index 9f8bc1f..8951c55 100644 --- a/src/ai/agent_support_reference.json +++ b/src/ai/agent_support_reference.json @@ -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" } +] } diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index 74bf65d..3ba6879 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -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)); diff --git a/src/components/EducationalProgramsPage.js b/src/components/EducationalProgramsPage.js index 92cb2dd..1f3571b 100644 --- a/src/components/EducationalProgramsPage.js +++ b/src/components/EducationalProgramsPage.js @@ -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;