Skip to content

Commit 5d72efc

Browse files
feat(web): attach the page that actually shows a named figure or table
Page images only reached the model when retrieval pulled a chunk from that page. A question naming "Figure 1" can match body text that references the figure from another page, leaving the figure itself unseen. The client already holds the full figure index (/figures), so when the question names a figure or table, its real page is looked up by caption and attached too (scoped to the top retrieved papers, max two extra pages, deduped against pages already attached).
1 parent abe36d0 commit 5d72efc

3 files changed

Lines changed: 42 additions & 5 deletions

File tree

web/app/api.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,30 @@
216216
}
217217
}
218218

219-
async function buildMessages(priorTurns, latestUserText, chunks, useImages) {
219+
// When the question names "Figure N" / "Table N", find that element's real
220+
// page via the figure index (/figures). Retrieval often returns body text
221+
// that only references the figure from another page — without this, the
222+
// page that actually shows it never reaches the model. Scoped to papers in
223+
// the top retrieved chunks; at most two extra pages.
224+
function referencedFigurePages(question, chunks, figureIndex) {
225+
if (!Array.isArray(figureIndex) || figureIndex.length === 0) return [];
226+
const refs = [...question.matchAll(/\b(fig(?:ure)?\.?|table)\s*(\d+)\b/gi)];
227+
if (refs.length === 0) return [];
228+
const papers = [...new Set(chunks.slice(0, 3).map((c) => c.paper_id))];
229+
const out = [];
230+
for (const [, kindRaw, num] of refs) {
231+
const re = /^t/i.test(kindRaw)
232+
? new RegExp(`^table\\.?\\s*${num}\\b`, "i")
233+
: new RegExp(`^fig(?:ure)?\\.?\\s*${num}\\b`, "i");
234+
for (const paperId of papers) {
235+
const f = figureIndex.find((g) => g.paper_id === paperId && re.test(String(g.caption || "").trim()));
236+
if (f && typeof f.page_number === "number") out.push({ paperId, page: f.page_number });
237+
}
238+
}
239+
return out.slice(0, 2);
240+
}
241+
242+
async function buildMessages(priorTurns, latestUserText, chunks, useImages, figureIndex) {
220243
const system = [
221244
"You are a careful research assistant answering questions from the supplied documents.",
222245
'- Use only the provided context for factual claims. If the user asks a factual question the context cannot answer, say exactly "Not stated in the provided context." — do not speculate.',
@@ -257,6 +280,20 @@
257280
}
258281
}
259282
}
283+
// Attach the page that actually shows a figure/table the question names,
284+
// when retrieval didn't already bring it in.
285+
if (useImages) {
286+
for (const ref of referencedFigurePages(latestUserText, chunks, figureIndex)) {
287+
const key = `${ref.paperId}:${ref.page}`;
288+
if (seenPages.has(key)) continue;
289+
seenPages.add(key);
290+
const dataUrl = await imageToDataUrl(pageImageUrl(ref.paperId, ref.page));
291+
if (dataUrl) {
292+
content.push({ type: "text", text: `[page image ${ref.paperId}::p${ref.page}::page]` });
293+
content.push({ type: "image_url", image_url: { url: dataUrl } });
294+
}
295+
}
296+
}
260297
content.push({ type: "text", text: `\nQuestion: ${latestUserText}` });
261298
messages.push({ role: "user", content });
262299
return messages;

web/app/app.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ function App() {
308308
</div>
309309

310310
<div className="view">
311-
{tab === "chat" && <ChatView settings={settings} set={set} layout={layout} apiKey={apiKey} model={model} papers={papers} pagesAvailable={pagesAvailable} demoAvailable={demoAvailable} routingAvailable={routingAvailable} onNeedKey={(reason) => setKeyModalOpen(reason || "quota")} />}
311+
{tab === "chat" && <ChatView settings={settings} set={set} layout={layout} apiKey={apiKey} model={model} papers={papers} figures={figures} pagesAvailable={pagesAvailable} demoAvailable={demoAvailable} routingAvailable={routingAvailable} onNeedKey={(reason) => setKeyModalOpen(reason || "quota")} />}
312312
{tab === "inspection" && <InspectionView settings={settings} apiKey={apiKey} model={model} papers={papers} pagesAvailable={pagesAvailable} routingAvailable={routingAvailable} />}
313313
{tab === "papers" && <PapersView setTab={setTab} papers={papers} figures={figures} />}
314314
{tab === "figures" && <FiguresView figures={figures} />}

web/app/chat.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ function RetrievalPanel({ turn, highlight, settings, paperTitle, routingAvailabl
270270
);
271271
}
272272

273-
function ChatView({ settings, set, layout, resetSignal, apiKey, model, papers, pagesAvailable, demoAvailable, routingAvailable, onNeedKey }) {
273+
function ChatView({ settings, set, layout, resetSignal, apiKey, model, papers, figures, pagesAvailable, demoAvailable, routingAvailable, onNeedKey }) {
274274
const [turns, setTurns] = useState([]);
275275
const [busy, setBusy] = useState(false);
276276
const [advOpen, setAdvOpen] = useState(false);
@@ -397,7 +397,7 @@ function ChatView({ settings, set, layout, resetSignal, apiKey, model, papers, p
397397
// attach page images when pages are served.
398398
const useImages = pagesAvailable && (hasKey ? window.RAG.supportsVision(model) : true);
399399
if (useImages) setStatus(hasKey ? "Reading the retrieved page images…" : "Free demo model is reading the retrieved pages…");
400-
const messages = await window.RAG.buildMessages(priorTurns, q, results, useImages);
400+
const messages = await window.RAG.buildMessages(priorTurns, q, results, useImages, figures);
401401
const tGen = performance.now();
402402
const onDelta = (delta) => updateLast((prev) => ({ answer: prev.answer + delta }));
403403
const { text, usage } = hasKey
@@ -445,7 +445,7 @@ function ChatView({ settings, set, layout, resetSignal, apiKey, model, papers, p
445445
} finally {
446446
setBusy(false);
447447
}
448-
}, [busy, apiKey, model, settings, pagesAvailable, demoAvailable, routingAvailable, onNeedKey]);
448+
}, [busy, apiKey, model, settings, figures, pagesAvailable, demoAvailable, routingAvailable, onNeedKey]);
449449

450450
const newChat = () => { setTurns([]); setHighlight(null); setBusy(false); setStatus(""); };
451451
// The retrieval panel is hidden in focus layout and at phone width, so a

0 commit comments

Comments
 (0)