From 00174bcaf137d4c9833c9018ab8bde9ec2bdc31c Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:57:53 -0500 Subject: [PATCH 1/4] feat: add shareable evaluation prompt links --- .../app/cypress/e2e/evaluation-chart.cy.ts | 58 ++++++++++++ .../app/src/app/api/v1/eval-samples/route.ts | 28 +++++- .../evaluation/ui/EvalSamplesDrawer.tsx | 91 ++++++++++++++++++- .../evaluation/ui/EvaluationTable.tsx | 29 +++++- .../app/src/hooks/api/use-eval-samples.ts | 7 +- packages/app/src/lib/api.ts | 4 + packages/db/src/queries/eval-samples.ts | 17 ++++ 7 files changed, 224 insertions(+), 10 deletions(-) diff --git a/packages/app/cypress/e2e/evaluation-chart.cy.ts b/packages/app/cypress/e2e/evaluation-chart.cy.ts index d06c2f1e2..01384cf88 100644 --- a/packages/app/cypress/e2e/evaluation-chart.cy.ts +++ b/packages/app/cypress/e2e/evaluation-chart.cy.ts @@ -142,3 +142,61 @@ describe('Evaluation Chart — Content & Interactions', () => { .should('be.checked'); }); }); + +describe('Evaluation sample sharing', () => { + beforeEach(() => { + cy.visit('/evaluation'); + cy.get('[data-testid="evaluation-chart-display"]').should('be.visible'); + cy.get('[data-testid="evaluation-view-toggle"]').contains('Table').click(); + }); + + it('copies and restores a link to the prompt drawer', () => { + cy.on('uncaught:exception', (error) => !error.message.includes('Hydration failed')); + cy.get('[title="View per-sample prompts and responses"]').first().click(); + + cy.window().then((win) => { + cy.stub(win.navigator.clipboard, 'writeText').as('writeDrawerLink').resolves(); + }); + cy.get('[data-testid="eval-drawer-share-button"]').click(); + cy.contains('[data-testid="eval-drawer-share-button"]', 'Copied').should('be.visible'); + + cy.get('@writeDrawerLink') + .should('have.been.calledOnce') + .then((stub) => { + const sharedUrl = String((stub as sinon.SinonStub).firstCall.args[0]); + const params = new URL(sharedUrl).searchParams; + expect(params.get('eval')).to.match(/^\d+$/); + expect(params.has('sample')).to.equal(false); + cy.visit(sharedUrl); + }); + + cy.get('[role="dialog"]').should('be.visible'); + cy.get('[data-testid="eval-drawer-share-button"]').should('be.visible'); + cy.get('[role="dialog"] li > button[aria-expanded="true"]').should('not.exist'); + }); + + it('copies and restores a link to one expanded sample', () => { + cy.on('uncaught:exception', (error) => !error.message.includes('Hydration failed')); + cy.get('[title="View per-sample prompts and responses"]').first().click(); + cy.get('[role="dialog"] li > button').first().click(); + + cy.window().then((win) => { + cy.stub(win.navigator.clipboard, 'writeText').as('writeShareLink').resolves(); + }); + cy.get('[data-testid^="eval-sample-share-"]').click(); + cy.contains('[data-testid^="eval-sample-share-"]', 'Copied').should('be.visible'); + + cy.get('@writeShareLink') + .should('have.been.calledOnce') + .then((stub) => { + const sharedUrl = String((stub as sinon.SinonStub).firstCall.args[0]); + expect(new URL(sharedUrl).searchParams.get('eval')).to.match(/^\d+$/); + expect(new URL(sharedUrl).searchParams.get('sample')).to.match(/^\d+$/); + cy.visit(sharedUrl); + }); + + cy.get('[role="dialog"]').should('be.visible'); + cy.get('[aria-expanded="true"]').should('exist'); + cy.get('[data-testid^="eval-sample-share-"]').scrollIntoView().should('be.visible'); + }); +}); diff --git a/packages/app/src/app/api/v1/eval-samples/route.ts b/packages/app/src/app/api/v1/eval-samples/route.ts index ec182ac8b..897eb5d2a 100644 --- a/packages/app/src/app/api/v1/eval-samples/route.ts +++ b/packages/app/src/app/api/v1/eval-samples/route.ts @@ -1,7 +1,10 @@ import { type NextRequest, NextResponse } from 'next/server'; import { getDb } from '@semianalysisai/inferencex-db/connection'; -import { getEvalSamples } from '@semianalysisai/inferencex-db/queries/eval-samples'; +import { + getEvalSampleOffset, + getEvalSamples, +} from '@semianalysisai/inferencex-db/queries/eval-samples'; import { cachedJson, cachedQuery } from '@/lib/api-cache'; import { extractDemonstrations } from '@/lib/eval-sample-utils'; @@ -17,9 +20,13 @@ const getCachedEvalSamples = cachedQuery( getEvalSamples(getDb(), evalResultId, filter, offset, limit), 'eval-samples', ); +const getCachedEvalSampleOffset = cachedQuery( + (evalResultId: number, docId: number) => getEvalSampleOffset(getDb(), evalResultId, docId), + 'eval-sample-offset', +); /** - * GET /api/v1/eval-samples?eval_result_id=N&filter=all|passed|failed&offset=0&limit=200 + * GET /api/v1/eval-samples?eval_result_id=N&filter=all|passed|failed&offset=0&limit=200&doc_id=N * * Returns a paginated slice of per-prompt samples for one `eval_results` row, * plus passed/failed totals for the filter-chip badges. Drawer use only — @@ -33,6 +40,8 @@ export async function GET(request: NextRequest) { const evalResultId = Number(params.get('eval_result_id')); const filterParam = params.get('filter') ?? 'all'; const offset = Math.max(0, Math.trunc(Number(params.get('offset') ?? '0'))); + const docIdParam = params.get('doc_id'); + const docId = docIdParam === null ? null : Math.trunc(Number(docIdParam)); const requestedLimit = Math.trunc(Number(params.get('limit') ?? String(DEFAULT_LIMIT))); const limit = Math.min(MAX_LIMIT, Math.max(1, requestedLimit || DEFAULT_LIMIT)); @@ -42,6 +51,9 @@ export async function GET(request: NextRequest) { { status: 400 }, ); } + if (docId !== null && (!Number.isFinite(docId) || docId < 0)) { + return NextResponse.json({ error: 'doc_id must be a non-negative integer' }, { status: 400 }); + } if (!ALLOWED_FILTERS.has(filterParam)) { return NextResponse.json( { error: `filter must be one of: ${[...ALLOWED_FILTERS].join(', ')}` }, @@ -51,8 +63,15 @@ export async function GET(request: NextRequest) { const filter = filterParam as 'all' | 'passed' | 'failed'; try { - const result = await getCachedEvalSamples(evalResultId, filter, offset, limit); - + const sampleOffset = + docId === null ? null : await getCachedEvalSampleOffset(evalResultId, docId); + if (docId !== null && sampleOffset === null) { + return NextResponse.json({ error: 'Sample not found' }, { status: 404 }); + } + const resolvedOffset = + sampleOffset === null ? offset : Math.floor(sampleOffset / limit) * limit; + const resolvedFilter = docId === null ? filter : 'all'; + const result = await getCachedEvalSamples(evalResultId, resolvedFilter, resolvedOffset, limit); return cachedJson({ samples: result.samples.map((s) => ({ docId: s.doc_id, @@ -69,6 +88,7 @@ export async function GET(request: NextRequest) { passedTotal: result.passedTotal, failedTotal: result.failedTotal, source: 'db' as const, + offset: resolvedOffset, }); } catch (error) { console.error('Error fetching eval samples:', error); diff --git a/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx b/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx index da5b0b4d7..72edec31d 100644 --- a/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx +++ b/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; +import { Check, ChevronLeft, ChevronRight, Search, Share2 } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import type { EvaluationChartData } from '@/components/evaluation/types'; @@ -8,6 +8,7 @@ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { useEvalSamples } from '@/hooks/api/use-eval-samples'; import { track } from '@/lib/analytics'; import type { EvalSamplesFilter, EvalSamplesLiveContext } from '@/lib/api'; +import { buildShareUrl } from '@/lib/url-state'; import { useLocale } from '@/lib/use-locale'; const PAGE_SIZE = 50; @@ -44,6 +45,8 @@ const STRINGS = { total === 0 ? '0 of 0' : `${start}–${end} of ${total}`, prevPage: 'Previous page', nextPage: 'Next page', + shareSample: 'Share', + copied: 'Copied', }, zh: { score: '得分', @@ -73,6 +76,8 @@ const STRINGS = { total === 0 ? '共 0 条' : `第 ${start}–${end} 条,共 ${total} 条`, prevPage: '上一页', nextPage: '下一页', + shareSample: '分享', + copied: '已复制', }, } as const; @@ -80,6 +85,8 @@ interface EvalSamplesDrawerProps { /** The selected row from the EvaluationTable, or null when closed. */ row: EvaluationChartData | null; onClose: () => void; + /** Sample selected by a shared URL. */ + initialDocId?: number | null; } /** @@ -91,7 +98,11 @@ interface EvalSamplesDrawerProps { * Inspired by the vLLM eval dashboard PoC * (credit: @khluu, @simon-mo, @robertgshaw2-redhat). */ -export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerProps) { +export default function EvalSamplesDrawer({ + row, + onClose, + initialDocId = null, +}: EvalSamplesDrawerProps) { const open = row !== null; const locale = useLocale(); const t = STRINGS[locale]; @@ -99,6 +110,8 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro const [page, setPage] = useState(0); const [search, setSearch] = useState(''); const [expanded, setExpanded] = useState>(new Set()); + const [requestedDocId, setRequestedDocId] = useState(initialDocId); + const [copiedTarget, setCopiedTarget] = useState(null); // Reset transient state whenever a new row is opened. useEffect(() => { @@ -107,7 +120,9 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro setPage(0); setSearch(''); setExpanded(new Set()); - }, [row?.evalResultId, open]); + setRequestedDocId(initialDocId); + setCopiedTarget(null); + }, [row?.evalResultId, open, initialDocId]); // Build a live-fetch context for unofficial runs from the row's identifying // fields. The hook ignores this when `evalResultId > 0` (DB-backed path). @@ -134,6 +149,7 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro filter, offset: page * PAGE_SIZE, limit: PAGE_SIZE, + docId: requestedDocId, }); // Client-side substring filter on the page slice — server-side full-text @@ -156,6 +172,14 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); const safePage = Math.min(page, totalPages - 1); + useEffect(() => { + if (requestedDocId === null || !data || data.offset === undefined) return; + if (!data.samples.some((sample) => sample.docId === requestedDocId)) return; + setPage(Math.floor(data.offset / PAGE_SIZE)); + setExpanded(new Set([requestedDocId])); + setRequestedDocId(null); + }, [data, requestedDocId]); + const handleFilterChange = (next: EvalSamplesFilter) => { setFilter(next); setPage(0); @@ -177,6 +201,33 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro track('evaluation_samples_paged', { direction: delta > 0 ? 'next' : 'prev' }); }; + const handleShare = async (docId?: number) => { + if (!row || row.evalResultId <= 0) return; + const url = new URL(buildShareUrl()); + url.searchParams.set('eval', String(row.evalResultId)); + if (docId === undefined) url.searchParams.delete('sample'); + else url.searchParams.set('sample', String(docId)); + + try { + await navigator.clipboard.writeText(url.toString()); + } catch { + const textArea = document.createElement('textarea'); + textArea.value = url.toString(); + document.body.append(textArea); + textArea.select(); + document.execCommand('copy'); + textArea.remove(); + } + + const target = docId ?? 'drawer'; + setCopiedTarget(target); + setTimeout(() => setCopiedTarget((current) => (current === target ? null : current)), 2000); + track(docId === undefined ? 'evaluation_drawer_link_copied' : 'evaluation_sample_link_copied', { + eval_result_id: row.evalResultId, + ...(docId === undefined ? {} : { doc_id: docId }), + }); + }; + const isUnofficial = row !== null && row.evalResultId <= 0; // Unofficial runs are renderable as long as we resolved a runId from the row's url. // If we couldn't (no run_url, malformed url, etc.), fall back to the empty-state copy below. @@ -229,6 +280,23 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro )} + {row && row.evalResultId > 0 && ( + + )} {/* Filter chips + search */} @@ -343,6 +411,23 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro emptyText={t.empty} /> )} + {row && row.evalResultId > 0 && ( + + )} )} diff --git a/packages/app/src/components/evaluation/ui/EvaluationTable.tsx b/packages/app/src/components/evaluation/ui/EvaluationTable.tsx index 6620d3997..ae677510a 100644 --- a/packages/app/src/components/evaluation/ui/EvaluationTable.tsx +++ b/packages/app/src/components/evaluation/ui/EvaluationTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { MessageSquareText } from 'lucide-react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useMemo, useState } from 'react'; import EvalSamplesDrawer from '@/components/evaluation/ui/EvalSamplesDrawer'; @@ -53,11 +54,20 @@ interface EvaluationTableProps { export default function EvaluationTable({ data }: EvaluationTableProps) { const { runIndexByUrl } = useUnofficialRun(); + const pathname = usePathname(); + const router = useRouter(); + const searchParams = useSearchParams(); const locale = useLocale(); const t = STRINGS[locale]; const sorted = useMemo(() => [...data].toSorted((a, b) => b.score - a.score), [data]); const hasDisaggConfigs = useMemo(() => data.some((d) => d.disagg), [data]); const [drawerRow, setDrawerRow] = useState(null); + const sharedEvalResultId = Number(searchParams.get('eval')); + const sharedDocId = Number(searchParams.get('sample')); + const hasSharedEval = + searchParams.has('eval') && Number.isInteger(sharedEvalResultId) && sharedEvalResultId > 0; + const hasSharedSample = + searchParams.has('sample') && Number.isInteger(sharedDocId) && sharedDocId >= 0; const openDrawer = (row: EvaluationChartData) => { setDrawerRow(row); @@ -73,6 +83,19 @@ export default function EvaluationTable({ data }: EvaluationTableProps) { }); }; + const closeDrawer = () => { + setDrawerRow(null); + const params = new URLSearchParams(searchParams); + params.delete('eval'); + params.delete('sample'); + router.replace(params.size > 0 ? `${pathname}?${params}` : pathname, { scroll: false }); + }; + + const sharedRow = hasSharedEval + ? (data.find((row) => Number(row.evalResultId) === sharedEvalResultId) ?? null) + : null; + const activeDrawerRow = drawerRow ?? sharedRow; + const columns = useMemo[]>( () => [ { @@ -206,7 +229,11 @@ export default function EvaluationTable({ data }: EvaluationTableProps) { testId="evaluation-results-table" analyticsPrefix="evaluation_table" /> - setDrawerRow(null)} /> + ); } diff --git a/packages/app/src/hooks/api/use-eval-samples.ts b/packages/app/src/hooks/api/use-eval-samples.ts index 9d6920333..9df3c2fbb 100644 --- a/packages/app/src/hooks/api/use-eval-samples.ts +++ b/packages/app/src/hooks/api/use-eval-samples.ts @@ -15,6 +15,8 @@ interface UseEvalSamplesArgs { filter: EvalSamplesFilter; offset: number; limit: number; + /** Shared sample to resolve into its containing page. DB-backed runs only. */ + docId?: number | null; } /** @@ -37,6 +39,7 @@ export function useEvalSamples({ filter, offset, limit, + docId, }: UseEvalSamplesArgs) { const useLive = evalResultId !== null && evalResultId <= 0 && Boolean(liveContext); const useDb = evalResultId !== null && evalResultId > 0; @@ -44,11 +47,11 @@ export function useEvalSamples({ return useQuery({ queryKey: useLive ? ['eval-samples-live', liveContext, filter, offset, limit] - : ['eval-samples', evalResultId, filter, offset, limit], + : ['eval-samples', evalResultId, filter, offset, limit, docId], queryFn: ({ signal }) => useLive ? fetchEvalSamplesLive(liveContext!, filter, offset, limit, signal) - : fetchEvalSamples(evalResultId!, filter, offset, limit, signal), + : fetchEvalSamples(evalResultId!, filter, offset, limit, docId, signal), enabled: useDb || useLive, placeholderData: keepPreviousData, }); diff --git a/packages/app/src/lib/api.ts b/packages/app/src/lib/api.ts index 270884558..1c8cb5e83 100644 --- a/packages/app/src/lib/api.ts +++ b/packages/app/src/lib/api.ts @@ -235,6 +235,8 @@ export interface EvalSamplesResponse { passedTotal: number; failedTotal: number; source: 'db' | 'github_artifact'; + /** Actual page offset; present when the server resolves a shared doc id. */ + offset?: number; } export type EvalSamplesFilter = 'all' | 'passed' | 'failed'; @@ -244,6 +246,7 @@ export function fetchEvalSamples( filter: EvalSamplesFilter, offset: number, limit: number, + docId?: number | null, signal?: AbortSignal, ) { const params = new URLSearchParams({ @@ -252,6 +255,7 @@ export function fetchEvalSamples( offset: String(offset), limit: String(limit), }); + if (docId !== null && docId !== undefined) params.set('doc_id', String(docId)); return fetchJson(`/api/v1/eval-samples?${params}`, signal); } diff --git a/packages/db/src/queries/eval-samples.ts b/packages/db/src/queries/eval-samples.ts index c5735552a..32f4843a6 100644 --- a/packages/db/src/queries/eval-samples.ts +++ b/packages/db/src/queries/eval-samples.ts @@ -94,3 +94,20 @@ export async function getEvalSamples( failedTotal: counts.failed_total, }; } + +/** Return the zero-based position of one sample in the unfiltered doc-id ordering. */ +export async function getEvalSampleOffset( + sql: DbClient, + evalResultId: number, + docId: number, +): Promise { + const [row] = (await sql` + select + count(*) filter (where doc_id < ${docId})::int as offset, + count(*) filter (where doc_id = ${docId})::int as matches + from eval_samples + where eval_result_id = ${evalResultId} + `) as unknown as { offset: number; matches: number }[]; + + return row?.matches === 1 ? row.offset : null; +} From 1a4fa7e19f2fa2b674503bb2d17619b20e2ca30f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:08:31 -0500 Subject: [PATCH 2/4] fix: restore shared eval state after hydration --- .../app/cypress/e2e/evaluation-chart.cy.ts | 9 ++++ .../evaluation/ui/EvaluationTable.tsx | 48 ++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/app/cypress/e2e/evaluation-chart.cy.ts b/packages/app/cypress/e2e/evaluation-chart.cy.ts index 01384cf88..8499d6c05 100644 --- a/packages/app/cypress/e2e/evaluation-chart.cy.ts +++ b/packages/app/cypress/e2e/evaluation-chart.cy.ts @@ -175,6 +175,15 @@ describe('Evaluation sample sharing', () => { cy.get('[role="dialog"] li > button[aria-expanded="true"]').should('not.exist'); }); + it('does not apply a stale sample id to a manually opened drawer', () => { + cy.visit('/evaluation?sample=0'); + cy.get('[data-testid="evaluation-chart-display"]').should('be.visible'); + cy.get('[title="View per-sample prompts and responses"]').first().click(); + + cy.get('[role="dialog"]').should('be.visible'); + cy.get('[role="dialog"] li > button[aria-expanded="true"]').should('not.exist'); + }); + it('copies and restores a link to one expanded sample', () => { cy.on('uncaught:exception', (error) => !error.message.includes('Hydration failed')); cy.get('[title="View per-sample prompts and responses"]').first().click(); diff --git a/packages/app/src/components/evaluation/ui/EvaluationTable.tsx b/packages/app/src/components/evaluation/ui/EvaluationTable.tsx index ae677510a..11bb8a2c8 100644 --- a/packages/app/src/components/evaluation/ui/EvaluationTable.tsx +++ b/packages/app/src/components/evaluation/ui/EvaluationTable.tsx @@ -1,8 +1,7 @@ 'use client'; import { MessageSquareText } from 'lucide-react'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import EvalSamplesDrawer from '@/components/evaluation/ui/EvalSamplesDrawer'; import type { EvaluationChartData } from '@/components/evaluation/types'; @@ -54,22 +53,18 @@ interface EvaluationTableProps { export default function EvaluationTable({ data }: EvaluationTableProps) { const { runIndexByUrl } = useUnofficialRun(); - const pathname = usePathname(); - const router = useRouter(); - const searchParams = useSearchParams(); const locale = useLocale(); const t = STRINGS[locale]; const sorted = useMemo(() => [...data].toSorted((a, b) => b.score - a.score), [data]); const hasDisaggConfigs = useMemo(() => data.some((d) => d.disagg), [data]); const [drawerRow, setDrawerRow] = useState(null); - const sharedEvalResultId = Number(searchParams.get('eval')); - const sharedDocId = Number(searchParams.get('sample')); - const hasSharedEval = - searchParams.has('eval') && Number.isInteger(sharedEvalResultId) && sharedEvalResultId > 0; - const hasSharedSample = - searchParams.has('sample') && Number.isInteger(sharedDocId) && sharedDocId >= 0; + const [sharedTarget, setSharedTarget] = useState<{ + evalResultId: number; + docId?: number; + } | null>(null); const openDrawer = (row: EvaluationChartData) => { + setSharedTarget(null); setDrawerRow(row); // Notify the first-visit nudge to dismiss itself once the user has // discovered the affordance on their own. @@ -83,16 +78,33 @@ export default function EvaluationTable({ data }: EvaluationTableProps) { }); }; + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const evalParam = params.get('eval'); + if (evalParam === null) return; + + const evalResultId = Number(evalParam); + if (!Number.isInteger(evalResultId) || evalResultId <= 0) return; + + const sampleParam = params.get('sample'); + const docId = sampleParam === null ? undefined : Number(sampleParam); + setSharedTarget({ + evalResultId, + ...(docId !== undefined && Number.isInteger(docId) && docId >= 0 ? { docId } : {}), + }); + }, []); + const closeDrawer = () => { setDrawerRow(null); - const params = new URLSearchParams(searchParams); - params.delete('eval'); - params.delete('sample'); - router.replace(params.size > 0 ? `${pathname}?${params}` : pathname, { scroll: false }); + setSharedTarget(null); + const url = new URL(window.location.href); + url.searchParams.delete('eval'); + url.searchParams.delete('sample'); + window.history.replaceState(window.history.state, '', url); }; - const sharedRow = hasSharedEval - ? (data.find((row) => Number(row.evalResultId) === sharedEvalResultId) ?? null) + const sharedRow = sharedTarget + ? (data.find((row) => Number(row.evalResultId) === sharedTarget.evalResultId) ?? null) : null; const activeDrawerRow = drawerRow ?? sharedRow; @@ -231,7 +243,7 @@ export default function EvaluationTable({ data }: EvaluationTableProps) { /> From d95564e304c5cb81a87dd40684b81d3123ee50ad Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:12:40 -0500 Subject: [PATCH 3/4] test: stub eval samples in sharing e2e --- .../app/cypress/e2e/evaluation-chart.cy.ts | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/app/cypress/e2e/evaluation-chart.cy.ts b/packages/app/cypress/e2e/evaluation-chart.cy.ts index 8499d6c05..b3169baac 100644 --- a/packages/app/cypress/e2e/evaluation-chart.cy.ts +++ b/packages/app/cypress/e2e/evaluation-chart.cy.ts @@ -145,13 +145,41 @@ describe('Evaluation Chart — Content & Interactions', () => { describe('Evaluation sample sharing', () => { beforeEach(() => { + cy.intercept('GET', '/api/v1/eval-samples*', { + statusCode: 200, + body: { + samples: [ + { + docId: 0, + prompt: 'What is 1 + 1?', + target: '2', + response: '2', + rawResponse: null, + demonstrations: null, + passed: true, + score: 1, + metrics: {}, + }, + ], + total: 1, + passedTotal: 1, + failedTotal: 0, + source: 'db', + offset: 0, + }, + }); cy.visit('/evaluation'); cy.get('[data-testid="evaluation-chart-display"]').should('be.visible'); cy.get('[data-testid="evaluation-view-toggle"]').contains('Table').click(); }); it('copies and restores a link to the prompt drawer', () => { - cy.on('uncaught:exception', (error) => !error.message.includes('Hydration failed')); + cy.on( + 'uncaught:exception', + (error) => + !error.message.includes('Hydration failed') && + !error.message.includes('Minified React error #418'), + ); cy.get('[title="View per-sample prompts and responses"]').first().click(); cy.window().then((win) => { @@ -185,7 +213,12 @@ describe('Evaluation sample sharing', () => { }); it('copies and restores a link to one expanded sample', () => { - cy.on('uncaught:exception', (error) => !error.message.includes('Hydration failed')); + cy.on( + 'uncaught:exception', + (error) => + !error.message.includes('Hydration failed') && + !error.message.includes('Minified React error #418'), + ); cy.get('[title="View per-sample prompts and responses"]').first().click(); cy.get('[role="dialog"] li > button').first().click(); From 555aae7f0de708781e1bbb738b8233127a802114 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:18:31 -0500 Subject: [PATCH 4/4] fix: polish shared evaluation navigation --- .../evaluation/ui/EvalSamplesDrawer.tsx | 11 +++++++++-- .../components/evaluation/ui/EvaluationTable.tsx | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx b/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx index 72edec31d..92e4c9af4 100644 --- a/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx +++ b/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx @@ -1,7 +1,7 @@ 'use client'; import { Check, ChevronLeft, ChevronRight, Search, Share2 } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { EvaluationChartData } from '@/components/evaluation/types'; import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; @@ -112,6 +112,7 @@ export default function EvalSamplesDrawer({ const [expanded, setExpanded] = useState>(new Set()); const [requestedDocId, setRequestedDocId] = useState(initialDocId); const [copiedTarget, setCopiedTarget] = useState(null); + const bodyRef = useRef(null); // Reset transient state whenever a new row is opened. useEffect(() => { @@ -177,6 +178,11 @@ export default function EvalSamplesDrawer({ if (!data.samples.some((sample) => sample.docId === requestedDocId)) return; setPage(Math.floor(data.offset / PAGE_SIZE)); setExpanded(new Set([requestedDocId])); + requestAnimationFrame(() => { + bodyRef.current + ?.querySelector(`[data-eval-sample-id="${requestedDocId}"]`) + ?.scrollIntoView({ block: 'center' }); + }); setRequestedDocId(null); }, [data, requestedDocId]); @@ -335,7 +341,7 @@ export default function EvalSamplesDrawer({ {/* Body */} -
+
{liveUnavailable && (

{t.liveUnavailable} @@ -368,6 +374,7 @@ export default function EvalSamplesDrawer({ {filteredSamples.map((s) => (