It's Never Just a Re-Render
Knowledge cards now render inline below the chat answer; reusing the card component takes a named export, unique per-panel ids, and its own expand state.
TL;DR
Moving knowledge cards inline into the chat answer panel sounded trivial but broke two hidden contracts. SourceCard became a named export, and card ids got a prefix to avoid duplicate ids across the document. Expanded state now lives locally per panel, keyed by turn and slug, so opening a card in one panel doesn't leak into the other.
The chat answer in the center panel showed up clean, explanation and all. But when I looked closer, the source cards backing that answer only showed up in the sidebar. A reader scrolling through the center panel, where the answer actually lives, would never see those cards.
That led to a small commit, 1d35066: knowledge cards now render inline below the answer, in the same panel where people read. The sidebar stays, just focused on articles and site pages. The plan sounded trivial, and when I started, I genuinely thought it was. What I hadn't realized: moving that render drags two old contracts along into new territory.
Probably just a re-render
The component already existed. SourceCard was already rendered by ChatContextPanel for the sidebar, so I'd just call it again from ChatOutputPanel. Done in an evening, or so I figured.
The first three lines were exactly that. The rest weren't.
First thing to bite: the export. The convention in the React docs on exporting components says a file exporting a single component is fine with a default export, but once that file exports multiple components, use named exports [2]. Once the center panel started using SourceCard too, ChatContextPanel officially had two components, so SourceCard became a named export. It's a one-word change up front, but it means the component is now public surface of the file, not a hidden detail inside it.
Second thing: the id. Two panels now render cards for the same sources. If the id attribute gets copied as-is, one document has duplicate ids, and the MDN rule on ids says an id must be unique across the whole document [3]. This kind of bug doesn't explode in your face. It hides out and causes weird stuff later, like anchors jumping to the wrong place.
The fix is a suffix. Cards in the center panel get an id with the ai-source-center prefix followed by the turnId and the card's position:
id={`ai-source-center-${turn.id}-${n}`}The sidebar keeps its own id namespace. Two panels, two sets of ids, no collisions.
Expanded state gets its own home
The cards are expandable. Click, content opens. What I didn't think about at first: who owns the expansion.
If the state is shared between the two panels, opening a card in the sidebar would open the same card in the center. You could sell that as a feature, but I didn't want it. The two panels are two different reading contexts, and expanding in one place shouldn't leak into the other.
So the expanded state is now owned by the center panel itself, holding {turnId, slug}. Two pieces of info in one state: which card is open, in which turn. And since plain local variables don't persist between renders per the React state docs [1], it has to be Hook state, period.
The reset doesn't use an extra useEffect. During render, the panel just checks whether expanded.turnId still matches the active turn.id. Different turn, assume nothing is open. Switch turns, the state forgets on its own.
I briefly considered lifting this state to the parent and sharing it nicely across both panels. Cancelled. Duplicating state per panel is a cheap price for clear behavior: panel A opens a card, panel B doesn't follow. Sharing state is worth it when the panels are meant to sync, and here they aren't.
One small filter closes it all out: only knowledgeSources, meaning sources that have content, render inline. The badge numbers still come from the original index in turn.sources, not the new filtered order, so the [n] references in the answer still point to the right card. Empty cards don't deserve to squat on space below the answer.
What this commit actually taught me isn't how to render a card. Re-exporting, importing, rendering, that's the five-minute part. What made me think were the contracts carried over invisibly: ids must stay unique in one document, state must keep a clear owner. Those two are easy to miss, and from now on I check them before saying "just a re-render".