diff --git a/src/App.js b/src/App.js index 9ff3241..411176d 100644 --- a/src/App.js +++ b/src/App.js @@ -28,7 +28,7 @@ import FinancialProfileForm from './components/FinancialProfileForm.js'; import CareerRoadmap from './components/CareerRoadmap.js'; import Paywall from './components/Paywall.js'; import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js'; -import MultiScenarioView from './components/MultiScenarioView.js'; +import RetirementPlanner from './components/RetirementPlanner.js'; import ResumeRewrite from './components/ResumeRewrite.js'; function App() { @@ -53,7 +53,7 @@ function App() { '/career-roadmap', '/paywall', '/financial-profile', - '/multi-scenario', + '/retirement-planner', '/premium-onboarding', '/enhancing', '/retirement', @@ -446,10 +446,10 @@ function App() { } /> - + } /> diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index 3f4a793..e486315 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -28,6 +28,7 @@ import { simulateFinancialProjection } from '../utils/FinancialProjectionService import parseFloatOrZero from '../utils/ParseFloatorZero.js'; import { getFullStateName } from '../utils/stateUtils.js'; import CareerCoach from "./CareerCoach.js"; + import { Button } from './ui/button.js'; import { Pencil } from 'lucide-react'; import ScenarioEditModal from './ScenarioEditModal.js'; diff --git a/src/components/RetirementChatBar.js b/src/components/RetirementChatBar.js new file mode 100644 index 0000000..134d083 --- /dev/null +++ b/src/components/RetirementChatBar.js @@ -0,0 +1,244 @@ +// src/components/RetirementChatBar.js +import React, { useEffect, useRef, useState } from 'react'; +import { cn } from '../utils/cn.js'; +import { Button } from './ui/button.js'; +import authFetch from '../utils/authFetch.js'; + +/* ────────────────────────────────────────────────────────────── + 1. Static “OPS cheat-sheet” card + - sent exactly once, identified by the sentinel below + - keep the FULL text here (truncated for brevity) + ────────────────────────────────────────────────────────────*/ +const OPS_SENTINEL = 'APTIVA OPS CHEAT-SHEET'; // do NOT change +const STATIC_SYSTEM_CARD = ` +${OPS_SENTINEL} + +──────────────────────────────────────────────────────── +🛠 Allowed OPS JSON +──────────────────────────────────────────────────────── +• CREATE / UPDATE / DELETE milestones, tasks, impacts +• Always wrap ops payload in a \`\`\`ops ... \`\`\` fence +• One short confirmation line → then the fence → nothing after +(…include your complete text here…) +`.trim(); + +/* Other global helpers */ +const CTX_SENTINEL = '[DETAILED USER PROFILE]'; +const MAX_TURNS = 6; + +/* ------------------------------------------------------------------ helpers */ +function buildStatusSituationCard(userProfile = {}, scenarioRow = {}) { + const careerName = scenarioRow.career_name || 'your chosen career'; + const status = scenarioRow.status || 'planned'; + const situation = userProfile.career_situation || 'exploring'; + + return `[STATUS] +Currently **${status}**, you are ${situation} ${careerName}.`; +} + +function buildMilestoneGridCard(grid = []) { + if (!grid.length) return '[CURRENT MILESTONES]\n- none -'; + const rows = grid.map(r => `${r.id}|${r.title.trim()}|${r.date}`).join('\n'); + return `[CURRENT MILESTONES] +(id | title | date) +${rows}`; +} + +/* Build the outbound message array each turn */ +function buildMessages({ + chatHistory = [], + userProfile = {}, + scenarioRow = {}, + milestoneGrid = [], + largeSummaryCard = '', + forceContext = false +}) { + const safeHistory = chatHistory.map(m => + m.content != null ? m : { ...m, content: m.text ?? '' } + ); + + /* Send static card once per convo */ + const needOpsCard = !safeHistory.some( + m => m.role === 'system' && m.content?.includes(OPS_SENTINEL) + ); + const staticCards = needOpsCard + ? [{ role: 'system', content: STATIC_SYSTEM_CARD }] + : []; + + /* Send big context when missing or forced */ + const needCtxCard = + forceContext || + !chatHistory.some( + m => m.role === 'system' && m.content?.startsWith(CTX_SENTINEL) + ); + const contextCards = + needCtxCard && largeSummaryCard + ? [{ role: 'system', content: `${CTX_SENTINEL}\n${largeSummaryCard}` }] + : []; + + /* Small per-turn helpers */ + const smallCards = [ + { role: 'system', content: buildStatusSituationCard(userProfile, scenarioRow) }, + { role: 'system', content: buildMilestoneGridCard(milestoneGrid) } + ]; + + const recent = safeHistory.slice(-MAX_TURNS); + return [...staticCards, ...contextCards, ...smallCards, ...recent] + .map(m => ({ ...m, content: m.content ?? '' })); +} + +/* ------------------------------------------------------------------ component */ +export default function RetirementChatBar({ + scenario = null, // full scenario row + userProfile = {}, // caller supplies or {} + financialProfile = {}, + collegeProfile = {}, + milestoneGrid = [], + onScenarioPatch, + className = '' +}) { + const [chatHistory, setChatHistory] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [forceCtx, setForceCtx] = useState(false); + const bottomRef = useRef(null); + + /* wipe chat on scenario change */ + useEffect(() => setChatHistory([]), [scenario?.id]); + + /* autoscroll */ + useEffect(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), + [chatHistory]); + + /* ------------------------------------------------------------------ */ +/* SEND PROMPT — guarded diff + safe input-handling */ +/* ------------------------------------------------------------------ */ +async function sendPrompt() { + const prompt = input.trim(); + if (!prompt || !scenario?.id) return; + + /* ① optimistic UI – show the user bubble immediately */ + const userMsg = { role: 'user', content: prompt }; + setChatHistory(h => [...h, userMsg]); + setInput(''); + setLoading(true); + + try { + /* ② rebuild outbound history (adds system helper cards) */ + const messagesToSend = buildMessages({ + chatHistory : [...chatHistory, userMsg], + userProfile, + scenarioRow : scenario, + milestoneGrid, + largeSummaryCard: window.CACHED_SUMMARY || '', + forceContext : forceCtx + }); + + /* ③ POST to the retirement endpoint */ + const res = await authFetch('/api/premium/retirement/aichat', { + method : 'POST', + headers: { 'Content-Type': 'application/json' }, + body : JSON.stringify({ + prompt, + scenario_id : scenario.id, // ← keep it minimal + chatHistory : messagesToSend // ← backend needs this to find userMsg + }) + }); + + const data = await res.json(); + const assistantReply = data.reply || '(no response)'; + setChatHistory(h => [...h, { role: 'assistant', content: assistantReply }]); + + /* ④ if we got a real patch, build + forward the diff array */ + if (data.scenarioPatch && onScenarioPatch) { + onScenarioPatch(scenario.id, data.scenarioPatch); // ✅ id + patch + } + } catch (err) { + console.error(err); + setChatHistory(h => [ + ...h, + { role: 'assistant', content: '⚠️ Server error.' } + ]); + } finally { + setLoading(false); + setForceCtx(false); + } +} + + +/* enter-to-send */ +function handleKeyUp(e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendPrompt(); + } +} + + + + /* ----------------------- render ----------------------- */ + const scenarioLabel = scenario + ? (scenario.scenario_title || scenario.career_name || 'Untitled Scenario') + : 'Select a scenario'; + + return ( +