I Made Hindsight's Memory Visible in the UI. Here's How
By Saqeef Sanan
The first version of our strategy screen gave a good answer, and nobody believed it.
It said "post a technical carousel about agent memory." Reasonable. But it read like something any chatbot would say, and there was no way to tell whether the agent had remembered anything or just guessed well. I ended up spending most of my frontend time on that one problem: making memory something you can see.
What the system does
We built a content strategist for brands. It remembers what a brand published, how each post performed, what the audience asked for in comments, and what the team decided last time. When someone asks "what should we post next week?", it recalls that history and answers with a recommendation and the evidence behind it.
Memory lives in Hindsight. A Node/Express API sits between my React + TypeScript + Tailwind frontend and three things: Hindsight, an LLM, and a small application database. Everything I render comes from one contract:
export interface StrategyResponse {
recommendation: string;
reasoning: string[]; // each line traceable to a recalled memory
format: string;
memoriesUsed: number; // how many memories recall returned
}
I locked this shape with the backend on day one. It let me build all seven pages against mock JSON while the memory layer was still being written.
The through-line: proof of memory is a UI problem
Agent memory is invisible by default. The model's answer looks the same whether it was grounded in 17 recalled facts or in nothing. If the interface doesn't show the difference, the feature doesn't exist for the user.
So every answer carries its evidence. The recommendation card renders the reasoning[] list as a "Why?" block and always shows the memory count in the footer:
export function StrategyCard({ r }: { r: StrategyResponse }) {
const cold = r.memoriesUsed === 0;
return (
<article className={cold ? 'border-slate-300 bg-slate-50' : 'border-accent bg-white'}>
<h3>{r.recommendation}</h3>
<p className="text-muted">{r.format}</p>
{cold
? <p>No memory yet. This is a generic answer.</p>
: <WhyList items={r.reasoning} />}
<footer>{r.memoriesUsed} memories used</footer>
</article>
);
}
The cold branch matters more than it looks. When memoriesUsed is zero, the card goes grey and says so. That single state is what makes the before/after contrast obvious inside the same screen.
Making the memory browsable
The second piece is a Memory Explorer. Hindsight stores facts, entities and relationships, but users think in categories: brand voice, audience, content history, experiments, gaps, decisions. The API returns a tree in that shape and I render it with one recursive component:
function Node({ n, onPick }: { n: MemoryNode; onPick: (n: MemoryNode) => void }) {
const [open, setOpen] = useState(false);
return (
<li>
<button onClick={() => (n.children ? setOpen(!open) : onPick(n))}>{n.label}</button>
{open && <ul>{n.children?.map(c => <Node key={c.id} n={c} onPick={onPick} />)}</ul>}
</li>
);
}
I didn't need a tree library. A detail panel on the right shows whatever leaf you click, such as "Technical explainers → strong saves." Next to it sits a vertical timeline, January to today, ending on the current recommendation. That timeline is what shows the knowledge is cumulative and not a snapshot.
What it looks like in use
Same question, same brand, asked twice.
Before any history is retained:
You could post about: AI trends, productivity, AI tools, industry news. (0 memories used)
After the brand's history is retained:
Why Most AI Agents Forget Everything, LinkedIn technical carousel
- Previous technical posts received stronger saves
- Audience repeatedly asked about agent memory
- "AI tools" was already covered twice this month
- Calendar has a gap around AI architecture
17 memories used
Then I retain one new comment, "we need more practical examples," and ask again. The recommendation changes to a carousel built around a real implementation example. I built a dedicated Before/After screen that shows the two columns side by side, because I got tired of explaining the difference out loud.
Lessons
-
Build against mocks first. A shared response type plus a
mocks/folder meant I was never blocked on the backend or on Hindsight integration. - Put the evidence next to the claim. A recommendation with no visible reasons looks the same as a hallucination.
- Design the empty state. "No memory yet" is a feature. It tells the truth and sets up the contrast.
-
Keep API calls out of components. Every request lives in
src/api/, typed with the shared interfaces. When the contract changed once, I fixed it in one file.
A limitation I haven't solved
The memory count measures quantity, not relevance. Seventeen loosely related memories render exactly like seventeen good ones. The next thing I want is a per-memory relevance signal from recall, so the "Why?" block can show which memories mattered most instead of just how many there were.
Top comments (0)