Stop the spending pie chart from flickering while typing in the advisor

The follow-up input's value lived on Dashboard, so every keystroke
re-rendered the entire page. Recharts replays its label animation on
each render, which is why the indicator lines and category labels
disappeared until typing settled. Extracts the form into a child
component that owns its own input state so keystrokes no longer escape
to the parent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 14:41:20 -07:00
parent 4939122cf2
commit 0cbc98c82b
+44 -25
View File
@@ -27,6 +27,46 @@ function formatCurrency(value: number) {
return value < 0 ? `-$${abs}` : `$${abs}`;
}
// Owns its own input state so keystrokes don't re-render the parent Dashboard
// — that was reflowing the recharts pie/bar charts and making their labels
// flicker out for ~5s of animation replay on every character typed.
function AdvisorFollowUpForm({
disabled,
onSubmit,
}: {
disabled: boolean;
onSubmit: (value: string) => Promise<void>;
}) {
const [value, setValue] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const trimmed = value.trim();
if (!trimmed || disabled) return;
setValue('');
await onSubmit(trimmed);
};
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Ask a follow-up..."
disabled={disabled}
/>
<Button
type="submit"
size="sm"
disabled={!value.trim() || disabled}
aria-label="Send"
>
<Send className="size-4" />
</Button>
</form>
);
}
type RangeKey = 'this-month' | 'last-30' | 'last-90' | 'ytd';
const RANGE_LABELS: Record<RangeKey, string> = {
@@ -78,7 +118,6 @@ export function Dashboard() {
sendMessage,
resetConversation,
} = useAdvisorStore();
const [followUp, setFollowUp] = useState('');
const chatScrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -87,14 +126,6 @@ export function Dashboard() {
}
}, [advisorMessages]);
const handleSendFollowUp = async (e: React.FormEvent) => {
e.preventDefault();
const trimmed = followUp.trim();
if (!trimmed || advisorLoading) return;
setFollowUp('');
await sendMessage(trimmed);
};
const handleRestart = async () => {
resetConversation();
await startConversation();
@@ -415,22 +446,10 @@ export function Dashboard() {
</div>
)}
</div>
<form onSubmit={handleSendFollowUp} className="flex gap-2">
<Input
value={followUp}
onChange={(e) => setFollowUp(e.target.value)}
placeholder="Ask a follow-up..."
disabled={advisorLoading}
/>
<Button
type="submit"
size="sm"
disabled={!followUp.trim() || advisorLoading}
aria-label="Send"
>
<Send className="size-4" />
</Button>
</form>
<AdvisorFollowUpForm
disabled={advisorLoading}
onSubmit={sendMessage}
/>
</div>
)}
</CardContent>