diff --git a/backend/utils/chatFreeEndpoint.js b/backend/utils/chatFreeEndpoint.js
index d4be88a..bd2dc9c 100644
--- a/backend/utils/chatFreeEndpoint.js
+++ b/backend/utils/chatFreeEndpoint.js
@@ -4,6 +4,7 @@ import path from "path";
import { fileURLToPath } from "url";
import { vectorSearch } from "./vectorSearch.js";
+import { fuzzyCareerLookup } from "./fuzzyCareerLookup.js";
/* Resolve current directory ─────────────────────────────────────────────── */
const __filename = fileURLToPath(import.meta.url);
@@ -47,21 +48,34 @@ const buildContext = (user = {}, page = "", intent = "guide") => {
};
};
+/** Returns a \n-joined bullet list of banner + page tasks */
+const taskListForPage = (page = "", isPremium = false) => {
+ const banner = (SUPPORT_TASKS.global_banner || [])
+ // hide the “Upgrade” action for premium users
+ .filter(t => isPremium || t.id !== "GN-07");
+
+ const pageTasks = SUPPORT_TASKS[page] || [];
+ return [...banner, ...pageTasks]
+ .map(t => `• ${t.label}`)
+ .join("\n");
+};
+
const INTEREST_PLAYBOOK = `
### When the user is on **CareerExplorer** and no career tiles are visible
1. Explain there are two ways to begin:
• Interest Inventory (7-minute, 60-question survey)
• Manual search (type a career in the “Search for Career” bar)
-2. If the user chooses **Inventory**
- 1. Tell them to click the green **“Start Interest Inventory”** button at the top of the page.
- 2. Explain the answer keys (A = Agree, U = Unsure, D = Dislike) and that each click advances to the next question.
- 3. Wait while the UI runs the survey. **Do NOT collect answers inside chat.**
- 4. When career tiles appear, say: “Great! Your matches are listed below. Click any blue tile for details.”
+2. If the user chooses **Interest Inventory**
+ 1. Tell them to open the Interest Inventory from the top by Clicking "Find Your Career" then "Interest Inventory".
+ 2. Explain the answer keys (A = Agree, U = Unsure, D = Dislike); each click moves to the next question.
+ 3. Wait while the UI runs the survey (do **not** collect answers in chat).
+ 4. When the survey finishes, blue career tiles appear; tell them to click any tile to open its detail modal.
+3. If the user chooses **Career Search**
3. If the user chooses **Manual search**
1. Tell them to click the **search bar** and type at least three letters.
- 2. After they pick a suggestion, remind them to click the blue tile to open its details.
-4. **Never call \`getONetInterestQuestions\` or \`submitInterestInventory\` yourself.**
-5. After tiles appear, you may call salary, projection, skills, or other data tools to answer follow-up questions.
+ 2. When the suggestion list appears, they should select the desired career.
+ 3. A detail modal opens automatically — no blue tile in this flow.
+4. After a modal is open, you can guide them to salary, projections, AI-risk, etc.
`;
const CAREER_EXPLORER_FEATURES = `
@@ -80,14 +94,15 @@ const CAREER_EXPLORER_FEATURES = `
• When users ask:
• “Which is better?” → tell them to add both careers and open the comparison table.
• “What’s a day in the life?” → tell them to open the modal’s *Overview* tab.
- • “How do I plan education?” → tell them to click *Select for Education*.
-• Use tools when numeric data is needed:
- • \`getSalaryData\`, \`getEconomicProjections\`, \`getAiRisk\`, \`getCareerDetails\`.
-• You may call \`addCareerToComparison\` or \`openCareerModal\`
- **only after the user has clearly asked you to do so** (e.g. “Yes, add it for me”).
- Always confirm first.
+ • “How do I plan education?” → tell them to click *Select for Education*.
`;
+const SUPPORT_TASKS = JSON.parse(
+ await fs.readFile(
+ path.join(repoRoot, "src", "ai", "agent_support_reference.json"),
+ "utf8"
+ )
+);
/* ----------------------------------------------------------------------------
FACTORY: registers POST /api/chat/free on the passed-in Express app
----------------------------------------------------------------------------- */
@@ -127,43 +142,9 @@ export default function chatFreeEndpoint(
}
},
- /* NEW — forward any UI tool-call to the browser via SSE */
- async __forwardUiTool(name, argsObj, res) {
- res.write(`__tool:${name}:${JSON.stringify(argsObj)}\n`);
- if (typeof res.flush === "function") res.flush();
- return { forwarded: true };
- },
};
- /* -------------------- UI TOOLS (CareerExplorer only) -------------------- */
-const UI_TOOLS = [
- {
- type: "function",
- function: {
- name: "addCareerToComparison",
- description: "Add a career tile to the comparison table in Career Explorer",
- parameters: {
- type: "object",
- properties: { socCode: { type: "string" } },
- required: ["socCode"]
- }
- }
- },
- {
- type: "function",
- function: {
- name: "openCareerModal",
- description: "Open the Career-details modal for the given SOC code",
- parameters: {
- type: "object",
- properties: { socCode: { type: "string" } },
- required: ["socCode"]
- }
- }
- }
-];
-
-
+
const SUPPORT_TOOLS = [
{
type: "function",
@@ -202,7 +183,17 @@ const UI_TOOLS = [
authenticateUser,
async (req, res) => {
try {
- const { prompt = "", chatHistory = [], pageContext = "" } = req.body || {};
+ res.writeHead(200, {
+ "Content-Type": "text/plain; charset=utf-8",
+ "Cache-Control": "no-cache",
+ Connection : "keep-alive",
+ "X-Accel-Buffering": "no"
+ });
+ res.flushHeaders?.();
+
+ const sendChunk = (txt = "") => { res.write(txt); res.flush?.(); };
+
+ const { prompt = "", chatHistory = [], pageContext = "", snapshot = {} } = req.body || {};
if (!prompt.trim()) return res.status(400).json({ error: "Empty prompt" });
/* ---------- 0️⃣ FAQ fast-path ---------- */
@@ -220,20 +211,49 @@ const UI_TOOLS = [
/* --------------------------------------- */
const intent = classifyIntent(prompt);
- let { system } = buildContext(req.user || {}, pageContext, intent);
- if (pageContext === "CareerExplorer") {
- system += INTEREST_PLAYBOOK + CAREER_EXPLORER_FEATURES;
- }
+
+ /* ---------- system-prompt scaffold ---------- */
+ let { system } = buildContext(req.user || {}, pageContext, intent);
+
+ /* 1) Add master playbooks per page (optional) */
+ if (pageContext === "CareerExplorer") {
+ system += INTEREST_PLAYBOOK + CAREER_EXPLORER_FEATURES;
+ }
+
+ /* 2) Append task catalogue so the bot can describe valid actions */
+ const isPremium = req.user?.plan_type === "premium";
+ system +=
+ "\n\n### What the user can do on this screen\n" +
+ taskListForPage(pageContext, isPremium) +
+ "\n\n(Remember: do not click for the user; only explain the steps.)";
+
+ const modalPayload = snapshot?.modalCtx;
+
+ if (modalPayload) {
+ const {
+ socCode = "n/a",
+ title = "n/a",
+ aiRisk = "n/a",
+ salary = {},
+ projections = {},
+ description = {},
+ tasks = []
+ } = modalPayload;
+
+ system +=
+ `\n\n### Current career in focus\n` +
+ `SOC: ${socCode} | Title: ${title}\n` +
+ `AI-risk: ${aiRisk}\n` +
+ `Median salary – regional: $${salary.regional ?? "n/a"}, ` +
+ `national: $${salary.national ?? "n/a"}\n` +
+ `Projections: ${JSON.stringify(projections)}` +
+ `Description: ${description}\n` +
+ `Key tasks: ${tasks.slice(0,5).join("; ")}\n`;
+ }
/* ── Build tool list for this request ────────────────────── */
- let tools = intent === "support" ? [...SUPPORT_TOOLS] : [];
- const uiNamesForPage = PAGE_TOOLMAP[pageContext] || [];
- for (const def of BOT_TOOLS) {
- if (uiNamesForPage.includes(def.name)) {
- tools.push({ type: "function", function: def });
- }
- }
- const messages = [
+ const tools = [...SUPPORT_TOOLS]; // guidance mode → support-only;
+ let messages = [
{ role: "system", content: system },
...chatHistory,
{ role: "user", content: prompt }
@@ -244,70 +264,24 @@ const UI_TOOLS = [
stream : true,
messages,
tools,
- tool_choice : tools.length ? "auto" : undefined
});
- /* ── keep state while a tool call streams in ─────────────── */
- let pendingName = null; // addCareerToComparison
- let pendingArgs = ""; // '{"socCode":"15-2051"}'
+for await (const part of chatStream) {
+ const txt = part.choices?.[0]?.delta?.content;
+ if (txt) sendChunk(txt);
+ }
- for await (const part of chatStream) {
- const delta = part.choices?.[0]?.delta || {};
-
- /* 1️⃣ handle function / tool calls immediately */
- if (delta.tool_calls?.length) {
- const callObj = delta.tool_calls[0];
- const fn = callObj.function || {};
- if (fn.name) pendingName = fn.name; // keep first
- if (fn.arguments) pendingArgs += fn.arguments; // append
-
- // Try to parse the JSON only when it’s complete
- let args;
- try { args = JSON.parse(pendingArgs); } catch { continue; }
- /* run the resolver */
- let result;
- if (toolResolvers[pendingName]) {
- result = await toolResolvers[pendingName]({ ...args, user: req.user }, res);
- } else {
- result = await toolResolvers.__forwardUiTool(pendingName, args, res);
- }
- /* feed the result back to the model so it can finish */
- const followStream = await openai.chat.completions.create({
- model: "gpt-4o-mini",
- stream: true,
- messages: [
- ...messages,
- { role: "assistant", tool_call_id: callObj.id, content: null },
- {
- role: "tool",
- tool_call_id: callObj.id,
- content: JSON.stringify(result)
- }
- ]
- });
-
- /* stream the follow-up answer */
- for await (const follow of followStream) {
- const txt = follow.choices?.[0]?.delta?.content;
- if (txt) res.write(txt);
- }
- res.end();
- return; // ✔ done
- }
-
- /* 2️⃣ normal text tokens */
- if (!delta.tool_calls && delta.content) {
- res.write(delta.content); // SSE-safe
- }
- } // ← closes the for-await loop
-
- res.end(); // finished without tools
- } catch (err) { // ← closes the try block above
+ // tell the front-end we are done and close the stream
+ res.write("\n");
+res.end();
+
+
+ } catch (err) {
console.error("/api/chat/free error:", err);
if (!res.headersSent) {
res.status(500).json({ error: "Internal server error" });
}
}
- } // ← closes the async (req,res) => { … }
-); // ← closes app.post(…)
-} // ← closes export default chatFreeEndpoint
\ No newline at end of file
+ }
+);
+}
\ No newline at end of file
diff --git a/backend/utils/fuzzyCareerLookup.js b/backend/utils/fuzzyCareerLookup.js
new file mode 100644
index 0000000..7a3a33c
--- /dev/null
+++ b/backend/utils/fuzzyCareerLookup.js
@@ -0,0 +1,23 @@
+import path from "path";
+import fs from "node:fs";
+import { fileURLToPath } from "url";
+import Fuse from "fuse.js";
+
+/* resolve …/backend/utils → …/public/careers_with_ratings.json */
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const careersPath = path.join(__dirname, "..", "..", "public", "careers_with_ratings.json");
+
+const CAREERS = JSON.parse(fs.readFileSync(careersPath, "utf-8"));
+
+const fuse = new Fuse(CAREERS, {
+ keys : ["title"],
+ threshold : 0.30,
+ ignoreLocation: true,
+});
+
+export function fuzzyCareerLookup(label = "") {
+ if (!label.trim()) return null;
+ const [hit] = fuse.search(label.trim(), { limit: 1 });
+ return hit?.item ?? null;
+}
diff --git a/src/App.js b/src/App.js
index 1d439e8..c4fbcfd 100644
--- a/src/App.js
+++ b/src/App.js
@@ -33,6 +33,7 @@ import ResumeRewrite from './components/ResumeRewrite.js';
import LoanRepaymentPage from './components/LoanRepaymentPage.js';
import usePageContext from './utils/usePageContext.js';
import ChatDrawer from './components/ChatDrawer.js';
+import ChatCtx from './contexts/ChatCtx.js';
@@ -42,7 +43,7 @@ export const ProfileCtx = React.createContext();
function App() {
const navigate = useNavigate();
const location = useLocation();
- const pageContext = usePageContext();
+ const { pageContext, snapshot: routeSnapshot } = usePageContext();
/* ------------------------------------------
ChatDrawer – route-aware tool handlers
@@ -73,6 +74,7 @@ const uiToolHandlers = useMemo(() => {
// Auth states
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
+ const [chatSnapshot, setChatSnapshot] = useState(null);
// Loading state while verifying token
const [isLoading, setIsLoading] = useState(true);
@@ -213,6 +215,7 @@ const uiToolHandlers = useMemo(() => {
scenario, setScenario,
user, }}
>
+
{/* Header */}
@@ -553,12 +556,14 @@ const uiToolHandlers = useMemo(() => {
{/* Session Handler (Optional) */}
+
);
}
diff --git a/src/Root.js b/src/Root.js
new file mode 100644
index 0000000..f742296
--- /dev/null
+++ b/src/Root.js
@@ -0,0 +1,12 @@
+// Root.jsx
+import React from "react";
+import { PageFlagsProvider } from "./utils/PageFlagsContext";
+import App from "./App";
+
+export default function Root() {
+ return (
+
+
+
+ );
+}
diff --git a/src/ai/agent_support_reference.json b/src/ai/agent_support_reference.json
new file mode 100644
index 0000000..9f8bc1f
--- /dev/null
+++ b/src/ai/agent_support_reference.json
@@ -0,0 +1,46 @@
+{
+ "global_banner": [
+ { "id": "GN-01", "label": "Find Your Career tab (dropdown)" },
+ { "id": "GN-01A", "label": "Career Explorer option in dropdown" },
+ { "id": "GN-01B", "label": "Interest Inventory option in dropdown" },
+
+ { "id": "GN-02", "label": "Preparing & UpSkilling tab" },
+ { "id": "GN-03", "label": "Enhancing Your Career tab" },
+ { "id": "GN-04", "label": "Retirement Planning tab" },
+
+ { "id": "GN-05", "label": "Profile tab" },
+ { "id": "GN-06", "label": "Logout link" },
+
+ { "id": "GN-07", "label": "Upgrade to Premium button (green, only for free users)" }
+ ],
+ "CareerExplorer": [
+ { "id": "CE-01", "label": "Open Interest Inventory" },
+ { "id": "CE-02", "label": "Search & auto-select career" },
+ { "id": "CE-03", "label": "Open career modal" },
+ { "id": "CE-04", "label": "Add to Comparison" },
+ { "id": "CE-05", "label": "Interest & Meaning modal" },
+ { "id": "CE-06", "label": "Remove from Comparison" },
+ { "id": "CE-07", "label": "Plan Education/Skills" },
+ { "id": "CE-08", "label": "Edit Career Priorities" },
+ { "id": "CE-09", "label": "Save Career Priorities" },
+ { "id": "CE-10", "label": "Filter by Prep Level" },
+ { "id": "CE-11", "label": "Filter by Fit Level" },
+ { "id": "CE-12", "label": "Reload Career Suggestions" },
+ { "id": "CE-13", "label": "Legend (limited-data)" },
+ { "id": "CE-P1", "label": "Career Priorities popup appears (first-time users)" },
+ { "id": "CE-P2", "label": "Answer the six priority questions and click Save" },
+ { "id": "CE-M1", "label": "Career detail modal is open" },
+ { "id": "CE-M2", "label": "Scroll through the modal sections (AI-Risk banner → Job Description & Tasks → Salary Data → Economic Projections)" }
+ ],
+ "EducationalProgramsPage": [
+ { "id": "EP-01", "label": "Search & select career" },
+ { "id": "EP-02", "label": "Change Career button" },
+ { "id": "EP-03", "label": "Knowledge/Skills/Abilities table" },
+ { "id": "EP-04", "label": "Coursera / edX course links" },
+ { "id": "EP-05", "label": "Sort dropdown (Tuition / Distance)" },
+ { "id": "EP-06", "label": "Max Tuition filter" },
+ { "id": "EP-07", "label": "Max Distance filter" },
+ { "id": "EP-08", "label": "In-State Only checkbox" },
+ { "id": "EP-09", "label": "Select School button" }
+ ]
+}
diff --git a/src/assets/botTools.json b/src/assets/botTools.json
index 54bab63..d36f80d 100644
--- a/src/assets/botTools.json
+++ b/src/assets/botTools.json
@@ -19,6 +19,15 @@
]
}
},
+ {
+ "name": "resolveCareerTitle",
+ "description": "Convert a free-text career label to its canonical SOC record",
+ "parameters": {
+ "type": "object",
+ "properties": { "title": { "type": "string" } },
+ "required": ["title"]
+ }
+},
{
"name": "getSchoolsForCIPs",
"description": "Return a list of schools whose CIP codes match the supplied prefixes in the given state",
diff --git a/src/assets/pageToolMap.json b/src/assets/pageToolMap.json
index 777e57f..e716d12 100644
--- a/src/assets/pageToolMap.json
+++ b/src/assets/pageToolMap.json
@@ -9,6 +9,7 @@
"getTuitionForCIPs"
],
"CareerExplorer": [
+ "resolveCareerTitle",
"getEconomicProjections",
"getSalaryData",
"addCareerToComparison",
diff --git a/src/components/CareerExplorer.js b/src/components/CareerExplorer.js
index 4fa01a4..0489f8a 100644
--- a/src/components/CareerExplorer.js
+++ b/src/components/CareerExplorer.js
@@ -1,12 +1,12 @@
import React, { useState, useEffect, useMemo, useCallback, useContext } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
+import ChatCtx from '../contexts/ChatCtx.js';
import CareerSuggestions from './CareerSuggestions.js';
import CareerPrioritiesModal from './CareerPrioritiesModal.js';
import CareerModal from './CareerModal.js';
import InterestMeaningModal from './InterestMeaningModal.js';
import CareerSearch from './CareerSearch.js';
-import ChatDrawer from './ChatDrawer.js';
import { Button } from './ui/button.js';
import axios from 'axios';
@@ -72,6 +72,8 @@ function CareerExplorer() {
const [error, setError] = useState(null);
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
+ const { setChatSnapshot } = useContext(ChatCtx);
+
const [showInterestMeaningModal, setShowInterestMeaningModal] = useState(false);
const [modalData, setModalData] = useState({
career: null,
@@ -97,6 +99,43 @@ function CareerExplorer() {
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
+
+ // Weighted "match score" logic. (unchanged)
+ const priorityWeight = (priority, response) => {
+ const weightMap = {
+ interests: {
+ 'I know my interests (completed inventory)': 5,
+ 'I’m not sure yet': 1,
+ },
+ meaning: {
+ 'Yes, very important': 5,
+ 'Somewhat important': 3,
+ 'Not as important': 1,
+ },
+ stability: {
+ 'Very important': 5,
+ 'Somewhat important': 3,
+ 'Not as important': 1,
+ },
+ growth: {
+ 'Yes, very important': 5,
+ 'Somewhat important': 3,
+ 'Not as important': 1,
+ },
+ balance: {
+ 'Yes, very important': 5,
+ 'Somewhat important': 3,
+ 'Not as important': 1,
+ },
+ recognition: {
+ 'Very important': 5,
+ 'Somewhat important': 3,
+ 'Not as important': 1,
+ },
+ };
+ return weightMap[priority][response] || 1;
+ };
+
const jobZoneLabels = {
'1': 'Little or No Preparation',
'2': 'Some Preparation Needed',
@@ -494,6 +533,7 @@ function CareerExplorer() {
code: obj.soc_code,
title: obj.title,
cipCode: obj.cip_code,
+ fromManualSearch: true
};
console.log('[Dashboard -> handleCareerFromSearch] adapted =>', adapted);
handleCareerClick(adapted);
@@ -543,6 +583,54 @@ function CareerExplorer() {
'recognition',
];
+
+ /* ---------- core context: sent every turn ---------- */
+const coreCtx = useMemo(() => {
+ // 1) Riasec scores
+ const riasecScores = userProfile?.riasec_scores
+ ? JSON.parse(userProfile.riasec_scores)
+ : null;
+
+ // 2) priority weights normalised 0-1
+ const priorityWeights = priorities ? {
+ stability : priorityWeight('stability' , priorities.stability) / 5,
+ growth : priorityWeight('growth' , priorities.growth) / 5,
+ balance : priorityWeight('balance' , priorities.balance) / 5,
+ recognition : priorityWeight('recognition', priorities.recognition)/ 5,
+ interests : priorityWeight('interests' , priorities.interests) / 5,
+ mission : priorityWeight('meaning' , priorities.meaning) / 5,
+ } : null;
+
+ return { riasecScores, priorityWeights };
+}, [userProfile, priorities]);
+
+/* ---------- modal context: exists only while a modal is open ---------- */
+const modalCtx = useMemo(() => {
+ if (!selectedCareer || !careerDetails) return null;
+
+ const medianRow = careerDetails.salaryData
+ ?.find(r => r.percentile === "Median");
+
+ return {
+ socCode : selectedCareer.code,
+ title : selectedCareer.title,
+ aiRisk : careerDetails.aiRisk?.riskLevel ?? "n/a",
+ salary : medianRow
+ ? { regional : medianRow.regionalSalary,
+ national : medianRow.nationalSalary }
+ : null,
+ projections : careerDetails.economicProjections ?? {},
+ description : careerDetails.jobDescription,
+ tasks : careerDetails.tasks,
+ };
+}, [selectedCareer, careerDetails]);
+
+
+useEffect(() => {
+ // send null when no modal is open → ChatDrawer simply omits it
+ setChatSnapshot({ coreCtx, modalCtx });
+}, [coreCtx, modalCtx, setChatSnapshot]);
+
const getCareerRatingsBySocCode = (socCode) => {
return (
masterCareerRatings.find((c) => c.soc_code === socCode)?.ratings || {}
@@ -587,15 +675,18 @@ function CareerExplorer() {
const masterRatings = getCareerRatingsBySocCode(career.code);
// 2) figure out interest
- const userHasInventory = priorities.interests !== "I’m not sure yet";
+ const userHasInventory =
+ !career.fromManualSearch && // ← skip the shortcut if manual
+ priorities.interests &&
+ priorities.interests !== "I’m not sure yet";
const defaultInterestValue =
userHasInventory
- ? // if user has done inventory, we rely on fit rating or fallback to .json
+ ?
(fitRatingMap[career.fit] || masterRatings.interests || 3)
- : // otherwise, just start them at 3 (we'll ask in the modal)
+ :
3;
- // 3) always ask for meaning, start at 3
+
const defaultMeaningValue = 3;
// 4) open the InterestMeaningModal instead of using prompt()
@@ -726,41 +817,7 @@ const handleSelectForEducation = (career) => {
});
}, [careerSuggestions, selectedJobZone, selectedFit]);
- // Weighted "match score" logic. (unchanged)
- const priorityWeight = (priority, response) => {
- const weightMap = {
- interests: {
- 'I know my interests (completed inventory)': 5,
- 'I’m not sure yet': 1,
- },
- meaning: {
- 'Yes, very important': 5,
- 'Somewhat important': 3,
- 'Not as important': 1,
- },
- stability: {
- 'Very important': 5,
- 'Somewhat important': 3,
- 'Not as important': 1,
- },
- growth: {
- 'Yes, very important': 5,
- 'Somewhat important': 3,
- 'Not as important': 1,
- },
- balance: {
- 'Yes, very important': 5,
- 'Somewhat important': 3,
- 'Not as important': 1,
- },
- recognition: {
- 'Very important': 5,
- 'Somewhat important': 3,
- 'Not as important': 1,
- },
- };
- return weightMap[priority][response] || 1;
- };
+
useEffect(() => {
/* ---------- add-to-comparison ---------- */
@@ -827,37 +884,6 @@ const handleSelectForEducation = (career) => {
);
};
- /* ---------- Chat-assistant snapshot ---------- */
-const explorerSnapshot = useMemo(() => {
- // 1) RIASEC scores (if the interest inventory has been done)
- const riasecScores = userProfile?.riasec_scores
- ? JSON.parse(userProfile.riasec_scores) // stored as JSON in DB
- : null;
-
- // 2) User-selected career-priority weights, normalised to 0-1
- const priorityWeights = priorities
- ? {
- stability : priorityWeight('stability' , priorities.stability) / 5,
- growth : priorityWeight('growth' , priorities.growth) / 5,
- balance : priorityWeight('balance' , priorities.balance) / 5,
- recognition : priorityWeight('recognition', priorities.recognition)/ 5,
- interests : priorityWeight('interests' , priorities.interests) / 5,
- mission : priorityWeight('meaning' , priorities.meaning) / 5,
- }
- : null;
-
- // 3) Matrix of careers currently on-screen (or in the comparison list, pick one)
- const careerMatrix = filteredCareers.map(c => ({
- socCode : c.code,
- careerName : c.title,
- // quick-and-dirty match score; replace with your own if you’ve already got one
- matchScore : typeof c.matchScore === 'number'
- ? c.matchScore
- : (fitRatingMap[c.fit] || 0) * 20 // e.g. “Best” → 100, “Good” → 60
- }));
-
- return { riasecScores, priorityWeights, careerMatrix };
-}, [userProfile, priorities, filteredCareers]);
// ------------------------------------------------------
// Render
@@ -885,7 +911,17 @@ const explorerSnapshot = useMemo(() => {
/>
+
Career Comparison
+ {/* quick-edit link */}
+
+
{careerList.length ? (
diff --git a/src/components/ChatDrawer.js b/src/components/ChatDrawer.js
index ccca719..4a3c2f8 100644
--- a/src/components/ChatDrawer.js
+++ b/src/components/ChatDrawer.js
@@ -10,16 +10,13 @@ import { MessageCircle } from "lucide-react";
----------------------------------------------------------------*/
export default function ChatDrawer({
pageContext = "Home",
- snapshot = {},
- uiToolHandlers = {} // e.g. { addCareerToComparison, openCareerModal }
+ snapshot = null,
}) {
/* state */
const [open, setOpen] = useState(false);
const [prompt, setPrompt] = useState("");
const [messages, setMessages] = useState([]); // { role, content }
const listRef = useRef(null);
-
- console.log("CHATDRAWER-BUILD-TAG-2025-07-02");
/* auto-scroll */
useEffect(() => {
@@ -76,67 +73,26 @@ export default function ChatDrawer({
const decoder = new TextDecoder();
let buf = "";
+ /* ─────────────── STREAM LOOP ─────────────── */
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (!value) continue;
- const chunk = decoder.decode(value);
- buf += decoder.decode(value);
+ buf += decoder.decode(value, { stream: true });
- /* 1️⃣ process every complete “__tool:” line in the buffer */
+ let nl;
+ while ((nl = buf.indexOf("\n")) !== -1) {
+ const line = buf.slice(0, nl).trim(); // one full line
+ buf = buf.slice(nl + 1); // keep remainder
- for (const lineRaw of chunk.split(/\n/)) { // ← NEW
- const line = lineRaw.trim();
- if (!line.startsWith("__tool:")) continue;
-
- const firstColon = line.indexOf(":", 7);
- const name = line.slice(7, firstColon).trim();
- const argsJson = line.slice(firstColon + 1).trim();
- let args = {};
- try { args = JSON.parse(argsJson); } catch {/* keep {} */}
-
- const fn = uiToolHandlers[name];
- if (typeof fn === "function") {
- try {
- await fn(args);
- pushAssistant(`\n✓ ${name} completed.\n`);
- } catch (err) {
- console.error("[uiTool handler]", err);
- pushAssistant(`\nSorry – couldn’t complete ${name}.\n`);
- }
- } else {
- console.warn("No uiToolHandler for", name);
- pushAssistant(`\n(UI handler “${name}” isn’t wired on this page.)\n`);
- }
- return; // finished processing this chunk
- }
-
-
- /* 2️⃣ legacy JSON tool payload _________________________ */
- let json;
- try { json = JSON.parse(chunk); } catch {/* not JSON */ }
- if (json && json.uiTool) {
- const fn = uiToolHandlers[json.uiTool];
- if (typeof fn === "function") {
- try {
- await fn(JSON.parse(json.args || "{}"));
- pushAssistant(`\n✓ ${json.uiTool} completed.\n`);
- } catch (err) {
- console.error("[uiTool handler]", err);
- pushAssistant(`\nSorry – couldn’t complete ${json.uiTool}.\n`);
- }
- } else {
- console.warn("No uiToolHandler for", json.uiTool);
- pushAssistant(
- `\n(UI handler “${json.uiTool}” is not wired in the page.)\n`
- );
- }
- } else {
- /* plain assistant text */
- pushAssistant(chunk);
+ /* 2️⃣ normal assistant text */
+ if (line) pushAssistant(line + "\n");
}
}
+ /* ───────── END STREAM LOOP ───────── */
+
+ if (buf.trim()) pushAssistant(buf);
} catch (err) {
console.error("[ChatDrawer] stream error", err);
pushAssistant("Sorry — something went wrong. Please try again later.");
diff --git a/src/contexts/ChatCtx.js b/src/contexts/ChatCtx.js
new file mode 100644
index 0000000..4df5048
--- /dev/null
+++ b/src/contexts/ChatCtx.js
@@ -0,0 +1,5 @@
+// src/contexts/ChatCtx.js
+import { createContext } from 'react';
+
+const ChatCtx = createContext({ setChatSnapshot: () => {} });
+export default ChatCtx;
diff --git a/src/index.js b/src/index.js
index 4051a1d..3f9813c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,13 +4,16 @@ import './index.css';
import App from './App.js';
import { BrowserRouter } from 'react-router-dom'; // Import BrowserRouter
import reportWebVitals from './reportWebVitals.js';
+import { PageFlagsProvider } from './utils/PageFlagsContext.js';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
- {/* Wrap App with BrowserRouter */}
-
+
+
+
+
-);
+ );
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
diff --git a/src/utils/PageFlagsContext.js b/src/utils/PageFlagsContext.js
new file mode 100644
index 0000000..860f966
--- /dev/null
+++ b/src/utils/PageFlagsContext.js
@@ -0,0 +1,13 @@
+// src/utils/PageFlagsContext.js
+import React, { useState, createContext, useContext } from "react";
+
+const PageFlagsCtx = createContext([{}, () => {}]);
+
+export const PageFlagsProvider = ({ children }) => {
+ const state = useState({}); // [flags, setFlags]
+ return (
+ {children}
+ );
+};
+
+export const usePageFlags = () => useContext(PageFlagsCtx);
diff --git a/src/utils/usePageContext.js b/src/utils/usePageContext.js
index 07609ef..6955666 100644
--- a/src/utils/usePageContext.js
+++ b/src/utils/usePageContext.js
@@ -1,8 +1,8 @@
// src/utils/usePageContext.js
-import { useEffect, useState } from "react";
+import { useEffect, useState, useMemo } from "react";
import { useLocation } from "react-router-dom";
-/* route → page-key map */
+/* -------- route → page map (unchanged) -------- */
const routeMap = [
{ test: p => p.startsWith("/career-explorer"), page: "CareerExplorer" },
{ test: p => p.startsWith("/educational-programs"), page: "EducationalProgramsPage" },
@@ -11,7 +11,8 @@ const routeMap = [
{ test: p => p.startsWith("/resume-rewrite"), page: "ResumeRewrite" },
];
-export default function usePageContext() {
+export default function usePageContext(coreCtx = {}, modalCtx = null) {
+ /* -------- 1) figure out which page we’re on -------- */
const { pathname } = useLocation();
const [page, setPage] = useState("Home");
@@ -20,5 +21,13 @@ export default function usePageContext() {
setPage(found ? found.page : "Home");
}, [pathname]);
- return page; // ← hook now RETURNS the page string
+ /* -------- 2) memo-build one compact snapshot -------- */
+ const snapshot = useMemo(() => {
+ const snap = { ...coreCtx }; // always-on stuff
+ if (modalCtx) snap.modalPayload = modalCtx; // only when defined
+ return snap;
+ }, [coreCtx, modalCtx]);
+
+ /* You still get `page`, but now also `snapshot` */
+ return { pageContext: page, snapshot };
}
diff --git a/user_profile.db b/user_profile.db
index 57ef844..00f0fa0 100644
Binary files a/user_profile.db and b/user_profile.db differ