import React, { useState, useEffect, useRef } from "react";
import authFetch from "../utils/authFetch.js";
const isoToday = new Date().toISOString().slice(0,10); // top-level helper
function buildInterviewPrompt(careerName, jobDescription = "") {
return `
You are an expert interviewer for the role **${careerName}**.
Ask one challenging behavioural or technical question **specific to this career**,
wait for the candidate's reply, then:
• Score the answer 1–5
• Give concise feedback (1-2 sentences)
• Ask the next question (up to 5 total)
After 5 questions or if the user types "quit interview", end the session.
Do NOT output milestones JSON.`;
}
/* ----------------------------------------------
Hidden prompts for the quick-action buttons
---------------------------------------------- */
function planPrompt( _opts={}) {
return `
# ⛔️ DO NOT wrap in markdown, do NOT add back-ticks, do NOT add commentary.
# ⛔️ Respond with ONE thing only: the VALID JSON object below.
# ⛔️ The very first character of your reply MUST be “{”.
{
"milestones":[
{
"title":"", /* GPT must fill — make unique & action-oriented */
"date":"YYYY-MM-DD", /* ≤ 6 mo, ≥ ${isoToday} */
"description":"1-2 sentences",
"impacts":[],
"tasks":[ … ]
},
{
"title":"", /* GPT must fill — phase-2 name */
"date":"YYYY-MM-DD", /* 9-18 mo out */
"description":"1-2 sentences",
"impacts":[],
"tasks":[ … ]
}
]
}
/* Rules — read carefully:
• You must provide DISTINCT, action-oriented titles.
• Titles may NOT duplicate any existing milestone (case-insensitive).
• Include at least one concrete noun (event, cert, org, etc.).
*/
`.trim();
}
const QUICK_PROMPTS = {
networking: ({ careerName, goalsText }) => `
MODE : networking_plan
ROLE : ${careerName}
GOALS: ${goalsText || "N/A"}
Create three milestones that expand professional connections.
${planPrompt()}
`.trim(),
jobSearch: ({ careerName, goalsText }) => `
MODE : job_search_plan
ROLE : ${careerName}
GOALS: ${goalsText || "N/A"}
Draft three milestones that accelerate the job hunt.
${planPrompt()}
`.trim(),
aiGrowth: ({ careerName, skillsText = "N/A", goalsText = "N/A" }) => `
MODE: ai_growth
TODAY = ${isoToday}
ROLE : ${careerName}
SKILLS : ${skillsText}
GOALS : ${goalsText}
INSTRUCTIONS
1. List 2-3 role tasks already touched by AI.
– For each: how to *collaborate* with AI + one human-edge upskilling step.
2. Surface adjacent AI-created roles.
3. End with a 90-day action plan (bullets, dates ≥ TODAY).
${planPrompt({ label: "AI Growth" })}
`.trim(),
interview: `
MODE: interview
You are an expert interview coach.
Ask one behavioural or technical question, wait for the user's reply,
score 1-5, give concise feedback, then ask the next question.
Stop after 5 questions or if the user types "quit interview".
Do NOT output milestones JSON.`.trim()
};
export default function CareerCoach({
userProfile,
financialProfile,
scenarioRow,
setScenarioRow,
careerProfileId,
collegeProfile,
onMilestonesCreated,
onAiRiskFetched
}) {
/* -------------- state ---------------- */
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [aiRisk, setAiRisk] = useState(null);
const chatRef = useRef(null);
const [showGoals , setShowGoals ] = useState(false);
const [draftGoals, setDraftGoals] = useState(scenarioRow?.career_goals || "");
const [saving , setSaving ] = useState(false);
const [threadId, setThreadId] = useState(null);
/* -------------- scroll --------------- */
useEffect(() => {
if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight;
}, [messages]);
useEffect(() => {
(async () => {
if (!careerProfileId) return;
// list threads for this profile
const r = await authFetch(
`/api/premium/coach/chat/threads?careerProfileId=${encodeURIComponent(careerProfileId)}`
);
if (!(r.ok && (r.headers.get('content-type') || '').includes('application/json'))) {
setThreadId(null); // coach offline; no network errors on mount
return;
}
const { threads = [] } = await r.json();
const existing = threads.find(Boolean);
if (!existing?.id) {
setThreadId(null); // no thread yet; lazy-create on first send
return;
}
const id = existing.id;
setThreadId(id);
// preload history
const r3 = await authFetch(
`/api/premium/coach/chat/threads/${id}?careerProfileId=${encodeURIComponent(careerProfileId)}`
);
if (r3.ok && (r3.headers.get('content-type') || '').includes('application/json')) {
const { messages: msgs = [] } = await r3.json();
setMessages(msgs);
}
})();
}, [careerProfileId]);
/* -------------- intro ---------------- */
useEffect(() => {
if (!scenarioRow) return;
setMessages(prev =>
prev.length ? prev // keep what we loaded
: [generatePersonalizedIntro()] );
}, [scenarioRow?.id]);
/* ---------- helpers you already had ---------- */
function buildStatusSituationMessage(status, situation, careerName) {
/* (unchanged body) */
const sStatus = (status || "").toLowerCase();
const sSituation = (situation || "").toLowerCase();
let nowPart = "";
switch (sStatus) {
case "planned":
nowPart = `It appears you’re looking ahead to a possible future as it pertains to ${careerName}.`;
break;
case "current":
nowPart = `It appears you’re currently working in a role as it pertains to ${careerName}.`;
break;
case "exploring":
nowPart = `It appears you’re exploring how ${careerName} might fit your plans.`;
break;
default:
nowPart = `I don’t have a clear picture of your involvement with ${careerName}, but I’m here to help.`;
}
let nextPart = "";
switch (sSituation) {
case "planning":
nextPart = `You're aiming to clarify your strategy for moving into this field.`;
break;
case "preparing":
nextPart = `You're actively developing the skills you need for new opportunities.`;
break;
case "enhancing":
nextPart = `You’d like to deepen or broaden your responsibilities.`;
break;
case "retirement":
nextPart = `You're considering how to transition toward retirement.`;
break;
default:
nextPart = `I'm not entirely sure of your next direction, but we’ll keep your background in mind.`;
}
const friendlyNote = `
Feel free to use AptivaAI however it best suits you—there’s no "wrong" answer.
It's really about where you want to go from here (that's all you can control anyway).
We can refine details anytime or just jump straight to what you're most interested in exploring now!`;
return `${nowPart} ${nextPart}\n${friendlyNote}`;
}
function generatePersonalizedIntro() {
/* (unchanged body) */
const careerName = scenarioRow?.career_name || "this career";
const goalsText = scenarioRow?.career_goals?.trim() || null;
const riskLevel = scenarioRow?.riskLevel;
const riskReasoning = scenarioRow?.riskReasoning;
const userSituation = userProfile?.career_situation?.toLowerCase();
const userStatus = scenarioRow?.status?.toLowerCase();
const combined = buildStatusSituationMessage(userStatus, userSituation, careerName);
const intro = `
Hi! ${combined}
${goalsText ? `Your goals include:
${goalsText.split(/^\d+\.\s+/gm).filter(Boolean).map(g => `• ${g.trim()}`).join("
")}
` : ""}
${riskLevel ? `Note: This role has a ${riskLevel} automation risk over the next 10 years. ${riskReasoning}
` : ""}
I'm here to support you with personalized coaching. What would you like to focus on today?`;
return { role: "assistant", content: intro };
}
/* ------------ shared AI caller ------------- */
async function callAi(updatedHistory, opts = {}) {
setLoading(true);
try {
if (!threadId) throw new Error('thread not ready');
const context = { userProfile, financialProfile, scenarioRow, collegeProfile };
const r = await authFetch(`/api/premium/coach/chat/threads/${threadId}/messages`, {
method:'POST',
headers:{ 'Content-Type':'application/json' },
body: JSON.stringify({ content: updatedHistory.at(-1)?.content || '', context })
});
let reply = 'Sorry, something went wrong.';
if (r.ok && (r.headers.get('content-type')||'').includes('application/json')) {
const data = await r.json();
reply = (data?.reply || '').trim() || reply;
}
setMessages(prev => [...prev, { role:'assistant', content: reply }]);
} catch (e) {
console.error(e);
setMessages(prev => [...prev, { role:'assistant', content:'Sorry, something went wrong.' }]);
} finally {
setLoading(false);
}
}
/* ------------ normal send ------------- */
function handleSubmit(e) {
e.preventDefault();
if (!input.trim() || loading) return;
const userMsg = { role: "user", content: input.trim() };
const newHistory = [...messages, userMsg];
setMessages(newHistory);
setInput("");
callAi(newHistory);
}
/* ------------ quick-action buttons ------------- */
function triggerQuickAction(type) {
if (loading) return; // debounce
/* shared context */
const careerName = scenarioRow?.career_name || "your role";
const goalsText = scenarioRow?.career_goals || "";
const skillsText = userProfile?.key_skills || "";
const avoidList =
[...messages].reverse()
.find(m => m.role === "system" && m.content.startsWith("[CURRENT MILESTONES]"))
?.content.split("\n").slice(2).join("\n") || "";
/* 1) Mock-Interview (special flow) */
if (type === "interview") {
const desc = scenarioRow?.job_description || "";
const hiddenSystem = { role:"system", content: buildInterviewPrompt(careerName, desc) };
const note = { role:"assistant", content:`Starting mock interview on **${careerName}**. Answer each question and I'll give feedback!` };
const updated = [...messages, note, hiddenSystem];
setMessages([...messages, note]);
callAi(updated);
return;
}
/* 2) All other quick actions share the same pattern */
const note = {
role : "assistant",
content: {
networking: "Sure! Let me create a Networking roadmap for you…",
jobSearch : "Sure! Let me create a Job-Search roadmap for you…",
aiGrowth : "Sure! Let’s map out how you can *partner* with AI in this career…"
}[type] || "OK!"
};
const hiddenSystem = {
role : "system",
content: QUICK_PROMPTS[type]({
careerName, goalsText, skillsText, avoidList
})
};
const updated = [...messages, note, hiddenSystem];
setMessages([...messages, note]);
const needsContext = ["networking", "jobSearch", "aiGrowth"].includes(type);
callAi(updated, {forceContext: needsContext});
}
/* ------------ render ------------- */
return (