Skip to content
Merged
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
8 changes: 6 additions & 2 deletions specs/datasets.md
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,9 @@ that one `DatasetRowUpdate` can address:
- **An instance's argument**, reached directly (`seededTask` › `title`) or through references
(`toolsContext` › … › `rootTaskId` › `title`). The edit replaces that instance in the update's
`resources` with the argument changed. Editing the focus task's title from the grid is the case
this exists for.
this exists for. Clearing one in place (Delete, or emptying the editor) does nothing: it would
delete the argument, and an argument that isn't there has nothing to type into. The details pane
removes an argument.

Anything nested inside an argument's value object stays read-only for now. `resources` updates
replace a whole instance, which is fine for a top-level argument but would mean rebuilding a nested
Expand All @@ -1037,7 +1039,9 @@ value from a path. The details pane already edits those.
`dataset-provider-contract.ts`.
3. `columns` and `rowLabel`: the migration, `setColumns`, the route, validation, and
`deleteField` pruning its columns. The server checks that a path's root exists in the schema or
in some row, and that `keys` are non-empty strings.
in some row, and that `keys` are non-empty strings. An instance root the stored layout or row
label already has is accepted even when no row declares it any more, so a layout can always be
saved; it reads `"absent"` until a row declares the instance again.
4. `reconcileColumns`, as a pure function next to `parseTraceColumns`'s pattern. Unit tests cover
a dropped field, a new field and a new key being appended, a duplicate path, and a stored list
with no `columns`, which must produce today's layout.
Expand Down
39 changes: 39 additions & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
TraceCheckResult,
} from "../eval/eval-types";
import type { AgentContext, AgentInfo } from "../shared/agent";
import type { ColumnPath, DatasetColumnSpec } from "../shared/column-paths";
import type { AddDatasetFieldRequest } from "../shared/dataset-fields";
import type { SetupTask } from "../shared/setup-task";
import type {
Expand All @@ -24,6 +25,7 @@ import type {
ConflictChoices,
Dataset,
DatasetField,
DatasetPathChild,
DatasetProviderInfo,
DatasetRow,
DatasetRowsOverview,
Expand Down Expand Up @@ -778,6 +780,43 @@ export async function deleteDatasetField(
await throwIfError(res);
}

/**
* The keys one level inside `path` in a dataset's rows, each with a sample
* value — what the column picker's path tree expands a node into.
*/
export async function describeDatasetPath(
providerId: string,
datasetId: string,
path: ColumnPath,
): Promise<DatasetPathChild[]> {
const res = await fetch(datasetUrl(providerId, datasetId, "/paths"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path }),
});
await throwIfError(res);
return res.json();
}

/**
* Replaces a dataset's shared column layout and row label (`null` clears
* it). Resolves to the updated dataset.
*/
export async function setDatasetColumns(
providerId: string,
datasetId: string,
columns: DatasetColumnSpec[],
rowLabel: ColumnPath | null,
): Promise<Dataset> {
const res = await fetch(datasetUrl(providerId, datasetId, "/columns"), {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ columns, rowLabel }),
});
await throwIfError(res);
return res.json();
}

/** Deletes one row of a dataset. */
export async function deleteDatasetRow(
providerId: string,
Expand Down
19 changes: 1 addition & 18 deletions src/client/components/AskAgentButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState } from "react";
import { createPortal } from "react-dom";
import { type AgentInfo, OTHER_AGENT_URL } from "../../shared/agent";
import ProviderIcon from "./ProviderIcon";
import { ChevronDownIcon } from "./trace/icons";
import { useAnchoredPopover } from "./use-anchored-popover";

interface Props {
Expand Down Expand Up @@ -148,21 +149,3 @@ function AgentIcon() {
</svg>
);
}

function ChevronDownIcon() {
return (
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<polyline points="6 9 12 15 18 9" />
</svg>
);
}
79 changes: 64 additions & 15 deletions src/client/components/DatasetColumnMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import type { DatasetField } from "../../shared/types";
import { useAnchoredPopover } from "./use-anchored-popover";

interface Props {
/** The field the menu is for. */
field: DatasetField;
/**
* The field the column is, when it's a field's whole column — which is
* what offers renaming and deleting.
*/
field?: DatasetField;
/** The column header's place on screen, which the menu opens below. */
anchor: Rectangle;
/**
Expand All @@ -19,12 +22,20 @@ interface Props {
onRename: (name: string) => Promise<void>;
/** The field is to be deleted; the caller confirms, then closes the menu. */
onDelete: () => void;
/** Hides the column. */
onHide: () => void;
/**
* Whether the column is the row label, and makes it so or stops it being
* so — absent for a column that can't name a row (the source, say).
*/
rowLabel?: { isLabel: boolean; toggle: () => void };
/** Called when the menu is dismissed or has done its job. */
onClose: () => void;
}

/**
* The menu a dataset grid's column header opens: rename the field, or delete
* The menu a dataset grid's column header opens: hide the column, make it
* the row label, and — for a field's own column — rename the field or delete
* it. Renaming swaps the menu for a name box in place. Placed against `anchor`
* — the header cell's rectangle, which Glide reports but draws on a canvas —
* through an invisible fixed element standing in for a trigger.
Expand All @@ -34,10 +45,12 @@ export function DatasetColumnMenu({
anchor,
onRename,
onDelete,
onHide,
rowLabel,
onClose,
}: Props) {
const [renaming, setRenaming] = useState(false);
const [name, setName] = useState(field.def.name);
const [name, setName] = useState(field?.def.name ?? "");
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const { triggerRef, popoverRef, style } = useAnchoredPopover<HTMLSpanElement>(
Expand All @@ -47,7 +60,7 @@ export function DatasetColumnMenu({
const submit = async () => {
const trimmed = name.trim();
if (busy) return;
if (!trimmed || trimmed === field.def.name) return onClose();
if (!trimmed || trimmed === field?.def.name) return onClose();
setBusy(true);
try {
await onRename(trimmed);
Expand Down Expand Up @@ -125,18 +138,54 @@ export function DatasetColumnMenu({
type="button"
role="menuitem"
className="trace-header-menu-item"
onClick={() => setRenaming(true)}
>
<span className="trace-header-menu-item-label">Rename field…</span>
</button>
<button
type="button"
role="menuitem"
className="trace-header-menu-item dataset-menu-delete"
onClick={onDelete}
onClick={() => {
onHide();
onClose();
}}
>
<span className="trace-header-menu-item-label">Delete field</span>
<span className="trace-header-menu-item-label">Hide column</span>
</button>
{rowLabel && (
<button
type="button"
role="menuitem"
className="trace-header-menu-item"
onClick={() => {
rowLabel.toggle();
onClose();
}}
>
<span className="trace-header-menu-item-label">
{rowLabel.isLabel
? "Don't use as row label"
: "Use as row label"}
</span>
</button>
)}
{field && (
<>
<button
type="button"
role="menuitem"
className="trace-header-menu-item"
onClick={() => setRenaming(true)}
>
<span className="trace-header-menu-item-label">
Rename field…
</span>
</button>
<button
type="button"
role="menuitem"
className="trace-header-menu-item dataset-menu-delete"
onClick={onDelete}
>
<span className="trace-header-menu-item-label">
Delete field
</span>
</button>
</>
)}
</div>
)}
</>,
Expand Down
Loading
Loading