AI agent for CareerExplorer: AddtoComparison
This commit is contained in:
parent
2160f20f93
commit
58a8e15e09
@ -299,6 +299,21 @@ async function geocodeZipCode(zipCode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @aiTool {
|
||||||
|
"name": "getDistanceInMiles",
|
||||||
|
"description": "Return driving distance and duration between the user ZIP and destination(s)",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"userZipcode": { "type": "string" },
|
||||||
|
"destinations": { "type": "string", "description": "Pipe-separated lat,lng pairs or addresses" }
|
||||||
|
},
|
||||||
|
"required": ["userZipcode", "destinations"]
|
||||||
|
},
|
||||||
|
"pages": ["EducationalProgramsPage", "LoanRepayment"],
|
||||||
|
"write": false
|
||||||
|
} */
|
||||||
|
|
||||||
// Distance
|
// Distance
|
||||||
app.post('/api/maps/distance', async (req, res) => {
|
app.post('/api/maps/distance', async (req, res) => {
|
||||||
const { userZipcode, destinations } = req.body;
|
const { userZipcode, destinations } = req.body;
|
||||||
@ -496,6 +511,27 @@ app.get('/api/cip/:socCode', (req, res) => {
|
|||||||
res.status(404).json({ error: 'CIP code not found' });
|
res.status(404).json({ error: 'CIP code not found' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @aiTool {
|
||||||
|
"name": "getSchoolsForCIPs",
|
||||||
|
"description": "Return a list of schools whose CIP codes match the supplied prefixes in the given state",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cipCodes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated CIP prefixes, e.g. \"1101,1103\""
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Two-letter state abbreviation, e.g. \"GA\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cipCodes", "state"]
|
||||||
|
},
|
||||||
|
"pages": ["EducationalProgramsPage"],
|
||||||
|
"write": false
|
||||||
|
} */
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* Single schools / tuition / etc. routes
|
* Single schools / tuition / etc. routes
|
||||||
**************************************************/
|
**************************************************/
|
||||||
@ -553,6 +589,21 @@ app.get('/api/schools', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @aiTool {
|
||||||
|
"name": "getTuitionForCIPs",
|
||||||
|
"description": "Return in-state / out-state tuition rows for schools matching CIP prefixes in a given state",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cipCodes": { "type": "string", "description": "Comma-separated prefixes, e.g. \"1101,1103\"" },
|
||||||
|
"state": { "type": "string", "description": "Two-letter state code, e.g. \"GA\"" }
|
||||||
|
},
|
||||||
|
"required": ["cipCodes", "state"]
|
||||||
|
},
|
||||||
|
"pages": ["EducationalProgramsPage", "LoanRepayment"],
|
||||||
|
"write": false
|
||||||
|
} */
|
||||||
|
|
||||||
// tuition
|
// tuition
|
||||||
app.get('/api/tuition', (req, res) => {
|
app.get('/api/tuition', (req, res) => {
|
||||||
const { cipCodes, state } = req.query;
|
const { cipCodes, state } = req.query;
|
||||||
@ -603,6 +654,21 @@ app.get('/api/tuition', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @aiTool {
|
||||||
|
"name": "getEconomicProjections",
|
||||||
|
"description": "Return state and national employment projections for a SOC code",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"socCode": { "type": "string" },
|
||||||
|
"state": { "type": "string", "description": "Optional state abbreviation" }
|
||||||
|
},
|
||||||
|
"required": ["socCode"]
|
||||||
|
},
|
||||||
|
"pages": ["CareerExplorer"],
|
||||||
|
"write": false
|
||||||
|
} */
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* SINGLE route for projections from economicproj.json
|
* SINGLE route for projections from economicproj.json
|
||||||
**************************************************/
|
**************************************************/
|
||||||
@ -661,6 +727,21 @@ app.get('/api/projections/:socCode', (req, res) => {
|
|||||||
return res.json(result);
|
return res.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @aiTool {
|
||||||
|
"name": "getSalaryData",
|
||||||
|
"description": "Return residential area and national salary percentiles for a SOC code",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"socCode": { "type": "string" },
|
||||||
|
"area": { "type": "string", "description": "User's residential area" }
|
||||||
|
},
|
||||||
|
"required": ["socCode"]
|
||||||
|
},
|
||||||
|
"pages": ["CareerExplorer"],
|
||||||
|
"write": false
|
||||||
|
} */
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* Salary route
|
* Salary route
|
||||||
**************************************************/
|
**************************************************/
|
||||||
|
@ -1,15 +1,36 @@
|
|||||||
// utils/chatFreeEndpoint.js
|
/* ─── backend/utils/chatFreeEndpoint.js (TOP-OF-FILE REPLACEMENT) ───────── */
|
||||||
import path from "path";
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { vectorSearch } from "./vectorSearch.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import { vectorSearch } from "./vectorSearch.js";
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const rootPath = path.resolve(__dirname, "..", ".."); // backend/
|
/* Resolve current directory ─────────────────────────────────────────────── */
|
||||||
const FAQ_PATH = path.join(rootPath, "user_profile.db");
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/* Directories ───────────────────────────────────────────────────────────── */
|
||||||
|
/* repoRoot = “…/aptiva-dev1-app” (one level up from backend/) */
|
||||||
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||||
|
|
||||||
|
/* assetsDir = “…/aptiva-dev1-app/src/assets” (where the JSONs live) */
|
||||||
|
const assetsDir = path.join(repoRoot, "src", "assets");
|
||||||
|
|
||||||
|
/* FAQ SQLite DB (unchanged) */
|
||||||
|
const FAQ_PATH = path.join(repoRoot, "backend", "user_profile.db");
|
||||||
|
|
||||||
|
/* Constants ─────────────────────────────────────────────────────────────── */
|
||||||
const FAQ_THRESHOLD = 0.80;
|
const FAQ_THRESHOLD = 0.80;
|
||||||
const HELP_TRIGGERS = ["help","error","bug","issue","login","password","support"];
|
const HELP_TRIGGERS = ["help","error","bug","issue","login","password","support"];
|
||||||
|
|
||||||
|
/* Load tool manifests just once at boot ─────────────────────────────────── */
|
||||||
|
const BOT_TOOLS = JSON.parse(
|
||||||
|
await fs.readFile(path.join(assetsDir, "botTools.json"), "utf8")
|
||||||
|
);
|
||||||
|
const PAGE_TOOLMAP = JSON.parse(
|
||||||
|
await fs.readFile(path.join(assetsDir, "pageToolMap.json"), "utf8")
|
||||||
|
);
|
||||||
|
|
||||||
/* ---------- helpers ---------- */
|
/* ---------- helpers ---------- */
|
||||||
const classifyIntent = txt =>
|
const classifyIntent = txt =>
|
||||||
HELP_TRIGGERS.some(k => txt.toLowerCase().includes(k)) ? "support" : "guide";
|
HELP_TRIGGERS.some(k => txt.toLowerCase().includes(k)) ? "support" : "guide";
|
||||||
@ -26,6 +47,47 @@ const buildContext = (user = {}, page = "", intent = "guide") => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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.”
|
||||||
|
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.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CAREER_EXPLORER_FEATURES = `
|
||||||
|
### Aptiva Career Explorer — features you can (and should) guide the user to
|
||||||
|
• **Search bar** – type ≥3 letters, pick a suggestion, then click the blue tile.
|
||||||
|
• **Interest Inventory** – green “Start Interest Inventory” button (60 Qs, 7 min).
|
||||||
|
• **Blue career tile** – opens a modal with:
|
||||||
|
• *Overview* tab (description & “Day-in-the-Life” tasks)
|
||||||
|
• *Salary* tab (regional & national percentiles)
|
||||||
|
• *Projections* tab (state + national growth)
|
||||||
|
• *AI Risk* tab (Aptiva’s proprietary impact level)
|
||||||
|
• **Add to Comparison** button – builds a side-by-side table above the tiles.
|
||||||
|
• **Filters** dropdowns – “Preparation Level” (Job Zone 1-5) and “Fit Level” (Best / Great / Good).
|
||||||
|
• **Reload Career Suggestions** – re-runs your interest-based match with updated filters.
|
||||||
|
• **Select for Education** – jumps to Educational Programs with the career’s CIP codes.
|
||||||
|
• 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.
|
||||||
|
`;
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
FACTORY: registers POST /api/chat/free on the passed-in Express app
|
FACTORY: registers POST /api/chat/free on the passed-in Express app
|
||||||
----------------------------------------------------------------------------- */
|
----------------------------------------------------------------------------- */
|
||||||
@ -63,9 +125,45 @@ export default function chatFreeEndpoint(
|
|||||||
} catch {
|
} catch {
|
||||||
return { status: "db_error" };
|
return { status: "db_error" };
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/* 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 = [
|
const SUPPORT_TOOLS = [
|
||||||
{
|
{
|
||||||
type: "function",
|
type: "function",
|
||||||
@ -122,14 +220,24 @@ export default function chatFreeEndpoint(
|
|||||||
/* --------------------------------------- */
|
/* --------------------------------------- */
|
||||||
|
|
||||||
const intent = classifyIntent(prompt);
|
const intent = classifyIntent(prompt);
|
||||||
const { system } = buildContext(req.user || {}, pageContext, intent);
|
let { system } = buildContext(req.user || {}, pageContext, intent);
|
||||||
const tools = intent === "support" ? SUPPORT_TOOLS : [];
|
if (pageContext === "CareerExplorer") {
|
||||||
|
system += INTEREST_PLAYBOOK + CAREER_EXPLORER_FEATURES;
|
||||||
|
}
|
||||||
|
|
||||||
const messages = [
|
/* ── Build tool list for this request ────────────────────── */
|
||||||
{ role:"system", content: system },
|
let tools = intent === "support" ? [...SUPPORT_TOOLS] : [];
|
||||||
...chatHistory,
|
const uiNamesForPage = PAGE_TOOLMAP[pageContext] || [];
|
||||||
{ role:"user", content: prompt }
|
for (const def of BOT_TOOLS) {
|
||||||
];
|
if (uiNamesForPage.includes(def.name)) {
|
||||||
|
tools.push({ type: "function", function: def });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messages = [
|
||||||
|
{ role: "system", content: system },
|
||||||
|
...chatHistory,
|
||||||
|
{ role: "user", content: prompt }
|
||||||
|
];
|
||||||
|
|
||||||
const chatStream = await openai.chat.completions.create({
|
const chatStream = await openai.chat.completions.create({
|
||||||
model : "gpt-4o-mini",
|
model : "gpt-4o-mini",
|
||||||
@ -139,33 +247,67 @@ export default function chatFreeEndpoint(
|
|||||||
tool_choice : tools.length ? "auto" : undefined
|
tool_choice : tools.length ? "auto" : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ---------- SSE headers ---------- */
|
/* ── keep state while a tool call streams in ─────────────── */
|
||||||
res.setHeader("Content-Type", "text/event-stream");
|
let pendingName = null; // addCareerToComparison
|
||||||
res.setHeader("Cache-Control", "no-cache");
|
let pendingArgs = ""; // '{"socCode":"15-2051"}'
|
||||||
res.setHeader("Connection", "keep-alive");
|
|
||||||
|
|
||||||
for await (const part of chatStream) {
|
for await (const part of chatStream) {
|
||||||
const delta = part.choices?.[0]?.delta || {};
|
const delta = part.choices?.[0]?.delta || {};
|
||||||
const chunk = delta.content || "";
|
|
||||||
if (chunk) res.write(chunk); // ← plain text, no “data:” prefix
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
|
|
||||||
/* ---------- tool calls ---------- */
|
/* 1️⃣ handle function / tool calls immediately */
|
||||||
chatStream.on("tool", async call => {
|
if (delta.tool_calls?.length) {
|
||||||
const fn = toolResolvers[call.name];
|
const callObj = delta.tool_calls[0];
|
||||||
if (!fn) return;
|
const fn = callObj.function || {};
|
||||||
try {
|
if (fn.name) pendingName = fn.name; // keep first
|
||||||
const args = JSON.parse(call.arguments || "{}");
|
if (fn.arguments) pendingArgs += fn.arguments; // append
|
||||||
await fn({ ...args, user: req.user || {} });
|
|
||||||
} catch (err) {
|
// Try to parse the JSON only when it’s complete
|
||||||
console.error("[tool resolver]", err);
|
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
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} catch (err) {
|
/* 2️⃣ normal text tokens */
|
||||||
console.error("/api/chat/free error:", err);
|
if (!delta.tool_calls && delta.content) {
|
||||||
res.status(500).json({ error: "Internal server error" });
|
res.write(delta.content); // SSE-safe
|
||||||
}
|
}
|
||||||
|
} // ← closes the for-await loop
|
||||||
|
|
||||||
|
res.end(); // finished without tools
|
||||||
|
} catch (err) { // ← closes the try block above
|
||||||
|
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
|
327
package-lock.json
generated
327
package-lock.json
generated
@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "aptiva-dev1-app",
|
"name": "aptiva-dev1-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
@ -38,7 +39,7 @@
|
|||||||
"multer": "^1.4.5-lts.2",
|
"multer": "^1.4.5-lts.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"node-cron": "^4.1.0",
|
"node-cron": "^4.1.0",
|
||||||
"openai": "^4.97.0",
|
"openai": "^4.104.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
@ -60,10 +61,12 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/parser": "^7.28.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"glob": "^11.0.3",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^3.4.17"
|
"tailwindcss": "^3.4.17"
|
||||||
}
|
}
|
||||||
@ -437,12 +440,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.27.7",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||||
"integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==",
|
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.27.7"
|
"@babel/types": "^7.28.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@ -2049,9 +2052,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.27.7",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
|
||||||
"integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==",
|
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
@ -2499,6 +2502,29 @@
|
|||||||
"deprecated": "Use @eslint/object-schema instead",
|
"deprecated": "Use @eslint/object-schema instead",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/balanced-match": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/brace-expansion": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/balanced-match": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@ -2777,6 +2803,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jest/reporters/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jest/reporters/node_modules/source-map": {
|
"node_modules/@jest/reporters/node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@ -6196,6 +6243,28 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cacache/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cacache/node_modules/lru-cache": {
|
"node_modules/cacache/node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
@ -9588,6 +9657,27 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fork-ts-checker-webpack-plugin/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": {
|
"node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||||
@ -9952,21 +10042,24 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs.realpath": "^1.0.0",
|
"foreground-child": "^3.3.1",
|
||||||
"inflight": "^1.0.4",
|
"jackspeak": "^4.1.1",
|
||||||
"inherits": "2",
|
"minimatch": "^10.0.3",
|
||||||
"minimatch": "^3.1.1",
|
"minipass": "^7.1.2",
|
||||||
"once": "^1.3.0",
|
"package-json-from-dist": "^1.0.0",
|
||||||
"path-is-absolute": "^1.0.0"
|
"path-scurry": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@ -9990,6 +10083,75 @@
|
|||||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/glob/node_modules/jackspeak": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/cliui": "^8.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/lru-cache": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/brace-expansion": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob/node_modules/path-scurry": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/global-modules": {
|
"node_modules/global-modules": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
|
||||||
@ -11553,6 +11715,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-config/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-diff": {
|
"node_modules/jest-diff": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
|
||||||
@ -11881,6 +12064,27 @@
|
|||||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-runtime/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-serializer": {
|
"node_modules/jest-serializer": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz",
|
||||||
@ -13713,6 +13917,28 @@
|
|||||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-gyp/node_modules/npmlog": {
|
"node_modules/node-gyp/node_modules/npmlog": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
|
||||||
@ -17084,6 +17310,27 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rimraf/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "2.79.2",
|
"version": "2.79.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||||
@ -19087,6 +19334,27 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/test-exclude/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@ -20353,6 +20621,27 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/workbox-build/node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/workbox-build/node_modules/json-schema-traverse": {
|
"node_modules/workbox-build/node_modules/json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
"multer": "^1.4.5-lts.2",
|
"multer": "^1.4.5-lts.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"node-cron": "^4.1.0",
|
"node-cron": "^4.1.0",
|
||||||
"openai": "^4.97.0",
|
"openai": "^4.104.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
@ -58,7 +58,9 @@
|
|||||||
"start": "react-scripts start --host 0.0.0.0",
|
"start": "react-scripts start --host 0.0.0.0",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
|
"gen:tools": "node scripts/genTools.cjs",
|
||||||
|
"postinstall": "npm run gen:tools"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@ -84,10 +86,12 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/parser": "^7.28.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"glob": "^11.0.3",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"tailwindcss": "^3.4.17"
|
"tailwindcss": "^3.4.17"
|
||||||
}
|
}
|
||||||
|
40
scripts/genTools.cjs
Normal file
40
scripts/genTools.cjs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/* Generates:
|
||||||
|
* – src/assets/botTools.json
|
||||||
|
* – src/assets/pageToolMap.json
|
||||||
|
*
|
||||||
|
* It scans backend/server*.js for comment blocks like:
|
||||||
|
* /** @aiTool { "name": "...", "pages": ["CareerExplorer"], ... } *\/
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const glob = require('glob');
|
||||||
|
const { parse } = require('@babel/parser');
|
||||||
|
|
||||||
|
const files = glob.sync('backend/server*.js'); // adjust if server paths differ
|
||||||
|
|
||||||
|
const tools = [];
|
||||||
|
const pageToolMap = {};
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const code = fs.readFileSync(file, 'utf8');
|
||||||
|
const ast = parse(code, { sourceType: 'module', plugins: ['jsx'] });
|
||||||
|
|
||||||
|
(ast.comments || []).forEach(c => {
|
||||||
|
if (!c.value.includes('@aiTool')) return;
|
||||||
|
const meta = JSON.parse(c.value.replace(/^[\s\S]*@aiTool\s*/, '').trim());
|
||||||
|
|
||||||
|
tools.push({
|
||||||
|
name: meta.name,
|
||||||
|
description: meta.description,
|
||||||
|
parameters: meta.parameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
(meta.pages || ['Global']).forEach(p =>
|
||||||
|
(pageToolMap[p] ||= []).push(meta.name)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync('src/assets', { recursive: true });
|
||||||
|
fs.writeFileSync('src/assets/botTools.json', JSON.stringify(tools, null, 2));
|
||||||
|
fs.writeFileSync('src/assets/pageToolMap.json', JSON.stringify(pageToolMap, null, 2));
|
||||||
|
console.log('✅ botTools.json & pageToolMap.json generated');
|
37
src/App.js
37
src/App.js
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
@ -32,15 +32,43 @@ import RetirementPlanner from './components/RetirementPlanner.js';
|
|||||||
import ResumeRewrite from './components/ResumeRewrite.js';
|
import ResumeRewrite from './components/ResumeRewrite.js';
|
||||||
import LoanRepaymentPage from './components/LoanRepaymentPage.js';
|
import LoanRepaymentPage from './components/LoanRepaymentPage.js';
|
||||||
import usePageContext from './utils/usePageContext.js';
|
import usePageContext from './utils/usePageContext.js';
|
||||||
|
import ChatDrawer from './components/ChatDrawer.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const ProfileCtx = React.createContext();
|
export const ProfileCtx = React.createContext();
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
usePageContext();
|
const pageContext = usePageContext();
|
||||||
|
|
||||||
|
/* ------------------------------------------
|
||||||
|
ChatDrawer – route-aware tool handlers
|
||||||
|
------------------------------------------ */
|
||||||
|
const uiToolHandlers = useMemo(() => {
|
||||||
|
if (pageContext === "CareerExplorer") {
|
||||||
|
return {
|
||||||
|
// __tool:addCareerToComparison:{"socCode":"15-2051","careerName":"Data Scientist"}
|
||||||
|
addCareerToComparison: ({ socCode, careerName }) => {
|
||||||
|
console.log('[dispatch]', socCode, careerName);
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("add-career", { detail: { socCode, careerName } })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// __tool:openCareerModal:{"socCode":"15-2051"}
|
||||||
|
openCareerModal: ({ socCode }) => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("open-career", { detail: { socCode } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {}; // every other page exposes no UI tools
|
||||||
|
}, [pageContext]);
|
||||||
|
|
||||||
|
|
||||||
// Auth states
|
// Auth states
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
@ -523,6 +551,11 @@ function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<ChatDrawer
|
||||||
|
pageContext={pageContext}
|
||||||
|
uiToolHandlers={uiToolHandlers}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Session Handler (Optional) */}
|
{/* Session Handler (Optional) */}
|
||||||
<SessionExpiredHandler />
|
<SessionExpiredHandler />
|
||||||
</div>
|
</div>
|
||||||
|
130
src/assets/botTools.json
Normal file
130
src/assets/botTools.json
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "getDistanceInMiles",
|
||||||
|
"description": "Return driving distance and duration between the user ZIP and destination(s)",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"userZipcode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"destinations": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Pipe-separated lat,lng pairs or addresses"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"userZipcode",
|
||||||
|
"destinations"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getSchoolsForCIPs",
|
||||||
|
"description": "Return a list of schools whose CIP codes match the supplied prefixes in the given state",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cipCodes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated CIP prefixes, e.g. \"1101,1103\""
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Two-letter state abbreviation, e.g. \"GA\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cipCodes",
|
||||||
|
"state"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addCareerToComparison",
|
||||||
|
"description": "Add the career with the given SOC code to the comparison table in Career Explorer",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"socCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full O*NET SOC code, e.g. \"15-2051\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["socCode"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "openCareerModal",
|
||||||
|
"description": "Open the career-details modal for the given SOC code in Career Explorer",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"socCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Full O*NET SOC code, e.g. \"15-2051\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["socCode"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getTuitionForCIPs",
|
||||||
|
"description": "Return in-state / out-state tuition rows for schools matching CIP prefixes in a given state",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cipCodes": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated prefixes, e.g. \"1101,1103\""
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Two-letter state code, e.g. \"GA\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"cipCodes",
|
||||||
|
"state"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getEconomicProjections",
|
||||||
|
"description": "Return state and national employment projections for a SOC code",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"socCode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional state abbreviation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"socCode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getSalaryData",
|
||||||
|
"description": "Return residential area and national salary percentiles for a SOC code",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"socCode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"area": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "User's residential area"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"socCode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
17
src/assets/pageToolMap.json
Normal file
17
src/assets/pageToolMap.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"EducationalProgramsPage": [
|
||||||
|
"getDistanceInMiles",
|
||||||
|
"getSchoolsForCIPs",
|
||||||
|
"getTuitionForCIPs"
|
||||||
|
],
|
||||||
|
"LoanRepayment": [
|
||||||
|
"getDistanceInMiles",
|
||||||
|
"getTuitionForCIPs"
|
||||||
|
],
|
||||||
|
"CareerExplorer": [
|
||||||
|
"getEconomicProjections",
|
||||||
|
"getSalaryData",
|
||||||
|
"addCareerToComparison",
|
||||||
|
"openCareerModal"
|
||||||
|
]
|
||||||
|
}
|
@ -231,7 +231,6 @@ function CareerExplorer() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------
|
// --------------------------------------
|
||||||
// On mount, load suggestions from cache
|
// On mount, load suggestions from cache
|
||||||
// --------------------------------------
|
// --------------------------------------
|
||||||
@ -763,6 +762,49 @@ const handleSelectForEducation = (career) => {
|
|||||||
return weightMap[priority][response] || 1;
|
return weightMap[priority][response] || 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/* ---------- add-to-comparison ---------- */
|
||||||
|
const onAdd = (e) => {
|
||||||
|
console.log('[onAdd] detail →', e.detail);
|
||||||
|
const { socCode, careerName } = e.detail || {};
|
||||||
|
if (!socCode) {
|
||||||
|
console.warn('[add-career] missing socCode – aborting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. see if the career is already in the filtered list
|
||||||
|
let career = filteredCareers.find((c) => c.code === socCode);
|
||||||
|
|
||||||
|
// 2. if not, make a stub so the list can still save
|
||||||
|
if (!career) {
|
||||||
|
career = {
|
||||||
|
code : socCode,
|
||||||
|
title: careerName || '(name unavailable)',
|
||||||
|
fit : 'Good',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. push it into the comparison table
|
||||||
|
addCareerToList(career);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------- open-modal ---------- */
|
||||||
|
const onOpen = (e) => {
|
||||||
|
const { socCode } = e.detail || {};
|
||||||
|
if (!socCode) return;
|
||||||
|
const career = filteredCareers.find((c) => c.code === socCode);
|
||||||
|
if (career) handleCareerClick(career);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('add-career', onAdd);
|
||||||
|
window.addEventListener('open-career', onOpen);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('add-career', onAdd);
|
||||||
|
window.removeEventListener('open-career', onOpen);
|
||||||
|
};
|
||||||
|
}, [filteredCareers, addCareerToList, handleCareerClick]);
|
||||||
|
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Loading Overlay
|
// Loading Overlay
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@ -1017,11 +1059,6 @@ const explorerSnapshot = useMemo(() => {
|
|||||||
defaultMeaning={modalData.defaultMeaning}
|
defaultMeaning={modalData.defaultMeaning}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChatDrawer
|
|
||||||
pageContext="explorer"
|
|
||||||
snapshot={explorerSnapshot} // the JSON you’re already building
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedCareer && (
|
{selectedCareer && (
|
||||||
<CareerModal
|
<CareerModal
|
||||||
career={selectedCareer}
|
career={selectedCareer}
|
||||||
@ -1035,35 +1072,47 @@ const explorerSnapshot = useMemo(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
<div className="mt-6 text-xs text-gray-500 border-t pt-2">
|
||||||
Career results and details provided by
|
This page includes information from
|
||||||
<a
|
<a
|
||||||
href="https://www.onetcenter.org"
|
href="https://www.onetcenter.org"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
className="underline"
|
||||||
{' '}
|
>
|
||||||
O*Net
|
O*NET OnLine
|
||||||
</a>
|
</a>
|
||||||
, in partnership with
|
by the U.S. Department of Labor, Employment & Training Administration
|
||||||
<a
|
(USDOL/ETA). Used under the
|
||||||
href="https://www.bls.gov"
|
<a
|
||||||
target="_blank"
|
href="https://creativecommons.org/licenses/by/4.0/"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
{' '}
|
className="underline"
|
||||||
Bureau of Labor Statistics
|
>
|
||||||
</a>
|
CC BY 4.0 license
|
||||||
and
|
</a>
|
||||||
<a
|
. **O*NET®** is a trademark of USDOL/ETA. Salary and employment data are
|
||||||
href="https://nces.ed.gov"
|
enriched with resources from the
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
href="https://www.bls.gov"
|
||||||
>
|
target="_blank"
|
||||||
{' '}
|
rel="noopener noreferrer"
|
||||||
NCES
|
className="underline"
|
||||||
</a>
|
>
|
||||||
.
|
Bureau of Labor Statistics
|
||||||
</div>
|
</a>
|
||||||
|
and program information from the
|
||||||
|
<a
|
||||||
|
href="https://nces.ed.gov"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
National Center for Education Statistics
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,141 +1,196 @@
|
|||||||
|
// ────────────────────────────────── ChatDrawer.jsx
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Sheet, SheetTrigger, SheetContent } from "./ui/sheet.js";
|
import { Sheet, SheetTrigger, SheetContent } from "./ui/sheet.js";
|
||||||
import { Card, CardContent } from "./ui/card.js";
|
|
||||||
import { Button } from "./ui/button.js";
|
import { Button } from "./ui/button.js";
|
||||||
import { Input } from "./ui/input.js";
|
import { Input } from "./ui/input.js";
|
||||||
import { MessageCircle } from "lucide-react";
|
import { MessageCircle } from "lucide-react";
|
||||||
|
|
||||||
/**
|
/* ---------------------------------------------------------------
|
||||||
* ChatDrawer – Aptiva free‑tier assistant (pure **JavaScript** version)
|
Streams from /api/chat/free and executes UI-tool callbacks
|
||||||
*
|
----------------------------------------------------------------*/
|
||||||
* ▸ Floating FAB → slide‑out Sheet.
|
export default function ChatDrawer({
|
||||||
* ▸ Streams SSE from `/api/chat/free`.
|
pageContext = "Home",
|
||||||
* ▸ JWT is read from `localStorage('token')` (same key CareerExplorer uses).
|
snapshot = {},
|
||||||
*/
|
uiToolHandlers = {} // e.g. { addCareerToComparison, openCareerModal }
|
||||||
export default function ChatDrawer({ pageContext = "explorer", snapshot = {} }) {
|
}) {
|
||||||
// ──────────────────────────────────────────────────────────────── state
|
/* state */
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [prompt, setPrompt] = useState("");
|
const [prompt, setPrompt] = useState("");
|
||||||
const [messages, setMessages] = useState([]); // { role, content }
|
const [messages, setMessages] = useState([]); // { role, content }
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────── auto‑scroll
|
console.log("CHATDRAWER-BUILD-TAG-2025-07-02");
|
||||||
|
|
||||||
|
/* auto-scroll */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (listRef.current) {
|
listRef.current &&
|
||||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
(listRef.current.scrollTop = listRef.current.scrollHeight);
|
||||||
}
|
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────── helper: append chunk
|
/* helper: stream-friendly append */
|
||||||
function appendAssistantChunk(chunk) {
|
const pushAssistant = (chunk) =>
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const last = prev[prev.length - 1];
|
const last = prev.at(-1);
|
||||||
if (last && last.role === "assistant") {
|
if (last?.role === "assistant") {
|
||||||
// mutate copy – React will re‑render because we return new array
|
|
||||||
const updated = [...prev];
|
const updated = [...prev];
|
||||||
updated[updated.length - 1] = {
|
updated[updated.length - 1] = {
|
||||||
...last,
|
...last,
|
||||||
content: last.content + chunk,
|
content: last.content + chunk
|
||||||
};
|
};
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
// first assistant chunk → push new msg
|
|
||||||
return [...prev, { role: "assistant", content: chunk }];
|
return [...prev, { role: "assistant", content: chunk }];
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────── send prompt
|
/* send prompt */
|
||||||
async function sendPrompt() {
|
async function sendPrompt() {
|
||||||
const text = prompt.trim();
|
const text = prompt.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
// optimistic user message
|
|
||||||
setMessages((m) => [...m, { role: "user", content: text }]);
|
setMessages((m) => [...m, { role: "user", content: text }]);
|
||||||
setPrompt("");
|
setPrompt("");
|
||||||
|
|
||||||
const token = localStorage.getItem("token") || "";
|
const body = JSON.stringify({
|
||||||
|
prompt: text,
|
||||||
|
pageContext,
|
||||||
|
chatHistory: messages,
|
||||||
|
snapshot
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem("token") || "";
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept : "text/event-stream",
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||||
|
};
|
||||||
|
|
||||||
const resp = await fetch("/api/chat/free", {
|
const resp = await fetch("/api/chat/free", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers,
|
||||||
"Content-Type": "application/json",
|
body
|
||||||
"Accept": "text/event-stream",
|
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: text,
|
|
||||||
pageContext,
|
|
||||||
chatHistory: messages,
|
|
||||||
snapshot,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
|
||||||
if (!resp.ok || !resp.body) {
|
const reader = resp.body.getReader();
|
||||||
throw new Error(`Request failed ${resp.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// stream the response
|
|
||||||
const reader = resp.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let done = false;
|
let buf = "";
|
||||||
while (!done) {
|
|
||||||
const { value, done: doneReading } = await reader.read();
|
while (true) {
|
||||||
done = doneReading;
|
const { value, done } = await reader.read();
|
||||||
if (value) {
|
if (done) break;
|
||||||
const chunk = decoder.decode(value);
|
if (!value) continue;
|
||||||
appendAssistantChunk(chunk);
|
|
||||||
|
const chunk = decoder.decode(value);
|
||||||
|
buf += decoder.decode(value);
|
||||||
|
|
||||||
|
/* 1️⃣ process every complete “__tool:” line in the buffer */
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ChatDrawer] error", err);
|
console.error("[ChatDrawer] stream error", err);
|
||||||
appendAssistantChunk("Sorry – something went wrong. Try again later.");
|
pushAssistant("Sorry — something went wrong. Please try again later.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────── key handler
|
/* Enter submits */
|
||||||
function handleKeyDown(e) {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPrompt();
|
sendPrompt();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────── render
|
/* UI */
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
{/* Floating action button */}
|
{/* floating button */}
|
||||||
<SheetTrigger>
|
<SheetTrigger>
|
||||||
<button
|
<button
|
||||||
aria-label="Open chat"
|
aria-label="Open chat"
|
||||||
className="fixed bottom-6 right-6 z-50 rounded-full bg-blue-600 p-3 text-white shadow-lg hover:bg-blue-700"
|
className="fixed bottom-6 right-6 z-50 rounded-full bg-blue-600 p-3 text-white shadow-lg hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
<MessageCircle size={24} />
|
<MessageCircle size={24} />
|
||||||
</button>
|
</button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
|
|
||||||
{/* Drawer */}
|
{/* drawer */}
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="flex max-h-screen w-[380px] flex-col px-0 md:w-[420px]"
|
className="flex max-h-screen w-[380px] flex-col px-0 md:w-[420px]"
|
||||||
>
|
>
|
||||||
{/* header */}
|
|
||||||
<div className="sticky top-0 z-10 border-b bg-white px-4 py-3 font-semibold">
|
<div className="sticky top-0 z-10 border-b bg-white px-4 py-3 font-semibold">
|
||||||
{pageContext === "explorer" ? "Career Explorer Guide" : "Programs Guide"}
|
{pageContext}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* messages list */}
|
{/* transcript */}
|
||||||
<div ref={listRef} className="flex-1 space-y-4 overflow-y-auto px-4 py-2 text-sm">
|
<div
|
||||||
|
ref={listRef}
|
||||||
|
className="flex-1 space-y-4 overflow-y-auto px-4 py-2 text-sm"
|
||||||
|
>
|
||||||
{messages.map((m, i) => (
|
{messages.map((m, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={m.role === "user" ? "text-right" : "text-left text-gray-800"}
|
className={
|
||||||
|
m.role === "user" ? "text-right" : "text-left text-gray-800"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{m.content}
|
{m.content}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* input */}
|
{/* prompt box */}
|
||||||
<div className="border-t p-4">
|
<div className="border-t p-4">
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
import { useEffect } from 'react';
|
// src/utils/usePageContext.js
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useEffect, useState } from "react";
|
||||||
import { sendSystemMessage } from '../utils/chatApi.js'; // you already call this for Jess
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export default function usePageContext () {
|
/* route → page-key map */
|
||||||
|
const routeMap = [
|
||||||
|
{ test: p => p.startsWith("/career-explorer"), page: "CareerExplorer" },
|
||||||
|
{ test: p => p.startsWith("/educational-programs"), page: "EducationalProgramsPage" },
|
||||||
|
{ test: p => p.startsWith("/career-roadmap"), page: "CareerRoadmap" },
|
||||||
|
{ test: p => p.startsWith("/retirement"), page: "RetirementPlanner" },
|
||||||
|
{ test: p => p.startsWith("/resume-rewrite"), page: "ResumeRewrite" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function usePageContext() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const [page, setPage] = useState("Home");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const page = pathname.split('/')[1] || 'Home';
|
const found = routeMap.find(r => r.test(pathname));
|
||||||
const featureFlags = window.__APTIVA_FLAGS__ || [];
|
setPage(found ? found.page : "Home");
|
||||||
|
|
||||||
// invisible system message for the adaptive assistant
|
|
||||||
sendSystemMessage('page_context', { page, featureFlags });
|
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
return page; // ← hook now RETURNS the page string
|
||||||
}
|
}
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user