RetirementPlanner chatbot changes for Support
This commit is contained in:
parent
5fc6d576b4
commit
6756f99c9b
@ -23,6 +23,7 @@ const FAQ_PATH = path.join(repoRoot, "backend", "user_profile.db");
|
|||||||
/* Constants ─────────────────────────────────────────────────────────────── */
|
/* 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"];
|
||||||
|
const TASKS_SENTINEL = '<<APTIVA TASK CATALOGUE>>';
|
||||||
|
|
||||||
/* Load tool manifests just once at boot ─────────────────────────────────── */
|
/* Load tool manifests just once at boot ─────────────────────────────────── */
|
||||||
const BOT_TOOLS = JSON.parse(
|
const BOT_TOOLS = JSON.parse(
|
||||||
@ -259,12 +260,19 @@ export default function chatFreeEndpoint(
|
|||||||
(Remember: you can’t click—just explain the steps.)`;
|
(Remember: you can’t click—just explain the steps.)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2) Append task catalogue so the bot can describe valid actions */
|
/* 2) Append task catalogue once per conversation ─────────── */
|
||||||
const isPremium = req.user?.plan_type === "premium";
|
const alreadySent = chatHistory.some(
|
||||||
system +=
|
m => m.role === 'system' && m.content?.includes(TASKS_SENTINEL)
|
||||||
"\n\n### What the user can do on this screen\n" +
|
);
|
||||||
taskListForPage(pageContext, isPremium) +
|
|
||||||
"\n\n(Remember: do not click for the user; only explain the steps.)";
|
if (!alreadySent) {
|
||||||
|
const isPremium = req.user?.plan_type === 'premium';
|
||||||
|
system +=
|
||||||
|
`\n\n${TASKS_SENTINEL}\n` + // ← sentinel tag
|
||||||
|
'### What the user can do on this screen\n' +
|
||||||
|
taskListForPage(pageContext, isPremium) +
|
||||||
|
'\n\n(Remember: do not click for the user; only explain the steps.)';
|
||||||
|
}
|
||||||
|
|
||||||
const modalPayload = snapshot?.modalCtx;
|
const modalPayload = snapshot?.modalCtx;
|
||||||
|
|
||||||
@ -323,6 +331,41 @@ export default function chatFreeEndpoint(
|
|||||||
|
|
||||||
system += "(Explain steps only; never click for the user.)";
|
system += "(Explain steps only; never click for the user.)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ──────────────────────────────────────────────────────────────
|
||||||
|
RetirementPlanner extras (NEW)
|
||||||
|
- front-end <RetirementPlanner> already sends `snapshot` shaped
|
||||||
|
like: {
|
||||||
|
scenarioCtx : { count, selectedId, selectedTitle },
|
||||||
|
simCtx : { years, interest }, // eg 50 | "No Interest"
|
||||||
|
chartCtx : { nestEgg } // projected value, number
|
||||||
|
}
|
||||||
|
---------------------------------------------------------------- */
|
||||||
|
if (pageContext === "RetirementPlanner" && snapshot) {
|
||||||
|
const { scenarioCtx = {}, simCtx = {}, chartCtx = {} } = snapshot;
|
||||||
|
|
||||||
|
system += "\n\n### Current context: Retirement Planner\n";
|
||||||
|
|
||||||
|
if (scenarioCtx.count != null) {
|
||||||
|
system += `Scenarios loaded : ${scenarioCtx.count}\n`;
|
||||||
|
if (scenarioCtx.selectedTitle)
|
||||||
|
system += `Active scenario : “${scenarioCtx.selectedTitle}”\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simCtx.years != null)
|
||||||
|
system += `Simulation horizon : ${simCtx.years} years\n`;
|
||||||
|
|
||||||
|
if (simCtx.interest)
|
||||||
|
system += `Interest model : ${simCtx.interest}\n`;
|
||||||
|
|
||||||
|
if (chartCtx.nestEgg != null)
|
||||||
|
system += `Projected nest-egg : $${chartCtx.nestEgg.toLocaleString()}\n`;
|
||||||
|
|
||||||
|
system +=
|
||||||
|
"(Explain steps only; never click for the user. Refer to buttons/inputs by their labels from the task catalogue.)";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── Build tool list for this request ────────────────────── */
|
/* ── Build tool list for this request ────────────────────── */
|
||||||
const tools = [...SUPPORT_TOOLS]; // guidance mode → support-only;
|
const tools = [...SUPPORT_TOOLS]; // guidance mode → support-only;
|
||||||
let messages = [
|
let messages = [
|
||||||
|
22
src/App.js
22
src/App.js
@ -44,6 +44,9 @@ function App() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pageContext, snapshot: routeSnapshot } = usePageContext();
|
const { pageContext, snapshot: routeSnapshot } = usePageContext();
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [drawerPane, setDrawerPane] = useState('support');
|
||||||
|
const [retireProps, setRetireProps] = useState(null);
|
||||||
|
|
||||||
/* ------------------------------------------
|
/* ------------------------------------------
|
||||||
ChatDrawer – route-aware tool handlers
|
ChatDrawer – route-aware tool handlers
|
||||||
@ -215,7 +218,13 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
scenario, setScenario,
|
scenario, setScenario,
|
||||||
user, }}
|
user, }}
|
||||||
>
|
>
|
||||||
<ChatCtx.Provider value={{ setChatSnapshot }}>
|
<ChatCtx.Provider value={{ setChatSnapshot,
|
||||||
|
openSupport: () => { setDrawerPane('support'); setDrawerOpen(true); },
|
||||||
|
openRetire : (props) => {
|
||||||
|
setRetireProps(props); // { scenario, financialProfile, onScenarioPatch }
|
||||||
|
setDrawerPane('retire');
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}}>
|
||||||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
||||||
@ -555,9 +564,16 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<ChatDrawer
|
<ChatDrawer
|
||||||
|
open={drawerOpen}
|
||||||
|
onOpenChange={setDrawerOpen}
|
||||||
|
|
||||||
|
pane={drawerPane}
|
||||||
|
setPane={setDrawerPane}
|
||||||
|
|
||||||
|
retireProps={retireProps}
|
||||||
|
|
||||||
pageContext={pageContext}
|
pageContext={pageContext}
|
||||||
title="Help & Support"
|
snapshot={chatSnapshot}
|
||||||
snapshot={chatSnapshot}
|
|
||||||
uiToolHandlers={uiToolHandlers}
|
uiToolHandlers={uiToolHandlers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -79,5 +79,17 @@
|
|||||||
{ "id": "CR-FTR8", "label": "Max Return (%) input (Random mode)" },
|
{ "id": "CR-FTR8", "label": "Max Return (%) input (Random mode)" },
|
||||||
{ "id": "CR-FTR9", "label": "Interest bias dropdown" },
|
{ "id": "CR-FTR9", "label": "Interest bias dropdown" },
|
||||||
{ "id": "CR-FTR10","label": "Annual base $ input" }
|
{ "id": "CR-FTR10","label": "Annual base $ input" }
|
||||||
|
],
|
||||||
|
"RetirementPlanner": [
|
||||||
|
{ "id": "RP-01", "label": "+ Add Scenario button" },
|
||||||
|
{ "id": "RP-02", "label": "Scenario selector dropdown (title bar)" },
|
||||||
|
{ "id": "RP-03", "label": "Simulation Years (yrs) input" },
|
||||||
|
{ "id": "RP-04", "label": "Apply Interest dropdown" },
|
||||||
|
{ "id": "RP-05", "label": "Nest-egg chart (projection graph)" },
|
||||||
|
{ "id": "RP-06", "label": "Milestones button" },
|
||||||
|
{ "id": "RP-07", "label": "Edit button (card footer)" },
|
||||||
|
{ "id": "RP-08", "label": "Clone button (card footer)" },
|
||||||
|
{ "id": "RP-09", "label": "Delete button (card footer)" },
|
||||||
|
{ "id": "RP-10", "label": "Retirement Helper chat icon / rail" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,172 +1,224 @@
|
|||||||
// ────────────────────────────────── ChatDrawer.jsx
|
// ───────────────────────── 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 { Button } from "./ui/button.js";
|
import { cn } from '../utils/cn.js';
|
||||||
import { Input } from "./ui/input.js";
|
import { Button } from './ui/button.js';
|
||||||
import { MessageCircle } from "lucide-react";
|
import { Input } from './ui/input.js';
|
||||||
|
import { MessageCircle } from 'lucide-react';
|
||||||
|
import RetirementChatBar from './RetirementChatBar.js';
|
||||||
|
|
||||||
/* ---------------------------------------------------------------
|
/* ------------------------------------------------------------------ */
|
||||||
Streams from /api/chat/free and executes UI-tool callbacks
|
/* ChatDrawer
|
||||||
----------------------------------------------------------------*/
|
– support-bot lives in this file (streamed from /api/chat/free)
|
||||||
|
– retirement helper is just a passthrough to <RetirementChatBar>
|
||||||
|
------------------------------------------------------------------ */
|
||||||
export default function ChatDrawer({
|
export default function ChatDrawer({
|
||||||
pageContext = "Home",
|
/* ─ props from App.js ─ */
|
||||||
snapshot = null,
|
pageContext = 'Home',
|
||||||
|
snapshot = null,
|
||||||
|
|
||||||
|
open: controlledOpen = false,
|
||||||
|
onOpenChange,
|
||||||
|
pane: controlledPane = 'support',
|
||||||
|
setPane: setControlledPane,
|
||||||
|
retireProps = null, // { scenario, financialProfile, … }
|
||||||
}) {
|
}) {
|
||||||
/* state */
|
/* ─────────────────────────── internal / fallback state ───────── */
|
||||||
const [open, setOpen] = useState(false);
|
const [openLocal, setOpenLocal] = useState(false);
|
||||||
const [prompt, setPrompt] = useState("");
|
const [paneLocal, setPaneLocal] = useState('support');
|
||||||
|
|
||||||
|
/* prefer the controlled props when supplied */
|
||||||
|
const open = controlledOpen ?? openLocal;
|
||||||
|
const setOpen = onOpenChange ?? setOpenLocal;
|
||||||
|
const pane = controlledPane ?? paneLocal;
|
||||||
|
const setPane = setControlledPane ?? setPaneLocal;
|
||||||
|
|
||||||
|
/* ────────────────── free-tier support-bot state ─────────────── */
|
||||||
|
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 */
|
/* auto-scroll on incoming messages */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listRef.current &&
|
listRef.current &&
|
||||||
(listRef.current.scrollTop = listRef.current.scrollHeight);
|
(listRef.current.scrollTop = listRef.current.scrollHeight);
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
/* helper: stream-friendly append */
|
/* helper: merge chunks while streaming */
|
||||||
const pushAssistant = (chunk) =>
|
const pushAssistant = (chunk) =>
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const last = prev.at(-1);
|
const last = prev.at(-1);
|
||||||
if (last?.role === "assistant") {
|
if (last?.role === 'assistant') {
|
||||||
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;
|
||||||
}
|
}
|
||||||
return [...prev, { role: "assistant", content: chunk }];
|
return [...prev, { role: 'assistant', content: chunk }];
|
||||||
});
|
});
|
||||||
|
|
||||||
/* send prompt */
|
/* ───────────────────────── send support-bot prompt ───────────── */
|
||||||
async function sendPrompt() {
|
async function sendPrompt() {
|
||||||
const text = prompt.trim();
|
const text = prompt.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
setMessages((m) => [...m, { role: "user", content: text }]);
|
setMessages((m) => [...m, { role: 'user', content: text }]);
|
||||||
setPrompt("");
|
setPrompt('');
|
||||||
|
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
prompt: text,
|
prompt: text,
|
||||||
pageContext,
|
pageContext,
|
||||||
chatHistory: messages,
|
chatHistory: messages,
|
||||||
snapshot
|
snapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("token") || "";
|
const token = localStorage.getItem('token') || '';
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
Accept : "text/event-stream",
|
Accept: 'text/event-stream',
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const resp = await fetch("/api/chat/free", {
|
const resp = await fetch('/api/chat/free', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body
|
body,
|
||||||
});
|
});
|
||||||
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
|
||||||
const reader = resp.body.getReader();
|
const reader = resp.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buf = "";
|
let buf = '';
|
||||||
|
|
||||||
/* ─────────────── STREAM LOOP ─────────────── */
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
|
/* eslint-enable no-await-in-loop */
|
||||||
if (done) break;
|
if (done) break;
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
|
|
||||||
buf += decoder.decode(value, { stream: true });
|
buf += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
let nl;
|
let nl;
|
||||||
while ((nl = buf.indexOf("\n")) !== -1) {
|
while ((nl = buf.indexOf('\n')) !== -1) {
|
||||||
const line = buf.slice(0, nl).trim(); // one full line
|
const line = buf.slice(0, nl).trim();
|
||||||
buf = buf.slice(nl + 1); // keep remainder
|
buf = buf.slice(nl + 1);
|
||||||
|
if (line) pushAssistant(line + '\n');
|
||||||
/* 2️⃣ normal assistant text */
|
|
||||||
if (line) pushAssistant(line + "\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ───────── END STREAM LOOP ───────── */
|
if (buf.trim()) pushAssistant(buf);
|
||||||
|
|
||||||
if (buf.trim()) pushAssistant(buf);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ChatDrawer] stream error", err);
|
console.error('[ChatDrawer] stream error', err);
|
||||||
pushAssistant("Sorry — something went wrong. Please try again later.");
|
pushAssistant(
|
||||||
|
'Sorry — something went wrong. Please try again later.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enter submits */
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPrompt();
|
sendPrompt();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* UI */
|
/* ──────────────────────────── UI ─────────────────────────────── */
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
{/* floating button */}
|
{/* floating FAB */}
|
||||||
<SheetTrigger>
|
<SheetTrigger asChild>
|
||||||
<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"
|
||||||
|
onClick={() => onOpenChange(!open)}
|
||||||
>
|
>
|
||||||
<MessageCircle size={24} />
|
<MessageCircle size={24} />
|
||||||
</button>
|
</button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
|
|
||||||
{/* drawer */}
|
{/* side-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]"
|
||||||
>
|
>
|
||||||
<div className="sticky top-0 z-10 border-b bg-white px-4 py-3 font-semibold">
|
{/* header – tab switch */}
|
||||||
{pageContext}
|
<div className="flex border-b text-sm font-semibold">
|
||||||
</div>
|
{[
|
||||||
|
{ id: 'support', label: 'Aptiva Support' },
|
||||||
{/* transcript */}
|
{ id: 'retire', label: 'Retirement Helper' },
|
||||||
<div
|
].map((tab) => (
|
||||||
ref={listRef}
|
<button
|
||||||
className="flex-1 space-y-4 overflow-y-auto px-4 py-2 text-sm"
|
key={tab.id}
|
||||||
>
|
onClick={() => setPane(tab.id)}
|
||||||
{messages.map((m, i) => (
|
className={cn(
|
||||||
<div
|
'flex-1 py-2',
|
||||||
key={i}
|
pane === tab.id
|
||||||
className={
|
? 'border-b-2 border-blue-600'
|
||||||
m.role === "user" ? "text-right" : "text-left text-gray-800"
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
{m.content}
|
{tab.label}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* prompt box */}
|
{/* body – conditional panes */}
|
||||||
<div className="border-t p-4">
|
{pane === 'support' ? (
|
||||||
<form
|
/* ─────────── Support bot ─────────── */
|
||||||
onSubmit={(e) => {
|
<>
|
||||||
e.preventDefault();
|
<div
|
||||||
sendPrompt();
|
ref={listRef}
|
||||||
}}
|
className="flex-1 space-y-4 overflow-y-auto px-4 py-2 text-sm"
|
||||||
className="flex gap-2"
|
>
|
||||||
>
|
{messages.map((m, i) => (
|
||||||
<Input
|
<div
|
||||||
value={prompt}
|
/* eslint-disable react/no-array-index-key */
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
key={i}
|
||||||
onKeyDown={handleKeyDown}
|
/* eslint-enable react/no-array-index-key */
|
||||||
placeholder="Ask me anything…"
|
className={
|
||||||
className="flex-1"
|
m.role === 'user'
|
||||||
/>
|
? 'text-right'
|
||||||
<Button type="submit" disabled={!prompt.trim()}>
|
: 'text-left text-gray-800'
|
||||||
Send
|
}
|
||||||
</Button>
|
>
|
||||||
</form>
|
{m.content}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t p-4">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPrompt();
|
||||||
|
}}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={prompt}
|
||||||
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Ask me anything…"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button type="submit" disabled={!prompt.trim()}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : retireProps ? (
|
||||||
|
/* ───────── Retirement helper ─────── */
|
||||||
|
<RetirementChatBar {...retireProps} />
|
||||||
|
) : (
|
||||||
|
<div className="m-auto px-6 text-center text-sm text-gray-400">
|
||||||
|
Select a scenario in
|
||||||
|
<strong>Retirement Planner</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// src/components/RetirementPlanner.js
|
// src/components/RetirementPlanner.js
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useCallback, useContext } from 'react';
|
||||||
import authFetch from '../utils/authFetch.js';
|
import authFetch from '../utils/authFetch.js';
|
||||||
import ScenarioContainer from './ScenarioContainer.js';
|
import ScenarioContainer from './ScenarioContainer.js';
|
||||||
import { Button } from './ui/button.js';
|
import { Button } from './ui/button.js';
|
||||||
import RetirementChatBar from './RetirementChatBar.js';
|
import RetirementChatBar from './RetirementChatBar.js';
|
||||||
import ScenarioDiffDrawer from './ScenarioDiffDrawer.js';
|
import ScenarioDiffDrawer from './ScenarioDiffDrawer.js';
|
||||||
|
import ChatCtx from '../contexts/ChatCtx.js';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
* tiny class‑name helper
|
* tiny class‑name helper
|
||||||
@ -39,6 +40,7 @@ export default function RetirementPlanner () {
|
|||||||
const [diff, setDiff] = useState(null);
|
const [diff, setDiff] = useState(null);
|
||||||
const [simYearsMap, setSimYearsMap] = useState({});
|
const [simYearsMap, setSimYearsMap] = useState({});
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { openRetire } = useContext(ChatCtx);
|
||||||
|
|
||||||
/* ----------------------- data loading -------------------------- */
|
/* ----------------------- data loading -------------------------- */
|
||||||
const loadAll = useCallback(async () => {
|
const loadAll = useCallback(async () => {
|
||||||
@ -141,7 +143,14 @@ export default function RetirementPlanner () {
|
|||||||
baselineYears={baselineYears}
|
baselineYears={baselineYears}
|
||||||
onClone={handleCloneScenario}
|
onClone={handleCloneScenario}
|
||||||
onRemove={handleRemoveScenario}
|
onRemove={handleRemoveScenario}
|
||||||
onSelect={() => { setSelectedScenario(sc); if (isMobile) setChatOpen(true); }}
|
onSelect={() => {
|
||||||
|
setSelectedScenario(sc);
|
||||||
|
openRetire({
|
||||||
|
scenario: sc,
|
||||||
|
financialProfile,
|
||||||
|
onScenarioPatch: applyPatch
|
||||||
|
});
|
||||||
|
}}
|
||||||
onSimDone={(id, yrs) => {
|
onSimDone={(id, yrs) => {
|
||||||
setSimYearsMap(prev => ({ ...prev, [id]: yrs }));
|
setSimYearsMap(prev => ({ ...prev, [id]: yrs }));
|
||||||
}}
|
}}
|
||||||
@ -150,50 +159,16 @@ export default function RetirementPlanner () {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* ================= CHAT RAIL ============================ */}
|
|
||||||
<aside
|
|
||||||
className={cn(
|
|
||||||
'fixed md:static top-0 right-0 h-full bg-white border-l shadow-lg',
|
|
||||||
'transition-transform duration-300',
|
|
||||||
'w-11/12 max-w-xs md:w-[340px] z-30',
|
|
||||||
chatOpen ? 'translate-x-0' : 'translate-x-full md:translate-x-0'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{selectedScenario ? (
|
|
||||||
<RetirementChatBar
|
|
||||||
scenario={selectedScenario}
|
|
||||||
financialProfile={financialProfile}
|
|
||||||
onScenarioPatch={applyPatch}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full flex items-center justify-center text-gray-400 text-sm p-4">
|
|
||||||
Select a scenario to chat ☝️
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* ================= MOBILE FABS ========================== */}
|
{/* ================= MOBILE FABS ========================== */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<>
|
<button
|
||||||
{/* chat toggle */}
|
onClick={handleAddScenario}
|
||||||
<button
|
className="fixed bottom-4 right-4 rounded-full bg-green-600 p-4 text-white text-xl shadow-md z-40"
|
||||||
onClick={() => setChatOpen(o => !o)}
|
aria-label="Add scenario"
|
||||||
className="fixed bottom-20 right-4 rounded-full bg-blue-600 p-3 text-white shadow-md z-40"
|
>
|
||||||
aria-label="Toggle chat"
|
+
|
||||||
>
|
</button>
|
||||||
💬
|
)}
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* add scenario */}
|
|
||||||
<button
|
|
||||||
onClick={handleAddScenario}
|
|
||||||
className="fixed bottom-4 right-4 rounded-full bg-green-600 p-4 text-white text-xl shadow-md z-40"
|
|
||||||
aria-label="Add scenario"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ================= DIFF DRAWER ========================== */}
|
{/* ================= DIFF DRAWER ========================== */}
|
||||||
{diff && (
|
{diff && (
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
// src/contexts/ChatCtx.js
|
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
const ChatCtx = createContext({ setChatSnapshot: () => {} });
|
const ChatCtx = createContext({
|
||||||
export default ChatCtx;
|
/* already used by Support bot */
|
||||||
|
setChatSnapshot: () => {},
|
||||||
|
|
||||||
|
/* NEW helpers — App.js will supply real fns */
|
||||||
|
openSupport: () => {}, // open drawer on Support pane
|
||||||
|
openRetire : () => {} // open drawer on Retirement Helper pane
|
||||||
|
});
|
||||||
|
export default ChatCtx;
|
Loading…
Reference in New Issue
Block a user