Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions viewer/src/lib/PrimerDropdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,47 @@
let highlightedIndex = $state(-1);
let triggerButton: HTMLButtonElement;
let menuList: HTMLUListElement;
// The menu is rendered with `position: fixed` and anchored to the trigger's
// viewport rect so it can escape any ancestor with a scroll/clip context
// (e.g. the `overflow-x-auto` wrapper around the evaluation-results table).
// An absolutely-positioned menu is clipped by such an ancestor and also
// inflates its scrollWidth, which spawns a stray horizontal scrollbar.
let menuStyle = $state('');

const VIEWPORT_MARGIN = 8;
const TRIGGER_GAP = 4;
const MIN_MENU_WIDTH = 192; // matches the 12rem min-width in CSS

function positionMenu() {
if (!triggerButton || !menuList) return;
const anchor = triggerButton.getBoundingClientRect();
const menu = menuList.getBoundingClientRect();

// Prefer left-aligned to the trigger; flip to right-aligned when that would
// overflow the viewport, then clamp so the menu is never off-screen.
let left = anchor.left;
if (left + menu.width > window.innerWidth - VIEWPORT_MARGIN) {
left = anchor.right - menu.width;
}
left = Math.max(
VIEWPORT_MARGIN,
Math.min(left, window.innerWidth - menu.width - VIEWPORT_MARGIN)
);

// Prefer opening downward; flip above the trigger only when there is room.
let top = anchor.bottom + TRIGGER_GAP;
const overflowsBottom = top + menu.height > window.innerHeight - VIEWPORT_MARGIN;
const fitsAbove = anchor.top - TRIGGER_GAP - menu.height >= VIEWPORT_MARGIN;
if (overflowsBottom && fitsAbove) {
top = anchor.top - TRIGGER_GAP - menu.height;
}
top = Math.max(VIEWPORT_MARGIN, top);

menuStyle =
`position: fixed; top: ${Math.round(top)}px; left: ${Math.round(left)}px; ` +
`min-width: ${Math.round(Math.max(MIN_MENU_WIDTH, anchor.width))}px;`;
}


function handleSelect(value: string) {
selected = value;
Expand Down Expand Up @@ -107,6 +148,28 @@
}
});

$effect(() => {
if (!open) {
menuStyle = '';
return;
}
// First pass positions from the pre-layout rect; the rAF pass corrects once
// the menu has been laid out at its final size.
positionMenu();
const frame = requestAnimationFrame(positionMenu);

const reposition = () => positionMenu();
// Capture phase so scrolling of any ancestor container is observed too.
window.addEventListener('scroll', reposition, true);
window.addEventListener('resize', reposition);

return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', reposition, true);
window.removeEventListener('resize', reposition);
};
});

$effect(() => {
if (open && highlightedIndex >= 0 && menuList) {
const items = menuList.querySelectorAll('[role="option"]');
Expand Down Expand Up @@ -146,6 +209,7 @@
bind:this={menuList}
class="ActionList ActionMenu-list"
role="listbox"
style={menuStyle}
onkeydown={handleKeyDown}
onblur={handleMenuBlur}
tabindex={-1}
Expand Down
52 changes: 47 additions & 5 deletions viewer/src/routes/suite/[suite_id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@
<p class="text-sm text-text-secondary">No evaluation results yet.</p>
</div>
{:else}
<div class="overflow-hidden rounded-lg border border-border">
<div class="overflow-x-auto rounded-lg border border-border">
<table class="w-full text-left text-sm">
<thead>
<tr class="border-b border-border bg-surface">
Expand All @@ -833,7 +833,7 @@
<th class="px-3 py-2 text-xs font-medium text-text-muted">Run date</th>
<th class="px-3 py-2 text-xs font-medium text-text-muted">Run status</th>
{#each selectedMetricCols as colDim, colIdx (colIdx)}
<th class="w-32 px-3 py-2 text-left text-xs font-medium text-text-muted whitespace-nowrap">
<th class="metric-col px-3 py-2 text-left text-xs font-medium text-text-muted">
<PrimerDropdown
ariaLabel={`Metric column ${colIdx + 1}`}
selected={colDim ?? ''}
Expand Down Expand Up @@ -887,9 +887,9 @@
<svg class="h-3 w-3 transition-transform duration-150 {isExpanded ? 'rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M9 5l7 7-7 7"/></svg>
</button>
{/if}
<div class="min-w-0 flex flex-col">
<a href="/suite/{data.suite_id}/{run.prompt_run_id ?? run.audit_run_id ?? run.run_id}" class="text-sm font-medium text-interactive hover:underline">{run.run_id}</a>
<span class="font-mono text-[10px] text-text-muted truncate" title={runTarget(run)}>{runTarget(run)}</span>
<div class="flex min-w-0 max-w-[22rem] flex-col">
<a href="/suite/{data.suite_id}/{run.prompt_run_id ?? run.audit_run_id ?? run.run_id}" class="truncate text-sm font-medium text-interactive hover:underline">{run.run_id}</a>
<span class="truncate font-mono text-[10px] text-text-muted" title={runTarget(run)}>{runTarget(run)}</span>
</div>
</div>
</td>
Expand Down Expand Up @@ -1168,3 +1168,45 @@
{/if}
</div>
{/if}

<style>
/* The metric columns are user-selectable dropdowns whose labels ("Impermissible
behavior violated") are far wider than the column needs. PrimerDropdown's
trigger is a fixed-height, nowrap button, so the label alone set the column
width and pushed the trailing Total column out of the container. Let the
label wrap inside a fixed-width header instead of truncating it, so the whole
table fits without a horizontal scroll at the container's max width. */
.metric-col {
width: 10rem;
white-space: normal;
}

.metric-col :global(.ActionMenu) {
display: block;
}

.metric-col :global(button.ActionMenu-button) {
width: 100%;
height: auto;
min-height: 0;
align-items: flex-start;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
line-height: 1.25;
white-space: normal;
}

.metric-col :global(.ActionMenu-button-content) {
display: block;
min-width: 0;
}

.metric-col :global(.ActionMenu-button-value) {
white-space: normal;
overflow-wrap: anywhere;
}

.metric-col :global(.ActionMenu-button-chevron) {
margin-top: 1px;
}
</style>
Loading