- Notifications
You must be signed in to change notification settings - Fork 0
Answer page: take the box off the answer, quieten the warnings, and show the cited document's front page#2377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a594f04
Answer page: take the box off the answer, and quieten the warnings
claude 9ace6ef
Answer page: show the cited document's front page in the source drawer
claude c391de3
Merge remote-tracking branch 'origin/main' into claude/answer-page-re…
claude 1998451
Regenerate the repo-awareness snapshot for the new cover route
claude fd98774
Answer sources: make a failed cover lookup retryable, and read the co…
claude 4a75d11
Merge branch 'main' into claude/answer-page-redesign-review-4ufdl5
BigSimmo 81bed42
fix: validate selected document cover
BigSimmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { z } from "zod"; | ||
| import { rateLimitJsonResponse } from "@/lib/api-rate-limit"; | ||
| import { demoImages } from "@/lib/demo-data"; | ||
| import { isDemoMode } from "@/lib/env"; | ||
| import { jsonError, PublicApiError, publicErrorResponse } from "@/lib/http"; | ||
| import { fetchDocumentCoverImageIds } from "@/lib/document-enrichment"; | ||
| import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline"; | ||
| import { parseRouteParams } from "@/lib/validation/params"; | ||
| import { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access"; | ||
| import { createAdminClient } from "@/lib/supabase/admin"; | ||
| import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth"; | ||
| export const runtime = "nodejs"; | ||
| const coverRouteParamsSchema = z.object({ id: z.string().uuid() }); | ||
| const coverImageIdSchema = z.string().uuid(); | ||
| /** | ||
| * The document's first-page cover thumbnail id, for surfaces that show what a | ||
| * cited document looks like rather than what it says. | ||
| * | ||
| * It exists as its own route because the only alternatives were worse. The | ||
| * cover id rides `RelatedDocument` on the search payload, but the answer | ||
| * surface never calls `/api/search`; adding it to the answer's own source rows | ||
| * would mean editing retrieval hydration, which is a protected RAG surface and | ||
| * a far larger blast radius than a thumbnail earns. `/api/documents/[id]` | ||
| * already carries the id but returns pages, chunks and images with it — a | ||
| * kilobyte-scale payload to render one 90px picture. | ||
| * | ||
| * Authorization is the same shape the rest of the document API uses: the read | ||
| * rate limit first, then an owner-scoped existence check on `documents` BEFORE | ||
| * `document_images` is touched, so an unauthorized caller cannot learn whether | ||
| * a document id is real from the difference between two responses. The id it | ||
| * returns is not itself a capability — `/api/images/[id]/signed-url` re-checks | ||
| * ownership and committed-generation before it signs anything. | ||
| */ | ||
| export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { | ||
| try { | ||
| const { id: rawId } = await params; | ||
| if (isDemoMode()) { | ||
| const cover = demoImages.find((image) => image.document_id === rawId && image.source_kind === "cover_page"); | ||
| return NextResponse.json({ coverImageId: cover?.id ?? null, demoMode: true }); | ||
| } | ||
| const { id } = parseRouteParams({ id: rawId }, coverRouteParamsSchema, "Invalid document id."); | ||
| const supabase = createAdminClient(); | ||
| const { access, rateLimit } = await enforceDocumentReadRateLimit(request, supabase); | ||
| if (rateLimit.limited) { | ||
| return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit); | ||
| } | ||
| request.signal.throwIfAborted(); | ||
| const { data: document, error: documentError } = await withOwnerReadScope( | ||
| supabase.from("documents").select("id,metadata").eq("id", id), | ||
| access.ownerId, | ||
| ) | ||
| .abortSignal(request.signal) | ||
| .maybeSingle(); | ||
| if (documentError) throw new Error(documentError.message); | ||
| if (!document) return publicErrorResponse("Document not found.", 404, { code: "document_not_found" }); | ||
| /* | ||
| * `documents.metadata.cover_image_id` is the SELECTED cover: the worker | ||
| * writes it in the same committed-core metadata patch as | ||
| * `index_generation_id` (worker/main.ts), so it names the cover belonging to | ||
| * the generation the document currently serves. | ||
| * | ||
| * Prefer it over scanning `document_images` for a `cover_page` row. That | ||
| * scan takes whichever row comes back first, with no ordering and no | ||
| * generation filter, so a document mid-reindex or mid-cover-repair can hand | ||
| * back a staged row — which `/api/images/[id]/signed-url` then refuses as | ||
| * uncommitted, silently losing the thumbnail — or an obsolete duplicate, | ||
| * which shows the wrong front page beside a citation. | ||
| */ | ||
| const metadata = | ||
| document.metadata && typeof document.metadata === "object" && !Array.isArray(document.metadata) | ||
| ? (document.metadata as Record<string, unknown>) | ||
| : null; | ||
| if (metadata && Object.hasOwn(metadata, "cover_image_id")) { | ||
| const parsedPointer = coverImageIdSchema.safeParse(metadata.cover_image_id); | ||
| if (!parsedPointer.success) return NextResponse.json({ coverImageId: null }); | ||
| const { data: cover, error: coverError } = await supabase | ||
| .from("document_images") | ||
| .select("id,metadata") | ||
| .eq("id", parsedPointer.data) | ||
| .eq("document_id", id) | ||
| .eq("source_kind", "cover_page") | ||
| .abortSignal(request.signal) | ||
| .maybeSingle(); | ||
| if (coverError) throw new Error(coverError.message); | ||
| if ( | ||
| !cover || | ||
| !isCommittedGenerationMetadata({ | ||
| rowMetadata: cover.metadata, | ||
| committedGeneration: committedIndexGeneration(metadata), | ||
| }) | ||
| ) { | ||
| return NextResponse.json({ coverImageId: null }); | ||
| } | ||
| return NextResponse.json({ coverImageId: cover.id }); | ||
| } | ||
| // Documents indexed before the pointer existed carry no such key. Fall back | ||
| // to the scan rather than drop their thumbnail: it is the same resolution | ||
| // the document search cards already use, and the signed-url route still | ||
| // re-checks ownership and committed generation before it hands anything out. | ||
| const covers = await fetchDocumentCoverImageIds(supabase, [id], request.signal); | ||
| return NextResponse.json({ coverImageId: covers.get(id) ?? null }); | ||
| } catch (error) { | ||
| if (error instanceof AuthenticationError) return unauthorizedResponse(); | ||
| if (error instanceof PublicApiError) return jsonError(error); | ||
| return jsonError(error); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 9 additions & 2 deletions
11 src/components/clinical-dashboard/answer-result-surface.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 39 additions & 3 deletions
42 src/components/clinical-dashboard/answer-source-drawer.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.