diff --git a/js/app/src/Routes.tsx b/js/app/src/Routes.tsx index 8ec91ad0877..2ea3c98f235 100644 --- a/js/app/src/Routes.tsx +++ b/js/app/src/Routes.tsx @@ -91,9 +91,7 @@ import { LoggedOutPage, LoginPage, NewCodeProjectEvaluatorPage, - NewGalleryCodeProjectEvaluatorPage, NewGalleryLlmFromTemplateProjectEvaluatorPage, - NewGalleryLlmProjectEvaluatorPage, NewLlmProjectEvaluatorPage, OAuth2ConsentPage, PlaygroundPage, @@ -562,40 +560,62 @@ export const appRouteObjects = createRoutesFromElements( agentRoute: { label: "Project Evaluator Gallery", description: - "Browse evaluator templates and start a project evaluator from a template or from scratch.", + "Browse evaluator templates and create a project evaluator from a template, from scratch, or an existing evaluator.", }, }} > + } + handle={{ + agentRoute: { + label: "New Project Evaluator From Gallery Template", + description: + "Create a project LLM evaluator seeded from the selected evaluator gallery template.", + }, + }} + /> } + element={} handle={{ agentRoute: { label: "New Project LLM Evaluator From Gallery", description: - "Author a new LLM-as-a-judge evaluator from scratch while browsing the evaluator gallery.", + "Author a new LLM-as-a-judge evaluator for a project from scratch, started from the evaluator gallery.", }, }} /> } + element={} handle={{ agentRoute: { label: "New Project Code Evaluator From Gallery", description: - "Author a new Python or TypeScript code evaluator from scratch while browsing the evaluator gallery.", + "Author a new Python or TypeScript code evaluator for a project from scratch, started from the evaluator gallery.", }, }} /> } + path="new/copy/:evaluatorId" + element={} handle={{ agentRoute: { - label: "New Project Evaluator From Gallery Template", + label: "Copy LLM Evaluator Into Project From Gallery", description: - "Create a project LLM evaluator seeded from the selected evaluator gallery template.", + "Create a project evaluator seeded from an existing LLM evaluator while browsing the evaluator gallery. The evaluatorId route param uses the GraphQL Evaluator.id Relay node ID of the evaluator being copied.", + }, + }} + /> + } + handle={{ + agentRoute: { + label: "Attach Code Evaluator To Project From Gallery", + description: + "Attach an existing code evaluator to a project while browsing the evaluator gallery. The evaluatorId route param uses the GraphQL Evaluator.id Relay node ID of the evaluator being attached.", }, }} /> diff --git a/js/app/src/components/core/badge/types.ts b/js/app/src/components/core/badge/types.ts index 5b2415b46c4..3a6fc938858 100644 --- a/js/app/src/components/core/badge/types.ts +++ b/js/app/src/components/core/badge/types.ts @@ -39,4 +39,8 @@ export interface BadgeProps extends StylableProps { * @default 'wrap' */ overflowMode?: BadgeOverflowMode; + /** + * Native tooltip text shown on hover. + */ + title?: string; } diff --git a/js/app/src/components/core/token/Token.tsx b/js/app/src/components/core/token/Token.tsx index 1a8ecc62ee7..66c1421dec5 100644 --- a/js/app/src/components/core/token/Token.tsx +++ b/js/app/src/components/core/token/Token.tsx @@ -57,7 +57,6 @@ const tokenBaseCSS = css` visual's margin so the pill reads as one unit. */ gap: var(--global-dimension-size-50); font-size: var(--global-dimension-font-size-75); - line-height: var(--global-line-height-s); padding: 0 var(--global-dimension-size-100); border-radius: var(--global-rounding-large); border: 1px solid transparent; @@ -72,10 +71,14 @@ const tokenBaseCSS = css` &[data-size="S"] { height: var(--global-dimension-size-200); + /* Match line-height to height (both 16px) — otherwise the 20px "S" + body line-height overflows this pill's shorter box. */ + line-height: var(--global-line-height-xs); } &[data-size="M"] { height: var(--global-dimension-size-250); + line-height: var(--global-line-height-s); } &[data-size="L"] { @@ -83,6 +86,7 @@ const tokenBaseCSS = css` /* The large token scales its typography up to body size so token-heavy detail views stay readable; S and M keep the compact font. */ font-size: var(--global-dimension-font-size-100); + line-height: var(--global-line-height-m); } /* Center the leading visual and the remove button inside the pill's diff --git a/js/app/src/pages/project/evaluators/AddProjectEvaluatorMenu.tsx b/js/app/src/pages/project/evaluators/AddProjectEvaluatorMenu.tsx index b51219c24a4..0fdfed1eaec 100644 --- a/js/app/src/pages/project/evaluators/AddProjectEvaluatorMenu.tsx +++ b/js/app/src/pages/project/evaluators/AddProjectEvaluatorMenu.tsx @@ -24,19 +24,28 @@ import { Truncate } from "@phoenix/components/core/utility/Truncate"; import { View } from "@phoenix/components/core/view"; import type { projectEvaluatorOptionsQuery } from "@phoenix/pages/project/evaluators/__generated__/projectEvaluatorOptionsQuery.graphql"; import { projectEvaluatorOptionsQuery as projectEvaluatorOptionsQueryNode } from "@phoenix/pages/project/evaluators/projectEvaluatorOptions"; -import { useProjectEvaluatorPaths } from "@phoenix/pages/project/evaluators/projectEvaluatorPaths"; +import { + type ProjectEvaluatorCreationPaths, + useProjectEvaluatorPaths, +} from "@phoenix/pages/project/evaluators/projectEvaluatorPaths"; export const AddProjectEvaluatorMenu = ({ size, + buttonClassName, + buttonLabel = "Add evaluator", + shouldShowGalleryLink = true, + creationPaths, ...props }: ProjectEvaluatorMenuTriggerProps) => { return ( } />} - shouldShowGalleryLink + shouldShowGalleryLink={shouldShowGalleryLink} + creationPaths={creationPaths} {...props} /> ); @@ -59,14 +68,22 @@ export const BuildProjectEvaluatorMenu = ({ type ProjectEvaluatorMenuTriggerProps = { size: ButtonProps["size"]; + buttonClassName?: string; + buttonLabel?: string; + /** Hide the "Browse eval gallery" item, e.g. when already on the gallery page. */ + shouldShowGalleryLink?: boolean; + /** The routes to use for every evaluator-creation action in this menu. */ + creationPaths: ProjectEvaluatorCreationPaths; } & Omit; function ProjectEvaluatorMenu({ size, + buttonClassName, buttonLabel, buttonVariant, buttonLeadingVisual, shouldShowGalleryLink, + creationPaths, ...props }: ProjectEvaluatorMenuTriggerProps & { buttonLabel: string; @@ -77,6 +94,7 @@ function ProjectEvaluatorMenu({ return ( - + + + + + + {evaluationTargets.map((target) => ( + + {capitalize(formatEvaluationTargetPlural(target))} + + ))} + ); } @@ -377,19 +512,17 @@ function EvaluatorTemplateDetails({ optimizationDirection: template.optimizationDirection, values: choices, }); + const messages = getProjectEvaluatorTemplateMessages(template); return ( {template.name} - LLM + {getProjectEvaluatorTemplateCategoryLabel(template.category)} - - {template.description} - {template.details ? ( {template.details} @@ -400,7 +533,7 @@ function EvaluatorTemplateDetails({
- Scope + Target
@@ -424,7 +557,7 @@ function EvaluatorTemplateDetails({ - Output choices + Annotation values {choices.map(({ label, score }) => ( @@ -454,13 +587,72 @@ function EvaluatorTemplateDetails({ ))} - + {messages.length > 0 ? ( + + + Prompt + +
+ + + {messages.map((message) => ( + + + {capitalize(message.role)} + + + {message.content} + + + ))} + + +
+
+ ) : null} + + + ); } +// Bleeds out to the edges of the details column's own padding/gap (both +// `var(--global-dimension-size-200)`) and re-adds that same space as padding +// inside this element's own background, so nothing scrolls behind it. +const stickyUseTemplateFooterCSS = css` + position: sticky; + bottom: calc(-1 * var(--project-evaluator-gallery-column-padding)); + z-index: 1; + margin: calc(-1 * var(--global-dimension-size-200)) + calc(-1 * var(--project-evaluator-gallery-column-padding)) + calc(-1 * var(--project-evaluator-gallery-column-padding)); + padding: var(--global-dimension-size-200) + var(--project-evaluator-gallery-column-padding) + var(--project-evaluator-gallery-column-padding); + background-color: var(--global-background-color-default); +`; + +const PROMPT_PREVIEW_COLLAPSED_HEIGHT = 160; + +const promptPreviewWellCSS = css` + background-color: var(--global-background-color-100); + border: var(--global-border-size-thin) solid + var(--global-border-color-default); + border-radius: var(--global-rounding-medium); + padding: var(--global-dimension-size-150); +`; + +const promptPreviewMessageCSS = css` + white-space: pre-wrap; +`; + function capitalize(value: string): string { return `${value.charAt(0).toUpperCase()}${value.slice(1)}`; } @@ -517,7 +709,7 @@ const galleryCSS = css` ); --project-evaluator-gallery-column-padding: var(--global-dimension-size-200); --project-evaluator-gallery-template-card-min-width: var( - --global-dimension-size-3000 + --global-dimension-size-4600 ); --project-evaluator-gallery-template-column-min-width: calc( var(--project-evaluator-gallery-template-card-min-width) + @@ -567,9 +759,9 @@ const galleryCSS = css` } .project-evaluator-gallery__category-list { - flex: 1 1 auto; - min-height: 0; + flex: none; gap: var(--global-dimension-size-100); + overflow: visible; .react-aria-ListBoxSection { display: flex; @@ -584,28 +776,52 @@ const galleryCSS = css` } } + .project-evaluator-gallery__category-scroll-region { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + gap: var(--global-dimension-size-100); + overflow-y: auto; + } + .project-evaluator-gallery__category-section-heading { padding: var(--global-dimension-size-50) var(--global-dimension-size-100); letter-spacing: 0.06em; text-transform: uppercase; } - .project-evaluator-gallery__scratch-actions, - .project-evaluator-gallery__compact-scratch-actions { + .project-evaluator-gallery__add-evaluator-button { flex: none; - padding-top: var(--global-dimension-size-200); - border-top: var(--global-border-size-thin) solid - var(--global-border-color-default); + align-self: stretch; + width: 100%; + margin-bottom: var(--global-dimension-size-200); } - .project-evaluator-gallery__compact-category-select, - .project-evaluator-gallery__compact-scratch-actions { + .project-evaluator-gallery__compact-category-select { display: none; } - .project-evaluator-gallery__template-list { + .project-evaluator-gallery__compact-add-evaluator-menu { + display: none; + } + + .project-evaluator-gallery__template-card-scroll-region { flex: 1 1 auto; min-height: 0; + display: flex; + flex-direction: column; + gap: var(--global-dimension-size-400); + margin-top: var(--global-dimension-size-100); + overflow-y: auto; + scroll-behavior: smooth; + + @media (prefers-reduced-motion: reduce) { + scroll-behavior: auto; + } + } + + .project-evaluator-gallery__template-category-section { display: grid; grid-template-columns: repeat( auto-fit, @@ -613,16 +829,29 @@ const galleryCSS = css` ); align-content: start; gap: var(--global-dimension-size-100); - margin-top: var(--global-dimension-size-100); + } - .react-aria-ListBoxItem { - min-height: var(--global-dimension-size-1400); - gap: var(--global-dimension-size-100); - margin: 0; - padding: var(--global-dimension-size-150); - border: var(--global-border-size-thin) solid - var(--global-border-color-default); - } + .project-evaluator-gallery__template-category-header { + grid-column: 1 / -1; + } + + .project-evaluator-gallery__template-category-heading { + /* Anchor target for the category nav; offset so scrollIntoView doesn't + tuck it flush against the scroll region's top edge. */ + scroll-margin-top: var(--global-dimension-size-100); + } + + .project-evaluator-gallery__template-card-footer { + width: 100%; + margin-top: auto; + } + + .project-evaluator-gallery__template-kind { + flex: none; + } + + .project-evaluator-gallery__template-targets { + min-width: 0; } .project-evaluator-gallery__definition-list { @@ -662,27 +891,17 @@ const galleryCSS = css` width: 100%; } + .project-evaluator-gallery__compact-add-evaluator-menu { + display: flex; + flex: none; + justify-content: flex-start; + } + .project-evaluator-gallery__templates { grid-column: 1; grid-row: 1; } - .project-evaluator-gallery__template-list-title { - display: none; - } - - .project-evaluator-gallery__template-list { - grid-template-columns: minmax( - var(--project-evaluator-gallery-template-card-min-width), - 1fr - ); - } - - .project-evaluator-gallery__compact-scratch-actions { - display: flex; - margin-top: var(--global-dimension-size-200); - } - .project-evaluator-gallery__details { grid-column: 2; grid-row: 1; diff --git a/js/app/src/pages/project/evaluators/ProjectEvaluatorSlideoverRoutes.tsx b/js/app/src/pages/project/evaluators/ProjectEvaluatorSlideoverRoutes.tsx index a40b30b7f9f..bfc68f70f51 100644 --- a/js/app/src/pages/project/evaluators/ProjectEvaluatorSlideoverRoutes.tsx +++ b/js/app/src/pages/project/evaluators/ProjectEvaluatorSlideoverRoutes.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { useLazyLoadQuery } from "react-relay"; -import { useNavigate, useParams } from "react-router"; +import { useLocation, useNavigate, useParams } from "react-router"; import invariant from "tiny-invariant"; import type { projectEvaluatorDetailsQuery } from "@phoenix/pages/project/evaluators/__generated__/projectEvaluatorDetailsQuery.graphql"; @@ -18,7 +18,6 @@ import { projectEvaluatorDetailsQueryNode, UNSUPPORTED_PROMPT_TEMPLATE_ERROR, } from "@phoenix/pages/project/evaluators/projectEvaluatorOptions"; -import { useProjectEvaluatorPaths } from "@phoenix/pages/project/evaluators/projectEvaluatorPaths"; import { ProjectEvaluatorSlideoverError } from "@phoenix/pages/project/evaluators/ProjectEvaluatorSlideoverError"; import { buildTemplateCreationMode, @@ -26,29 +25,24 @@ import { } from "@phoenix/pages/project/evaluators/projectEvaluatorTemplates"; /** - * Closes the slideover by leaving its route — to the evaluators list unless - * the caller names another destination. Replaces rather than pushes, so a - * slideover the user has dismissed does not sit one step back in history - * waiting to be reopened. + * Closes the slideover by leaving its nested route for its parent page. + * Replaces rather than pushes, so a dismissed slideover does not sit one step + * back in history waiting to be reopened. The current search is retained so + * the parent page restores the view state the slideover was opened over. */ -function useCloseSlideover(to?: string) { +function useCloseSlideover() { const navigate = useNavigate(); - const { list } = useProjectEvaluatorPaths(); - const destination = to ?? list; + const { search } = useLocation(); return (isOpen: boolean) => { if (!isOpen) { - navigate(destination, { replace: true }); + navigate( + { pathname: "..", search }, + { relative: "route", replace: true } + ); } }; } -function useCloseGallerySlideover() { - // Gallery slideovers close over the selected category and template rather - // than resetting the gallery to its default view. - const { galleryReturn } = useProjectEvaluatorPaths(); - return useCloseSlideover(galleryReturn); -} - function useRouteProjectId() { const { projectId } = useParams(); invariant(projectId, "projectId is required"); @@ -84,19 +78,6 @@ export function NewLlmProjectEvaluatorPage() { ); } -export function NewGalleryLlmProjectEvaluatorPage() { - const projectId = useRouteProjectId(); - const onOpenChange = useCloseGallerySlideover(); - return ( - - ); -} - export function NewCodeProjectEvaluatorPage() { const projectId = useRouteProjectId(); const onOpenChange = useCloseSlideover(); @@ -110,24 +91,11 @@ export function NewCodeProjectEvaluatorPage() { ); } -export function NewGalleryCodeProjectEvaluatorPage() { - const projectId = useRouteProjectId(); - const onOpenChange = useCloseGallerySlideover(); - return ( - - ); -} - export function NewGalleryLlmFromTemplateProjectEvaluatorPage() { const projectId = useRouteProjectId(); const { templateName } = useParams(); invariant(templateName, "templateName is required"); - const onOpenChange = useCloseGallerySlideover(); + const onOpenChange = useCloseSlideover(); const data = useLazyLoadQuery( projectEvaluatorTemplatesQuery, {}, @@ -233,10 +201,7 @@ export function AttachCodeProjectEvaluatorPage() { export function EditProjectEvaluatorPage() { const { projectEvaluatorId } = useParams(); invariant(projectEvaluatorId, "projectEvaluatorId is required"); - // The edit route nests under the evaluator's details page, so closing lands - // on the details view it was opened over rather than the list. - const { details } = useProjectEvaluatorPaths(); - const onOpenChange = useCloseSlideover(details(projectEvaluatorId)); + const onOpenChange = useCloseSlideover(); const { evaluator, sandboxConfigs } = useProjectEvaluator(projectEvaluatorId); if ( evaluator.evaluator.kind !== "LLM" && diff --git a/js/app/src/pages/project/evaluators/ProjectEvaluatorsEmptyState.tsx b/js/app/src/pages/project/evaluators/ProjectEvaluatorsEmptyState.tsx index 71cce7c00e4..c46292a553a 100644 --- a/js/app/src/pages/project/evaluators/ProjectEvaluatorsEmptyState.tsx +++ b/js/app/src/pages/project/evaluators/ProjectEvaluatorsEmptyState.tsx @@ -1,6 +1,6 @@ import { css } from "@emotion/react"; import { type ReactNode, Suspense, useRef, useState } from "react"; -import { useLazyLoadQuery } from "react-relay"; +import { graphql, useLazyLoadQuery } from "react-relay"; import { Flex, @@ -13,20 +13,31 @@ import { Text, } from "@phoenix/components"; import { ErrorBoundary } from "@phoenix/components/exception"; -import type { projectEvaluatorTemplatesQuery as ProjectEvaluatorTemplatesQueryType } from "@phoenix/pages/project/evaluators/__generated__/projectEvaluatorTemplatesQuery.graphql"; +import type { + projectEvaluatorCategoryCardsQuery as ProjectEvaluatorCategoryCardsQueryType, + projectEvaluatorCategoryCardsQuery$data, +} from "@phoenix/pages/project/evaluators/__generated__/projectEvaluatorCategoryCardsQuery.graphql"; import { BuildProjectEvaluatorMenu } from "@phoenix/pages/project/evaluators/AddProjectEvaluatorMenu"; import { useProjectEvaluatorPaths } from "@phoenix/pages/project/evaluators/projectEvaluatorPaths"; -import { - PROJECT_EVALUATOR_CATEGORIES, - type ProjectEvaluatorTemplate, - projectEvaluatorTemplatesQuery, -} from "@phoenix/pages/project/evaluators/projectEvaluatorTemplates"; +import { PROJECT_EVALUATOR_CATEGORIES } from "@phoenix/pages/project/evaluators/projectEvaluatorTemplates"; const MAX_CATEGORY_TEMPLATES = 3; const CATEGORY_CARDS_PER_VIEW = 3; const CATEGORY_CARD_MIN_HEIGHT = 250; const CATEGORY_CAROUSEL_ID = "project-evaluator-category-carousel"; +const projectEvaluatorCategoryCardsQuery = graphql` + query projectEvaluatorCategoryCardsQuery { + evaluatorGalleryConfigs { + name + category + } + } +`; + +type ProjectEvaluatorCategoryCardTemplate = + projectEvaluatorCategoryCardsQuery$data["evaluatorGalleryConfigs"][number]; + export function ProjectEvaluatorsEmptyState() { const paths = useProjectEvaluatorPaths(); return ( @@ -43,9 +54,12 @@ export function ProjectEvaluatorsEmptyState() { - + - Browse the library + Browse eval gallery @@ -53,8 +67,8 @@ export function ProjectEvaluatorsEmptyState() { } function EvaluatorCategoryCards() { - const data = useLazyLoadQuery( - projectEvaluatorTemplatesQuery, + const data = useLazyLoadQuery( + projectEvaluatorCategoryCardsQuery, {}, { fetchPolicy: "store-and-network" } ); @@ -64,7 +78,7 @@ function EvaluatorCategoryCards() { function CategoryCards({ templates, }: { - templates: readonly ProjectEvaluatorTemplate[]; + templates: readonly ProjectEvaluatorCategoryCardTemplate[]; }) { const paths = useProjectEvaluatorPaths(); // Keep the full track mounted so native scrolling can animate continuously @@ -82,22 +96,37 @@ function CategoryCards({ const showCategoryAtIndex = (categoryIndex: number) => { const categoryCardList = categoryCardListRef.current; const targetCategoryCard = categoryCardList?.children.item(categoryIndex); - if (targetCategoryCard instanceof HTMLElement) { - // The scroll port's padding keeps adjacent cards peeking at the edges. - targetCategoryCard.scrollIntoView({ - block: "nearest", - inline: "start", + if (categoryCardList && targetCategoryCard instanceof HTMLElement) { + const categoryCardListRect = categoryCardList.getBoundingClientRect(); + const targetCategoryCardRect = targetCategoryCard.getBoundingClientRect(); + const scrollPaddingInlineStart = + Number.parseFloat( + getComputedStyle(categoryCardList).scrollPaddingInlineStart + ) || 0; + // Move only the carousel. scrollIntoView would also move the table's + // shared horizontal scroll container when the table overflows. + categoryCardList.scrollTo({ + left: + categoryCardList.scrollLeft + + targetCategoryCardRect.left - + categoryCardListRect.left - + scrollPaddingInlineStart, }); } setFirstVisibleCategoryIndex(categoryIndex); }; const showPreviousCategories = () => { - showCategoryAtIndex(Math.max(firstVisibleCategoryIndex - 1, 0)); + showCategoryAtIndex( + Math.max(firstVisibleCategoryIndex - CATEGORY_CARDS_PER_VIEW, 0) + ); }; const showNextCategories = () => { showCategoryAtIndex( - Math.min(firstVisibleCategoryIndex + 1, lastFirstVisibleCategoryIndex) + Math.min( + firstVisibleCategoryIndex + CATEGORY_CARDS_PER_VIEW, + lastFirstVisibleCategoryIndex + ) ); }; @@ -253,7 +282,7 @@ function CategoryCarouselControls({ const emptyStateContentCSS = css` box-sizing: border-box; margin-inline: auto; - padding: var(--global-dimension-size-400) 0 var(--global-dimension-size-600); + padding: var(--global-dimension-size-700) 0 var(--global-dimension-size-600); `; const categoryCarouselControlCSS = css` diff --git a/js/app/src/pages/project/evaluators/ProjectEvaluatorsPage.tsx b/js/app/src/pages/project/evaluators/ProjectEvaluatorsPage.tsx index b7911b710dc..a460d96b467 100644 --- a/js/app/src/pages/project/evaluators/ProjectEvaluatorsPage.tsx +++ b/js/app/src/pages/project/evaluators/ProjectEvaluatorsPage.tsx @@ -4,10 +4,11 @@ import { graphql, useLazyLoadQuery } from "react-relay"; import { Outlet, useParams } from "react-router"; import invariant from "tiny-invariant"; -import { Flex, PageHeader, Skeleton, View } from "@phoenix/components"; +import { Flex, Skeleton, Text, View } from "@phoenix/components"; import { useTimeRange } from "@phoenix/components/datetime"; import type { ProjectEvaluatorsPageQuery } from "@phoenix/pages/project/evaluators/__generated__/ProjectEvaluatorsPageQuery.graphql"; import { AddProjectEvaluatorMenu } from "@phoenix/pages/project/evaluators/AddProjectEvaluatorMenu"; +import { useProjectEvaluatorPaths } from "@phoenix/pages/project/evaluators/projectEvaluatorPaths"; import { ProjectEvaluatorsTable } from "@phoenix/pages/project/evaluators/ProjectEvaluatorsTable"; import { ProjectEvaluatorsToolbar } from "@phoenix/pages/project/evaluators/ProjectEvaluatorsToolbar"; @@ -80,17 +81,34 @@ function ProjectEvaluatorsPageContent({ { fetchPolicy: "store-and-network" } ); invariant(data.project, "project is required"); + const paths = useProjectEvaluatorPaths(); const isEmptyState = (data.project.evaluatorCount ?? 0) === 0 && filter.trim().length === 0; return ( <> {isEmptyState ? ( - - } - /> + + + + Evaluators read span inputs, outputs, retrieved documents, and + tool calls, then return labels or scores you can filter, chart, + and alert on. + + + ) : ( ); } + // hasNext means more evaluators exist beyond this page, so rows.length is + // only the true total once the full connection has loaded. + const showGalleryPromo = + !isFiltered && !hasNext && rows.length < GALLERY_PROMO_MAX_EVALUATOR_COUNT; return (
) : null} + {showGalleryPromo ? ( + + + + ) : null} ); } diff --git a/js/app/src/pages/project/evaluators/ProjectEvaluatorsToolbar.tsx b/js/app/src/pages/project/evaluators/ProjectEvaluatorsToolbar.tsx index f02f779a7c3..b134727ea26 100644 --- a/js/app/src/pages/project/evaluators/ProjectEvaluatorsToolbar.tsx +++ b/js/app/src/pages/project/evaluators/ProjectEvaluatorsToolbar.tsx @@ -1,5 +1,6 @@ import { DebouncedSearch, Flex, View } from "@phoenix/components"; import { AddProjectEvaluatorMenu } from "@phoenix/pages/project/evaluators/AddProjectEvaluatorMenu"; +import { useProjectEvaluatorPaths } from "@phoenix/pages/project/evaluators/projectEvaluatorPaths"; /** * The evaluators tab's own header: search on the left, creation on the right. @@ -13,6 +14,7 @@ export function ProjectEvaluatorsToolbar({ filter: string; onFilterChange: (filter: string) => void; }) { + const paths = useProjectEvaluatorPaths(); return ( - + diff --git a/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorCategoryCardsQuery.graphql.ts b/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorCategoryCardsQuery.graphql.ts new file mode 100644 index 00000000000..f340ad66ac0 --- /dev/null +++ b/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorCategoryCardsQuery.graphql.ts @@ -0,0 +1,82 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type EvaluatorCategory = "AGENTS" | "GROUNDING_AND_RETRIEVAL" | "RESPONSE_QUALITY" | "SAFETY_AND_SECURITY" | "USER_EXPERIENCE"; +export type projectEvaluatorCategoryCardsQuery$variables = Record; +export type projectEvaluatorCategoryCardsQuery$data = { + readonly evaluatorGalleryConfigs: ReadonlyArray<{ + readonly category: EvaluatorCategory | null; + readonly name: string; + }>; +}; +export type projectEvaluatorCategoryCardsQuery = { + response: projectEvaluatorCategoryCardsQuery$data; + variables: projectEvaluatorCategoryCardsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "alias": null, + "args": null, + "concreteType": "ClassificationEvaluatorConfig", + "kind": "LinkedField", + "name": "evaluatorGalleryConfigs", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "projectEvaluatorCategoryCardsQuery", + "selections": (v0/*:: as any*/), + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "projectEvaluatorCategoryCardsQuery", + "selections": (v0/*:: as any*/) + }, + "params": { + "cacheID": "37f203807a26e3217d23ff16be421fca", + "id": null, + "metadata": {}, + "name": "projectEvaluatorCategoryCardsQuery", + "operationKind": "query", + "text": "query projectEvaluatorCategoryCardsQuery {\n evaluatorGalleryConfigs {\n name\n category\n }\n}\n" + } +}; +})(); + +(node as any).hash = "e41ebc1806e1a898862f3022618e20b9"; + +export default node; diff --git a/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorTemplatesQuery.graphql.ts b/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorTemplatesQuery.graphql.ts index aefb875852f..2f674d903cb 100644 --- a/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorTemplatesQuery.graphql.ts +++ b/js/app/src/pages/project/evaluators/__generated__/projectEvaluatorTemplatesQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<71a77d7dc4a2b943c67f5d9f41d16c54>> + * @generated SignedSource<<3ba5bece667c2aeb6e53064903e2ff41>> * @lightSyntaxTransform */ @@ -24,7 +24,6 @@ export type projectEvaluatorTemplatesQuery$data = { }>; readonly name: string; readonly optimizationDirection: OptimizationDirection; - readonly recommended: boolean; readonly scope: EvaluatorScope | null; }>; }; @@ -70,27 +69,20 @@ v4 = { "storageKey": null }, v5 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "recommended", - "storageKey": null -}, -v6 = { "alias": null, "args": null, "kind": "ScalarField", "name": "category", "storageKey": null }, -v7 = { +v6 = { "alias": null, "args": null, "kind": "ScalarField", "name": "details", "storageKey": null }, -v8 = { +v7 = { "kind": "InlineFragment", "selections": [ { @@ -115,7 +107,7 @@ v8 = { "type": "TextContentPart", "abstractKey": null }, -v9 = { +v8 = { "alias": null, "args": null, "kind": "ScalarField", @@ -144,7 +136,6 @@ return { (v4/*:: as any*/), (v5/*:: as any*/), (v6/*:: as any*/), - (v7/*:: as any*/), { "alias": null, "args": null, @@ -165,11 +156,11 @@ return { "name": "content", "plural": true, "selections": [ - (v8/*:: as any*/) + (v7/*:: as any*/) ], "storageKey": null }, - (v9/*:: as any*/) + (v8/*:: as any*/) ], "args": null, "argumentDefinitions": [] @@ -205,7 +196,6 @@ return { (v4/*:: as any*/), (v5/*:: as any*/), (v6/*:: as any*/), - (v7/*:: as any*/), { "alias": null, "args": null, @@ -229,11 +219,11 @@ return { "name": "__typename", "storageKey": null }, - (v8/*:: as any*/) + (v7/*:: as any*/) ], "storageKey": null }, - (v9/*:: as any*/) + (v8/*:: as any*/) ], "storageKey": null } @@ -243,16 +233,16 @@ return { ] }, "params": { - "cacheID": "df501872be9d85392c84aca78c3a9bdb", + "cacheID": "f0e73fa7c6554ae4221a97efd090ac9c", "id": null, "metadata": {}, "name": "projectEvaluatorTemplatesQuery", "operationKind": "query", - "text": "query projectEvaluatorTemplatesQuery {\n evaluatorGalleryConfigs {\n name\n description\n choices\n optimizationDirection\n scope\n recommended\n category\n details\n messages {\n ...promptUtils_promptMessages\n }\n }\n}\n\nfragment promptUtils_promptMessages on PromptMessage {\n content {\n __typename\n ... on TextContentPart {\n text {\n text\n }\n }\n }\n role\n}\n" + "text": "query projectEvaluatorTemplatesQuery {\n evaluatorGalleryConfigs {\n name\n description\n choices\n optimizationDirection\n scope\n category\n details\n messages {\n ...promptUtils_promptMessages\n }\n }\n}\n\nfragment promptUtils_promptMessages on PromptMessage {\n content {\n __typename\n ... on TextContentPart {\n text {\n text\n }\n }\n }\n role\n}\n" } }; })(); -(node as any).hash = "0e340a9d94ce94b0cc95de808a9b6de5"; +(node as any).hash = "157fdc1db6e6d4a22592570e2efd0a2d"; export default node; diff --git a/js/app/src/pages/project/evaluators/__tests__/projectEvaluatorPaths.test.tsx b/js/app/src/pages/project/evaluators/__tests__/projectEvaluatorPaths.test.tsx index 2db3eaddff6..75cfcfac354 100644 --- a/js/app/src/pages/project/evaluators/__tests__/projectEvaluatorPaths.test.tsx +++ b/js/app/src/pages/project/evaluators/__tests__/projectEvaluatorPaths.test.tsx @@ -26,7 +26,20 @@ function TestProjectEvaluatorPaths() { return ( { - it("separates the default gallery entry and slideover return destinations", () => { + it("builds list and gallery destinations while preserving view state", () => { act(() => { root.render( { }); const output = container.querySelector("output"); - expect(output?.getAttribute("data-gallery-return")).toBe( - "/projects/project-1/evaluator-gallery?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" - ); expect(output?.getAttribute("data-gallery")).toBe( "/projects/project-1/evaluator-gallery?timeRangeKey=7d&proof=preserved" ); + expect(output?.getAttribute("data-list-new-llm")).toBe( + "/projects/project-1/evaluators/new/llm?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-list-new-code")).toBe( + "/projects/project-1/evaluators/new/code?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-list-copy-llm")).toBe( + "/projects/project-1/evaluators/new/copy/Evaluator%3Allm%2Fsource?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-list-attach-code")).toBe( + "/projects/project-1/evaluators/new/attach/Evaluator%3Acode%2Fsource?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-gallery-new-llm")).toBe( + "/projects/project-1/evaluator-gallery/new/llm?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-gallery-new-code")).toBe( + "/projects/project-1/evaluator-gallery/new/code?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-gallery-copy-llm")).toBe( + "/projects/project-1/evaluator-gallery/new/copy/Evaluator%3Allm%2Fsource?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); + expect(output?.getAttribute("data-gallery-attach-code")).toBe( + "/projects/project-1/evaluator-gallery/new/attach/Evaluator%3Acode%2Fsource?timeRangeKey=7d&category=AGENTS&template=Hallucination&proof=preserved" + ); expect(output?.getAttribute("data-response-quality-gallery")).toBe( "/projects/project-1/evaluator-gallery?timeRangeKey=7d&category=RESPONSE_QUALITY&proof=preserved" ); diff --git a/js/app/src/pages/project/evaluators/projectEvaluatorPaths.ts b/js/app/src/pages/project/evaluators/projectEvaluatorPaths.ts index 7fdb3506d8d..597cb4ff9ce 100644 --- a/js/app/src/pages/project/evaluators/projectEvaluatorPaths.ts +++ b/js/app/src/pages/project/evaluators/projectEvaluatorPaths.ts @@ -25,6 +25,13 @@ export const newLlmProjectEvaluatorPath = (projectRootPath: string) => export const newCodeProjectEvaluatorPath = (projectRootPath: string) => `${projectEvaluatorsPath(projectRootPath)}/new/code`; +export type ProjectEvaluatorCreationPaths = { + newLlm: string; + newCode: string; + copyLlm: (evaluatorId: string) => string; + attachCode: (evaluatorId: string) => string; +}; + /** * The evaluator slideover paths for the project currently in the URL. * @@ -45,6 +52,20 @@ export function useProjectEvaluatorPaths() { const list = projectEvaluatorsPath(rootPath); const gallery = projectEvaluatorGalleryPath(rootPath); const withCurrentSearch = (path: string) => `${path}${search}`; + const buildCreationPaths = ( + parentPath: string + ): ProjectEvaluatorCreationPaths => ({ + newLlm: withCurrentSearch(`${parentPath}/new/llm`), + newCode: withCurrentSearch(`${parentPath}/new/code`), + copyLlm: (evaluatorId: string) => + withCurrentSearch( + `${parentPath}/new/copy/${encodeURIComponent(evaluatorId)}` + ), + attachCode: (evaluatorId: string) => + withCurrentSearch( + `${parentPath}/new/attach/${encodeURIComponent(evaluatorId)}` + ), + }); // A fresh gallery entry clears stale selection while preserving unrelated // project-page state in the query string. const defaultGallerySearch = withSearchParams(search, (searchParams) => { @@ -54,8 +75,6 @@ export function useProjectEvaluatorPaths() { return { list: withCurrentSearch(list), gallery: `${gallery}${defaultGallerySearch}`, - // Nested gallery routes use this exact return URL when they close. - galleryReturn: withCurrentSearch(gallery), galleryCategory: (category: EvaluatorCategory) => `${gallery}${withSearchParams(search, (searchParams) => { searchParams.set(PROJECT_EVALUATOR_CATEGORY_PARAM, category); @@ -72,22 +91,12 @@ export function useProjectEvaluatorPaths() { searchParams.set(PROJECT_EVALUATOR_CATEGORY_PARAM, category); searchParams.set(PROJECT_EVALUATOR_TEMPLATE_PARAM, templateName); })}`, - newLlm: withCurrentSearch(newLlmProjectEvaluatorPath(rootPath)), - newCode: withCurrentSearch(newCodeProjectEvaluatorPath(rootPath)), - galleryNewLlm: withCurrentSearch(`${gallery}/new/llm`), + listCreation: buildCreationPaths(list), + galleryCreation: buildCreationPaths(gallery), galleryNewLlmFromTemplate: (templateName: string) => withCurrentSearch( `${gallery}/new/template/${encodeURIComponent(templateName)}` ), - galleryNewCode: withCurrentSearch(`${gallery}/new/code`), - copyLlm: (evaluatorId: string) => - withCurrentSearch( - `${list}/new/copy/${encodeURIComponent(evaluatorId)}` - ), - attachCode: (evaluatorId: string) => - withCurrentSearch( - `${list}/new/attach/${encodeURIComponent(evaluatorId)}` - ), details: (projectEvaluatorId: string) => withCurrentSearch(`${list}/${encodeURIComponent(projectEvaluatorId)}`), edit: (projectEvaluatorId: string) => diff --git a/js/app/src/pages/project/evaluators/projectEvaluatorTemplates.ts b/js/app/src/pages/project/evaluators/projectEvaluatorTemplates.ts index c24e05ca612..f1f08933a88 100644 --- a/js/app/src/pages/project/evaluators/projectEvaluatorTemplates.ts +++ b/js/app/src/pages/project/evaluators/projectEvaluatorTemplates.ts @@ -16,7 +16,6 @@ export const projectEvaluatorTemplatesQuery = graphql` choices optimizationDirection scope - recommended category details messages { @@ -78,6 +77,14 @@ export function getProjectEvaluatorTemplateChoices(config: { return Object.entries(choices).map(([label, score]) => ({ label, score })); } +export function getProjectEvaluatorTemplateMessages( + config: ProjectEvaluatorTemplate +) { + return convertPromptVersionMessagesToPlaygroundInstanceMessages({ + promptMessagesRefs: config.messages, + }); +} + export function buildTemplateCreationMode( config: ProjectEvaluatorTemplate ): ProjectEvaluatorCreationMode { @@ -94,11 +101,7 @@ export function buildTemplateCreationMode( values: getProjectEvaluatorTemplateChoices(config), }, ], - defaultMessages: convertPromptVersionMessagesToPlaygroundInstanceMessages( - { - promptMessagesRefs: config.messages, - } - ), + defaultMessages: getProjectEvaluatorTemplateMessages(config), templateFormat: "MUSTACHE", includeExplanation: true, }, diff --git a/js/app/tests/projects.spec.ts b/js/app/tests/projects.spec.ts index 1bf7ecb51f2..acb9b3d99ab 100644 --- a/js/app/tests/projects.spec.ts +++ b/js/app/tests/projects.spec.ts @@ -215,7 +215,7 @@ test.describe.serial("Projects", () => { page.getByRole("heading", { name: "Evaluators", exact: true }) ).toBeVisible(); await expect( - page.getByRole("link", { name: "Browse the library" }) + page.getByRole("link", { name: "Browse eval gallery" }) ).toBeVisible(); await expect( page.getByRole("button", { name: "Build from scratch" }) @@ -230,7 +230,7 @@ test.describe.serial("Projects", () => { await page.getByRole("button", { name: "Build from scratch" }).click(); await expect( - page.getByRole("menuitem", { name: "Browse the whole library" }) + page.getByRole("menuitem", { name: "Browse eval gallery" }) ).toHaveCount(0); await page .getByRole("menuitem", { name: "Create new LLM evaluator" })