diff --git a/app/.eslintrc.js b/app/.eslintrc.js index a0c09d9593..2143ab9569 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -54,6 +54,11 @@ module.exports = { ], "deprecate/import": [ "error", + { + name: "Accordion", + module: "@arizeai/components", + use: "import { DisclosureGroup, Disclosure, DisclosureTrigger, DisclosurePanel } from '@phoenix/components'", + }, { name: "Button", module: "@arizeai/components", diff --git a/app/src/components/StopPropagation.tsx b/app/src/components/StopPropagation.tsx new file mode 100644 index 0000000000..fe11164061 --- /dev/null +++ b/app/src/components/StopPropagation.tsx @@ -0,0 +1,21 @@ +import React, { PropsWithChildren } from "react"; + +/** + * A component that stops the propagation of events. + * + * This is useful for preventing events from bubbling up to the parent component. + * Such as when buttons are nested, like inside of a DisclosureTrigger. + */ +export function StopPropagation({ children }: PropsWithChildren) { + return ( +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > + {children} +
+ ); +} diff --git a/app/src/components/button/styles.tsx b/app/src/components/button/styles.tsx index 7e28ecbcfe..6d13fcb1ec 100644 --- a/app/src/components/button/styles.tsx +++ b/app/src/components/button/styles.tsx @@ -14,8 +14,14 @@ export const buttonCSS = css` cursor: pointer; /* Disable outline since there are other mechanisms to show focus */ outline: none; + &[data-focus-visible] { + // Only show outline on focus-visible, aka only when tabbed but not clicked + outline: 1px solid var(--ac-global-input-field-border-color-active); + outline-offset: 1px; + } &:not([disabled]) { transition: all 0.2s ease-in-out; + transition: outline 0s; } &[disabled] { cursor: default; diff --git a/app/src/components/disclosure/Disclosure.tsx b/app/src/components/disclosure/Disclosure.tsx new file mode 100644 index 0000000000..9971033e73 --- /dev/null +++ b/app/src/components/disclosure/Disclosure.tsx @@ -0,0 +1,96 @@ +import React, { PropsWithChildren } from "react"; +import { + Button, + Disclosure as AriaDisclosure, + DisclosureGroup as AriaDisclosureGroup, + type DisclosureGroupProps as AriaDisclosureGroupProps, + DisclosurePanel as AriaDisclosurePanel, + type DisclosurePanelProps as AriaDisclosurePanelProps, + type DisclosureProps as AriaDisclosureProps, + Heading, +} from "react-aria-components"; + +import { Flex, Icon, Icons } from "@phoenix/components"; + +import { FlexStyleProps, SizingProps } from "../types"; + +import { disclosureCSS, disclosureGroupCSS } from "./styles"; + +export type DisclosureGroupProps = AriaDisclosureGroupProps; + +/** + * Wrap multiple Disclosure components in a DisclosureGroup to control + * the expanded state of the items more easily. + * + * AKA Accordion with one or more items + */ +export const DisclosureGroup = (props: DisclosureGroupProps) => { + return ( + + ); +}; + +export type DisclosureProps = AriaDisclosureProps & SizingProps; + +/** + * A Disclosure is a component that allows for a single item to be expanded. + * + * AKA Accordion (with a single item) / Accordion Item + */ +export const Disclosure = ({ size, ...props }: DisclosureProps) => { + return ( + + ); +}; + +export type DisclosurePanelProps = AriaDisclosurePanelProps; + +/** + * A DisclosurePanel is a component that contains the content of a Disclosure. + * + * AKA Accordion Content + */ +export const DisclosurePanel = (props: DisclosurePanelProps) => { + return ; +}; + +export type DisclosureTriggerProps = PropsWithChildren<{ + arrowPosition?: "start" | "end"; + justifyContent?: FlexStyleProps["justifyContent"]; +}>; + +/** + * A DisclosureTrigger is a component that triggers the Disclosure. + * + * AKA Accordion Title + */ +export const DisclosureTrigger = ({ + children, + arrowPosition, + justifyContent, +}: DisclosureTriggerProps) => { + return ( + + + + ); +}; diff --git a/app/src/components/disclosure/index.ts b/app/src/components/disclosure/index.ts new file mode 100644 index 0000000000..aecee5f509 --- /dev/null +++ b/app/src/components/disclosure/index.ts @@ -0,0 +1 @@ +export * from "./Disclosure"; diff --git a/app/src/components/disclosure/styles.tsx b/app/src/components/disclosure/styles.tsx new file mode 100644 index 0000000000..a5f09794ea --- /dev/null +++ b/app/src/components/disclosure/styles.tsx @@ -0,0 +1,99 @@ +import { css } from "@emotion/react"; + +export const disclosureGroupCSS = css` + & > * { + width: 100%; + .react-aria-Heading { + width: 100%; + .react-aria-Button[slot="trigger"] { + width: 100%; + } + } + } + + // add border between items, only when child is expanded + > *:not(:last-child) { + &[data-expanded] { + border-bottom: 1px solid var(--ac-global-border-color-default); + } + } +`; + +export const disclosureCSS = css` + .react-aria-Heading { + margin: 0; + } + + .react-aria-Button[slot="trigger"] { + // reset trigger styles + background: none; + border: none; + box-shadow: none; + font-size: 16px; + font-weight: 400; + line-height: 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: var(--ac-global-dimension-static-size-100) + var(--ac-global-dimension-static-size-200); + + // style trigger + color: var(--ac-global-text-color-900); + border-bottom: 1px solid var(--ac-global-border-color-default); + outline: none; + background-color: transparent; + &:hover:not([disabled]) { + background-color: var(--ac-global-input-field-background-color-active); + } + &[data-focus-visible] { + outline: 1px solid var(--ac-global-input-field-border-color-active); + outline-offset: -1px; + } + &:not([disabled]) { + transition: all 0.2s ease-in-out; + } + &[disabled] { + cursor: default; + opacity: 0.6; + } + + // style trigger icon + > svg, + > i { + rotate: 90deg; + transition: rotate 200ms; + width: 1em; + height: 1em; + fill: currentColor; + } + + &[data-arrow-position="start"] { + flex-direction: row-reverse; + > svg, + > i { + rotate: 0deg; + } + } + } + + &[data-size="L"] .react-aria-Button[slot="trigger"] { + height: 48px; + max-height: 48px; + } + + &[data-expanded] .react-aria-Button[slot="trigger"] { + > svg, + > i { + rotate: -90deg; + } + + &[data-arrow-position="start"] { + > svg, + > i { + rotate: 90deg; + } + } + } +`; diff --git a/app/src/components/index.tsx b/app/src/components/index.tsx index fbd3ab3163..fc8649927b 100644 --- a/app/src/components/index.tsx +++ b/app/src/components/index.tsx @@ -15,6 +15,7 @@ export * from "./ViewSummaryAside"; export * from "./CopyToClipboardButton"; // design system based components +export * from "./disclosure"; export * from "./combobox"; export * from "./button"; export * from "./icon"; diff --git a/app/src/pages/embedding/EventDetails.tsx b/app/src/pages/embedding/EventDetails.tsx index 6d682b8fa9..1c51188c45 100644 --- a/app/src/pages/embedding/EventDetails.tsx +++ b/app/src/pages/embedding/EventDetails.tsx @@ -7,9 +7,19 @@ import { } from "@tanstack/react-table"; import { css } from "@emotion/react"; -import { Accordion, AccordionItem, Counter, Label } from "@arizeai/components"; +import { Counter, Label } from "@arizeai/components"; -import { Flex, Heading, Icon, Icons, View } from "@phoenix/components"; +import { + Disclosure, + DisclosureGroup, + DisclosurePanel, + DisclosureTrigger, + Flex, + Heading, + Icon, + Icons, + View, +} from "@phoenix/components"; import { Empty } from "@phoenix/components/Empty"; import { tableCSS } from "@phoenix/components/table/styles"; import { numberFormatter } from "@phoenix/utils/numberFormatUtils"; @@ -73,101 +83,117 @@ export function EventDetails({ event }: { event: ModelEvent }) { `} > - + {isPredictionRecord ? ( - -
- {event.predictionId != null && ( -
-
Prediction ID
-
- {event.predictionId} -
-
- )} - {event.predictionLabel != null && ( -
-
Prediction Label
-
{event.predictionLabel}
-
- )} - {event.predictionScore != null && ( -
-
Prediction Score
-
{event.predictionScore}
-
- )} - {event.actualLabel != null && ( -
-
Actual Label
-
{event.actualLabel}
-
- )} - {event.actualScore != null && ( -
-
Actual Score
-
{event.actualScore}
-
- )} -
-
+ + Prediction Details + +
+ {event.predictionId != null && ( +
+
Prediction ID
+
+ {event.predictionId} +
+
+ )} + {event.predictionLabel != null && ( +
+
Prediction Label
+
{event.predictionLabel}
+
+ )} + {event.predictionScore != null && ( +
+
Prediction Score
+
{event.predictionScore}
+
+ )} + {event.actualLabel != null && ( +
+
Actual Label
+
{event.actualLabel}
+
+ )} + {event.actualScore != null && ( +
+
Actual Score
+
{event.actualScore}
+
+ )} +
+
+
) : ( - -
- {event.predictionId != null && ( -
-
Document ID
-
- {/* TODO - find a way to make the ID more semantic like a record ID */} - {event.predictionId} -
-
- )} -
-
+ + Document Details + +
+ {event.predictionId != null && ( +
+
Document ID
+
+ {/* TODO - find a way to make the ID more semantic like a record ID */} + {event.predictionId} +
+
+ )} +
+
+
)} - - - + + Dimensions + + + + {hasRetrievals && ( - + + Retrieved Documents {event.retrievedDocuments.length} - } - > -
    - {event.retrievedDocuments.map((document) => { - return ( -
  • - -
  • - ); - })} -
-
+ + +
    + {event.retrievedDocuments.map((document) => { + return ( +
  • + +
  • + ); + })} +
+
+ )} -
+ ); } @@ -353,32 +379,40 @@ function EventPreview({ event }: { event: ModelEvent }) { {rawData && ( - - + + Raw Data + {rawData} - - + + )} ); } else if (documentText) { content = ( - - + + Document + {documentText} - - + + ); } else if (promptAndResponse) { content = ( - - - {promptAndResponse.prompt} - - - {promptAndResponse.response} - - + + + Prompt + + {promptAndResponse.prompt} + + + + Response + + {promptAndResponse.response} + + + ); } else if (event.rawData) { { diff --git a/app/src/pages/embedding/ExportSelectionButton.tsx b/app/src/pages/embedding/ExportSelectionButton.tsx index 0feb433b58..b00deb2ea5 100644 --- a/app/src/pages/embedding/ExportSelectionButton.tsx +++ b/app/src/pages/embedding/ExportSelectionButton.tsx @@ -6,8 +6,6 @@ import CodeMirror from "@uiw/react-codemirror"; import { css } from "@emotion/react"; import { - Accordion, - AccordionItem, Alert, Dialog, DialogContainer, @@ -16,7 +14,17 @@ import { ListItem, } from "@arizeai/components"; -import { Button, Icon, Icons, Loading, View } from "@phoenix/components"; +import { + Button, + Disclosure, + DisclosureGroup, + DisclosurePanel, + DisclosureTrigger, + Icon, + Icons, + Loading, + View, +} from "@phoenix/components"; import { usePointCloudContext, useTheme } from "@phoenix/contexts"; import { ExportSelectionButtonExportsQuery } from "./__generated__/ExportSelectionButtonExportsQuery.graphql"; @@ -147,13 +155,16 @@ export function ExportSelectionButton() { - - - }> - - - - + + + Latest Exports + + }> + + + + + )} diff --git a/app/src/pages/model/ModelPage.tsx b/app/src/pages/model/ModelPage.tsx index 153f29650f..f9bee17807 100644 --- a/app/src/pages/model/ModelPage.tsx +++ b/app/src/pages/model/ModelPage.tsx @@ -3,14 +3,14 @@ import { graphql, useLazyLoadQuery } from "react-relay"; import { Outlet } from "react-router"; import { css } from "@emotion/react"; -import { - Accordion, - AccordionItem, - TabbedCard, - TabPane, - Tabs, -} from "@arizeai/components"; +import { TabbedCard, TabPane, Tabs } from "@arizeai/components"; +import { + Disclosure, + DisclosureGroup, + DisclosurePanel, + DisclosureTrigger, +} from "@phoenix/components"; import { PrimaryInferencesTimeRange, ReferenceInferencesTimeRange, @@ -69,14 +69,22 @@ export function ModelPage(_props: ModelPageProps) { > - - - - - - - - + + + Embeddings + + + + + + Dimensions + + + + + diff --git a/app/src/pages/playground/Playground.tsx b/app/src/pages/playground/Playground.tsx index 4f52ca6803..0f5c4f346c 100644 --- a/app/src/pages/playground/Playground.tsx +++ b/app/src/pages/playground/Playground.tsx @@ -10,10 +10,12 @@ import { } from "react-router-dom"; import { css } from "@emotion/react"; -import { Accordion, AccordionItem } from "@arizeai/components"; - import { Button, + Disclosure, + DisclosureGroup, + DisclosurePanel, + DisclosureTrigger, Flex, Heading, Icon, @@ -23,6 +25,7 @@ import { } from "@phoenix/components"; import { ConfirmNavigationDialog } from "@phoenix/components/ConfirmNavigation"; import { resizeHandleCSS } from "@phoenix/components/resize"; +import { StopPropagation } from "@phoenix/components/StopPropagation"; import { TemplateLanguages } from "@phoenix/components/templateEditor/constants"; import { PlaygroundProvider, @@ -254,35 +257,40 @@ function PlaygroundContent() { >
- - - - - - } - > -
- - {instances.map((instance) => ( - - + + + Prompts + + + + + + + + +
+ + {instances.map((instance) => ( + - - ))} - -
- - + minWidth={PLAYGROUND_PROMPT_PANEL_MIN_WIDTH} + > + +
+ ))} +
+
+ + +
@@ -293,29 +301,39 @@ function PlaygroundContent() { ) : (
- + {templateLanguage !== TemplateLanguages.NONE ? ( - - - - - + + + Inputs + + + + + + + ) : null} - - - - {instances.map((instance, i) => ( - - - - ))} - - - - + + + Output + + + + + {instances.map((instance, i) => ( + + + + ))} + + + + +
)} diff --git a/app/src/pages/playground/PlaygroundResponseFormat.tsx b/app/src/pages/playground/PlaygroundResponseFormat.tsx index 8eb72a2c5e..ff0ffb7cde 100644 --- a/app/src/pages/playground/PlaygroundResponseFormat.tsx +++ b/app/src/pages/playground/PlaygroundResponseFormat.tsx @@ -1,11 +1,15 @@ import React, { useCallback, useState } from "react"; import { JSONSchema7 } from "json-schema"; -import { Accordion, AccordionItem, Card } from "@arizeai/components"; +import { Card } from "@arizeai/components"; import { Button, CopyToClipboardButton, + Disclosure, + DisclosureGroup, + DisclosurePanel, + DisclosureTrigger, Flex, Icon, Icons, @@ -84,45 +88,50 @@ export function PlaygroundResponseFormat({ ); return ( - - - - - -