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;
|
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 */
|
/* 2) Append task catalogue so the bot can describe valid actions */
|
||||||
const isPremium = req.user?.plan_type === "premium";
|
const isPremium = req.user?.plan_type === "premium";
|
||||||
system +=
|
system +=
|
||||||
@ -251,6 +290,39 @@ export default function chatFreeEndpoint(
|
|||||||
`Key tasks: ${tasks.slice(0,5).join("; ")}\n`;
|
`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 ────────────────────── */
|
/* ── Build tool list for this request ────────────────────── */
|
||||||
const tools = [...SUPPORT_TOOLS]; // guidance mode → support-only;
|
const tools = [...SUPPORT_TOOLS]; // guidance mode → support-only;
|
||||||
let messages = [
|
let messages = [
|
||||||
|
@ -556,6 +556,7 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
|
|
||||||
<ChatDrawer
|
<ChatDrawer
|
||||||
pageContext={pageContext}
|
pageContext={pageContext}
|
||||||
|
title="Help & Support"
|
||||||
snapshot={chatSnapshot}
|
snapshot={chatSnapshot}
|
||||||
uiToolHandlers={uiToolHandlers}
|
uiToolHandlers={uiToolHandlers}
|
||||||
/>
|
/>
|
||||||
|
@ -42,5 +42,12 @@
|
|||||||
{ "id": "EP-07", "label": "Max Distance filter" },
|
{ "id": "EP-07", "label": "Max Distance filter" },
|
||||||
{ "id": "EP-08", "label": "In-State Only checkbox" },
|
{ "id": "EP-08", "label": "In-State Only checkbox" },
|
||||||
{ "id": "EP-09", "label": "Select School button" }
|
{ "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 { useLocation, useParams } from 'react-router-dom';
|
||||||
import { Line, Bar } from 'react-chartjs-2';
|
import { Line, Bar } from 'react-chartjs-2';
|
||||||
import { format } from 'date-fns'; // ⬅ install if not already
|
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 parseFloatOrZero from '../utils/ParseFloatorZero.js';
|
||||||
import { getFullStateName } from '../utils/stateUtils.js';
|
import { getFullStateName } from '../utils/stateUtils.js';
|
||||||
import CareerCoach from "./CareerCoach.js";
|
import CareerCoach from "./CareerCoach.js";
|
||||||
|
import ChatCtx from '../contexts/ChatCtx.js';
|
||||||
|
|
||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import { Pencil } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
@ -390,10 +391,9 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
|
|||||||
const [buttonDisabled, setButtonDisabled] = useState(false);
|
const [buttonDisabled, setButtonDisabled] = useState(false);
|
||||||
const [aiRisk, setAiRisk] = useState(null);
|
const [aiRisk, setAiRisk] = useState(null);
|
||||||
|
|
||||||
const {
|
const { setChatSnapshot } = useContext(ChatCtx);
|
||||||
projectionData: initProjData = [],
|
|
||||||
loanPayoffMonth: initLoanMonth = null
|
|
||||||
} = location.state || {};
|
|
||||||
|
|
||||||
const reloadScenarioAndCollege = useCallback(async () => {
|
const reloadScenarioAndCollege = useCallback(async () => {
|
||||||
if (!careerProfileId) return;
|
if (!careerProfileId) return;
|
||||||
@ -623,6 +623,39 @@ useEffect(() => {
|
|||||||
randomRangeMax
|
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(() => {
|
useEffect(() => {
|
||||||
if (recommendations.length > 0) {
|
if (recommendations.length > 0) {
|
||||||
localStorage.setItem('aiRecommendations', JSON.stringify(recommendations));
|
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 { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import CareerSearch from './CareerSearch.js';
|
import CareerSearch from './CareerSearch.js';
|
||||||
import { ONET_DEFINITIONS } from './definitions.js';
|
import { ONET_DEFINITIONS } from './definitions.js';
|
||||||
import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js';
|
import { fetchSchools, clientGeocodeZip, haversineDistance } from '../utils/apiUtils.js';
|
||||||
|
import ChatCtx from '../contexts/ChatCtx.js';
|
||||||
|
|
||||||
// Helper to combine IM and LV for each KSA
|
// Helper to combine IM and LV for each KSA
|
||||||
function combineIMandLV(rows) {
|
function combineIMandLV(rows) {
|
||||||
@ -89,6 +90,8 @@ function EducationalProgramsPage() {
|
|||||||
|
|
||||||
const [showSearch, setShowSearch] = useState(true);
|
const [showSearch, setShowSearch] = useState(true);
|
||||||
|
|
||||||
|
const { setChatSnapshot } = useContext(ChatCtx);
|
||||||
|
|
||||||
// If user picks a new career from CareerSearch
|
// If user picks a new career from CareerSearch
|
||||||
const handleCareerSelected = (foundObj) => {
|
const handleCareerSelected = (foundObj) => {
|
||||||
setCareerTitle(foundObj.title || '');
|
setCareerTitle(foundObj.title || '');
|
||||||
@ -352,6 +355,40 @@ useEffect(() => {
|
|||||||
return result;
|
return result;
|
||||||
}, [schools, inStateOnly, userState, maxTuition, maxDistance, sortBy]);
|
}, [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
|
// Render a single KSA row
|
||||||
function renderKsaRow(k, idx, careerTitle) {
|
function renderKsaRow(k, idx, careerTitle) {
|
||||||
const elementName = k.elementName;
|
const elementName = k.elementName;
|
||||||
|
Loading…
Reference in New Issue
Block a user