CareerRoadmap Supportbot context add.

This commit is contained in:
Josh 2025-07-07 16:55:34 +00:00
parent ec0ce1fce8
commit 0635b60792
5 changed files with 157 additions and 7 deletions

View File

@ -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 cant clickjust 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 = [

View File

@ -556,6 +556,7 @@ const uiToolHandlers = useMemo(() => {
<ChatDrawer
pageContext={pageContext}
title="Help & Support"
snapshot={chatSnapshot}
uiToolHandlers={uiToolHandlers}
/>

View File

@ -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" }
]
}

View File

@ -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));

View File

@ -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;