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 (
+
+ );
+}
diff --git a/src/components/RetirementLanding.js b/src/components/RetirementLanding.js
index d0794bc..0857445 100644
--- a/src/components/RetirementLanding.js
+++ b/src/components/RetirementLanding.js
@@ -16,7 +16,7 @@ function RetirementLanding() {
-
+
diff --git a/src/components/MultiScenarioView.css b/src/components/RetirementPlanner.css
similarity index 100%
rename from src/components/MultiScenarioView.css
rename to src/components/RetirementPlanner.css
diff --git a/src/components/MultiScenarioView.js b/src/components/RetirementPlanner.js
similarity index 84%
rename from src/components/MultiScenarioView.js
rename to src/components/RetirementPlanner.js
index 6a26c60..ce73276 100644
--- a/src/components/MultiScenarioView.js
+++ b/src/components/RetirementPlanner.js
@@ -1,14 +1,29 @@
-// src/components/MultiScenarioView.js
+// src/components/RetirementPlanner.js
import React, { useEffect, useState } from 'react';
import authFetch from '../utils/authFetch.js';
import ScenarioContainer from './ScenarioContainer.js';
import { Button } from './ui/button.js';
+import RetirementChatBar from './RetirementChatBar.js';
+import ScenarioDiffDrawer from './ScenarioDiffDrawer.js';
-export default function MultiScenarioView() {
+export default function RetirementPlanner() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [financialProfile, setFinancialProfile] = useState(null);
const [scenarios, setScenarios] = useState([]);
+ const [diff , setDiff ] = useState(null);
+ const [chatId, setChatId] = useState(null);
+ const [selectedScenario, setSelectedScenario] = useState(null);
+
+
+ const applyPatch = (id, patch) => {
+ setScenarios(prev => {
+ const base = prev.find(s => s.id === id);
+ const next = prev.map(s => (s.id === id ? { ...s, ...patch } : s));
+ setDiff({ base, patch });
+ return next;
+ });
+};
useEffect(() => {
loadScenariosAndFinancial();
@@ -29,7 +44,7 @@ export default function MultiScenarioView() {
setFinancialProfile(finData);
setScenarios(scenData.careerProfiles || []);
} catch (err) {
- console.error('MultiScenarioView =>', err);
+ console.error('RetirementPlanner =>', err);
setError(err.message || 'Failed to load');
} finally {
setLoading(false);
@@ -290,28 +305,51 @@ export default function MultiScenarioView() {
}
}
- if (loading) return Loading scenarios...
;
- if (error) return {error}
;
-
const visible = scenarios.slice(0, 2);
- return (
-
-
+ if (loading) return
Loading scenarios…
;
+ if (error) return
{error}
;
-
- {visible.map(sc => (
-
- ))}
+
+ return (
+
+ {/* main column */}
+
+
+
+
+ {visible.map(sc => (
+ setSelectedScenario(sc)}
+ />
+ ))}
+
+
+ {/* right rail */}
+
+
+ {/* diff drawer */}
+ {!!diff && (
+
setDiff(null)}
+ />
+ )}
);
}
diff --git a/src/components/ScenarioContainer.js b/src/components/ScenarioContainer.js
index 8c2d14e..6331c6d 100644
--- a/src/components/ScenarioContainer.js
+++ b/src/components/ScenarioContainer.js
@@ -17,7 +17,8 @@ export default function ScenarioContainer({
scenario,
financialProfile,
onRemove,
- onClone
+ onClone,
+ onSelect
}) {
// -------------------------------------------------------------
// 1) States
@@ -846,7 +847,11 @@ export default function ScenarioContainer({
// 10) Render
// -------------------------------------------------------------
return (
-
+
onSelect(localScenario.id)}
+ className="w-[420px] border border-gray-300 p-4 rounded cursor-pointer
+ hover:shadow-sm transition-shadow bg-white"
+ >
)}
-
+
);
}
diff --git a/src/components/ScenarioDiffDrawer.js b/src/components/ScenarioDiffDrawer.js
new file mode 100644
index 0000000..cbb44a5
--- /dev/null
+++ b/src/components/ScenarioDiffDrawer.js
@@ -0,0 +1,36 @@
+import React from "react";
+import { Button } from "./ui/button.js";
+
+export default function ScenarioDiffDrawer({ base, patch, onClose }) {
+ if (!patch) return null;
+ const keys = Object.keys(patch);
+
+ return (
+
+
Scenario changes
+
+
+
+
+ Field |
+ Before |
+ After |
+
+
+
+ {keys.map(k => (
+
+ {k} |
+ {String(base[k] ?? "—")} |
+ {String(patch[k])} |
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/utils/opsEngine.js b/src/utils/opsEngine.js
new file mode 100644
index 0000000..bec2d9d
--- /dev/null
+++ b/src/utils/opsEngine.js
@@ -0,0 +1,49 @@
+export async function applyOps(opsObj, { req, userId, scenarioId }) {
+ if (!Array.isArray(opsObj?.milestones)) return [];
+
+ const apiBase = process.env.APTIVA_INTERNAL_API || 'http://localhost:5002/api';
+ const auth = (p, o = {}) => internalFetch(req, `${apiBase}${p}`, o);
+
+ const confirmations = [];
+
+ for (const m of opsObj.milestones) {
+ const op = (m?.op || '').toUpperCase();
+
+ /* ---------- DELETE ---------- */
+ if (op === 'DELETE' && m.id) {
+ const cleanId = m.id.trim();
+ const r = await auth(`/premium/milestones/${cleanId}`, { method: 'DELETE' });
+ if (r.ok) confirmations.push(`Deleted milestone ${cleanId}`);
+ continue;
+ }
+
+ /* ---------- UPDATE ---------- */
+ if (op === 'UPDATE' && m.id && m.patch) {
+ const r = await auth(`/premium/milestones/${m.id}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(m.patch),
+ });
+ if (r.ok) confirmations.push(`Updated milestone ${m.id}`);
+ continue;
+ }
+
+ /* ---------- CREATE ---------- */
+ if (op === 'CREATE' && m.data) {
+ // inject career_profile_id if the bot forgot it
+ m.data.career_profile_id = m.data.career_profile_id || scenarioId;
+ const r = await auth('/premium/milestone', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(m.data),
+ });
+ if (r.ok) {
+ const j = await r.json();
+ const newId = Array.isArray(j) ? j[0]?.id : j.id;
+ confirmations.push(`Created milestone ${newId || '(new)'}`);
+ }
+ }
+ }
+
+ return confirmations;
+}