Skip to content

Commit 20bd7ef

Browse files
fix(web): retrieval panel + routing UX fixes
- surface cited page-images in their own panel section (previously orphaned off the ranked list) - sort cited candidates to the top - route bars count cited modality, not retrieval mix -- a text route that cited page-images read as 100% text - routing toggle: auto/text/visual/hybrid/agentic - route-pill copy; de-figure the page-image note - drop the embeddings chip and Index panel - html overflow:hidden -- kill the double scrollbar on Figures
1 parent e58be96 commit 20bd7ef

5 files changed

Lines changed: 60 additions & 18 deletions

File tree

web/app/app.jsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,6 @@ function Sidebar({ tab, setTab, theme, setTheme, layout, setLayout, stats, open
3232

3333
<div className="sidebar-spacer"></div>
3434

35-
<div className="corpus-card">
36-
<h4>Index</h4>
37-
<div className="stat-row"><span className="k">Papers</span><span className="v">{stats.papers || "—"}</span></div>
38-
<div className="stat-row"><span className="k">Figures</span><span className="v">{stats.figures || "—"}</span></div>
39-
<hr className="divider" style={{ margin: "8px 0" }} />
40-
<div className="stat-row"><span className="k">Text</span><span className="v idx">bge-m3</span></div>
41-
<div className="stat-row"><span className="k">Embeddings</span><span className="v idx">1024-d</span></div>
42-
</div>
43-
4435
<div className="sidebar-foot">
4536
<div className="theme-toggle" role="group" aria-label="Theme">
4637
<button className={theme === "light" ? "on" : ""} onClick={() => setTheme("light")} title="Light"><Icon name="sun" size={14} /></button>
@@ -322,7 +313,6 @@ function App() {
322313
<Segmented value={layout} onChange={setLayout}
323314
options={[{ value: "split", label: "split" }, { value: "single", label: "focus" }]} />
324315
}
325-
<span className="tag"><Icon name="layers" size={12} style={{ verticalAlign: -2, marginRight: 4 }} />1024-d embeddings</span>
326316
</div>
327317
</div>
328318

web/app/chat.jsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function AdvancedPanel({ settings, set, papers, routingAvailable }) {
3636
<Segmented value={settings.route} onChange={(v) => set("route", v)}
3737
options={[{ value: "auto", label: "auto" }, { value: "text", label: "text" },
3838
{ value: "visual", label: "visual", disabled: noRouter, disabledTitle: offTitle },
39+
{ value: "hybrid", label: "hybrid", disabled: noRouter, disabledTitle: offTitle },
3940
{ value: "agentic", label: "agentic" }]} />
4041
{noRouter &&
4142
<span className="field-note">visual routing needs the GPU leg — off on this CPU deployment (offline: +35% recall over text-only); figure questions still work via page images</span>
@@ -81,7 +82,6 @@ function EmptyState({ onAsk, routingAvailable }) {
8182
{window.RAG.SUGGESTIONS.map((s, i) => (
8283
<button key={i} className="suggest" onClick={() => onAsk(s.q)}>
8384
<span className="q">{s.q}</span>
84-
{!noRouter && <RoutePill route={s.route} />}
8585
</button>
8686
))}
8787
</div>
@@ -198,12 +198,40 @@ function RetrievalPanel({ turn, highlight, settings, paperTitle, routingAvailabl
198198
const relScore = (s) => (sMax === sMin ? 1 : (s - sMin) / (sMax - sMin));
199199
const vis = cands.filter((c) => c.kind === "visual");
200200
const total = cands.length;
201-
const visShare = total ? Math.round((vis.length / total) * 100) : 0;
201+
// Evidence-mix bars reflect what the answer CITED, not just what was
202+
// retrieved. On a text route the model can still cite page images attached at
203+
// generation, so a retrieval-only split falsely reads 100% text. Count cited
204+
// modality (page_cite / fig_cite / visual candidates all carry kind "visual");
205+
// fall back to the retrieved-candidate mix before any citations exist.
206+
const cites = turn.citations || [];
207+
const visCited = cites.filter((c) => c.kind === "visual").length;
208+
const visShare = cites.length
209+
? Math.round((visCited / cites.length) * 100)
210+
: total
211+
? Math.round((vis.length / total) * 100)
212+
: 0;
202213
const txtShare = 100 - visShare;
203214
const citedNum = (c) => {
204215
const ct = (turn.citations || []).find((x) => x.id === c.chunk_id);
205216
return ct ? ct.n : null;
206217
};
218+
// Page-image citations (`::page`) the model produced from the page images
219+
// attached at generation. They aren't retrieval candidates, so they get no
220+
// ranked row — surface them here so a cited [n] always resolves to something
221+
// in the panel. Skip any whose id is already a candidate (hybrid route, where
222+
// the visual leg retrieved the same page).
223+
const citedPages = (turn.citations || []).filter(
224+
(c) => c.page_cite && !cands.some((cd) => cd.chunk_id === c.id)
225+
);
226+
// Surface the candidates the answer actually cited at the top, then the rest.
227+
// Each group keeps the retriever's existing rank order — which for a text-only
228+
// result set is score DESC. We deliberately don't re-sort by raw `score`: text
229+
// rerank logits and visual patch-sim are different scales, so a global sort
230+
// would interleave the legs wrongly. The [n] badge is the citation's own number.
231+
const orderedCands = [
232+
...cands.filter((c) => citedNum(c) != null),
233+
...cands.filter((c) => citedNum(c) == null),
234+
];
207235
return (
208236
<aside className="retr-panel">
209237
<div className="retr-head">
@@ -258,7 +286,7 @@ function RetrievalPanel({ turn, highlight, settings, paperTitle, routingAvailabl
258286

259287
<div className="retr-section">
260288
<h4>Ranked candidates <span className="n">{total} chunks</span></h4>
261-
{cands.map((c, i) => {
289+
{orderedCands.map((c, i) => {
262290
const num = citedNum(c);
263291
const hl = highlight && num && String(num) === String(highlight);
264292
return (
@@ -280,6 +308,26 @@ function RetrievalPanel({ turn, highlight, settings, paperTitle, routingAvailabl
280308
})}
281309
</div>
282310

311+
{citedPages.length > 0 && (
312+
<div className="retr-section">
313+
<h4>Page images cited <span className="n">{citedPages.length}</span></h4>
314+
{citedPages.map((c, i) => (
315+
<div key={c.id || i} className="cand row-clickable"
316+
onClick={() => setPageItem({ chunk_id: c.id, paper: c.paper, page: c.page, pages: [c.page], kind: "visual", bbox: null, text: "", page_cite: true })}
317+
title="View cited page image">
318+
<div className="cand-top">
319+
<span className="cand-num visual">{c.n}</span>
320+
<span className="cand-src">{c.paper} · p.{c.page}</span>
321+
<span className="cand-score">page</span>
322+
</div>
323+
<div className="cand-meta">
324+
<span className="pin-note">the model read this whole page at generation; cited but not a ranked retrieval result</span>
325+
</div>
326+
</div>
327+
))}
328+
</div>
329+
)}
330+
283331
{vis.length > 0 && (
284332
<div className="retr-section">
285333
<h4>Retrieved figures <span className="n">{vis.length}</span></h4>
@@ -407,7 +455,7 @@ function ChatView({ settings, set, layout, resetSignal, apiKey, model, papers, f
407455
const t0 = performance.now();
408456
const { results, routing, trace } = await window.RAG.retrieve(searchQuery, {
409457
topK: settings.topk,
410-
forceRoute: settings.route === "text" ? "text" : settings.route === "visual" ? "hybrid" : "",
458+
forceRoute: ["text", "visual", "hybrid"].includes(settings.route) ? settings.route : "",
411459
routingMode: settings.routingMode || "",
412460
paperId: settings.paper || "",
413461
dci: settings.route === "agentic",

web/app/inspection.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function InspectionView({ settings, papers, routingAvailable }) {
7272
try {
7373
const { results, routing } = await window.RAG.retrieve(v, {
7474
topK: settings.topk,
75-
forceRoute: settings.route === "text" ? "text" : settings.route === "visual" ? "hybrid" : "",
75+
forceRoute: ["text", "visual", "hybrid"].includes(settings.route) ? settings.route : "",
7676
routingMode: settings.routingMode || "",
7777
paperId: settings.paper || "",
7878
onStatus: setStatus,

web/app/shared.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function Icon({ name, size = 16, className = "", strokeWidth = 1.7, fill = false
5252

5353
function RoutePill({ route }) {
5454
const cls = route === "visual" ? "visual" : route === "agentic" || route.includes("+") ? "mixed" : "text";
55-
const label = route === "visual" ? "visual route" : route === "agentic" ? "agentic search" : route.includes("+") ? "text + visual" : "text route";
55+
const label = route === "visual" ? "visual" : route === "agentic" ? "agentic search" : route.includes("+") ? "text + visual" : "text";
5656
return <span className={"pill " + cls}><span className="dot"></span>{label}</span>;
5757
}
5858

@@ -235,7 +235,7 @@ function PageRegionModal({ item, onClose, paperTitle }) {
235235
<div className="pm-note">
236236
<Icon name={isVis ? "image" : "text"} size={13} />
237237
<span>{item.page_cite
238-
? "Cited as a page image: the model read this page directly when describing the figure, so the whole page is the evidence."
238+
? "Cited as a page image: the model read this page directly, so the whole page is the evidence."
239239
: isVis
240240
? (hasBbox ? "Retrieved from the visual store over page images; the box marks the figure or table region on the source page." : "Retrieved from the visual store over page images.")
241241
: (hasBbox ? "Retrieved from the text store by the dense retriever; the box marks the cited passage on the page." : "Retrieved from the text store by the dense retriever. This chunk spans page boundaries, so no single region box is available.")}</span>

web/app/styles.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@
6262
}
6363

6464
* { box-sizing: border-box; }
65-
html, body { height: 100%; }
65+
/* The app shell never scrolls the document — every tab scrolls inside its own
66+
container (`.view` is overflow:hidden; panels scroll via `.scroll-view > .content-pad` or `.chat-scroll`).
67+
Without this, a tall inner grid (the Figures gallery) leaks its height to the
68+
root scroller and you get a second, page-level scrollbar. */
69+
html, body { height: 100%; overflow: hidden; }
6670
body {
6771
margin: 0;
6872
background: var(--bg);

0 commit comments

Comments
 (0)