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 (
-
-
-
-
-
- } />}
- size="S"
- onPress={() => {
- deleteInvocationParameterInput({
- instanceId: playgroundInstanceId,
- invocationParameterInputInvocationName:
- RESPONSE_FORMAT_PARAM_NAME,
- });
- }}
- />
-
- }
- >
-
+
+
+ Output Schema
+
+
+
+
+
+ } />}
+ size="S"
+ onPress={() => {
+ deleteInvocationParameterInput({
+ instanceId: playgroundInstanceId,
+ invocationParameterInputInvocationName:
+ RESPONSE_FORMAT_PARAM_NAME,
+ });
+ }}
+ />
+
}
>
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
}
diff --git a/app/src/pages/playground/PlaygroundTools.tsx b/app/src/pages/playground/PlaygroundTools.tsx
index 9e020f0aec..7097d36f5b 100644
--- a/app/src/pages/playground/PlaygroundTools.tsx
+++ b/app/src/pages/playground/PlaygroundTools.tsx
@@ -1,8 +1,15 @@
import React, { useMemo } from "react";
-import { Accordion, AccordionItem, Counter, Form } from "@arizeai/components";
+import { Counter, Form } from "@arizeai/components";
-import { Flex, View } from "@phoenix/components";
+import {
+ Disclosure,
+ DisclosureGroup,
+ DisclosurePanel,
+ DisclosureTrigger,
+ Flex,
+ View,
+} from "@phoenix/components";
import { ToolChoicePicker } from "@phoenix/components/generative";
import { usePlaygroundContext } from "@phoenix/contexts/PlaygroundContext";
@@ -37,42 +44,44 @@ export function PlaygroundTools(props: PlaygroundToolsProps) {
);
return (
-
- {tools.length}}
- >
-
-
-
-
- {tools.map((tool) => {
- return (
-
- );
- })}
+
+
+
+ Tools
+ {tools.length}
+
+
+
+
+
+
+ {tools.map((tool) => {
+ return (
+
+ );
+ })}
+
-
-
-
-
+
+
+
+
);
}
diff --git a/app/src/pages/trace/SpanFeedback.tsx b/app/src/pages/trace/SpanFeedback.tsx
index 1f632107ce..9bce740855 100644
--- a/app/src/pages/trace/SpanFeedback.tsx
+++ b/app/src/pages/trace/SpanFeedback.tsx
@@ -6,8 +6,12 @@ import {
useReactTable,
} from "@tanstack/react-table";
-import { Accordion, AccordionItem } from "@arizeai/components";
-
+import {
+ Disclosure,
+ DisclosureGroup,
+ DisclosurePanel,
+ DisclosureTrigger,
+} from "@phoenix/components";
import { PreformattedTextCell } from "@phoenix/components/table";
import { tableCSS } from "@phoenix/components/table/styles";
import { TableEmpty } from "@phoenix/components/table/TableEmpty";
@@ -128,14 +132,20 @@ export function SpanFeedback({ span }: { span: SpanFeedback_annotations$key }) {
}, [data.spanAnnotations]);
const hasAnnotations = data.spanAnnotations.length > 0;
return hasAnnotations ? (
-
-
-
-
-
-
-
-
+
+
+ Evaluations
+
+
+
+
+
+ Human Annotations
+
+
+
+
+
) : (
);
diff --git a/app/stories/Disclosure.stories.tsx b/app/stories/Disclosure.stories.tsx
new file mode 100644
index 0000000000..7ebd7765ae
--- /dev/null
+++ b/app/stories/Disclosure.stories.tsx
@@ -0,0 +1,186 @@
+import React from "react";
+import { Meta, StoryFn } from "@storybook/react";
+
+// eslint-disable-next-line deprecate/import
+import {
+ Accordion as LegacyAccordion,
+ AccordionItem as LegacyAccordionItem,
+} from "@arizeai/components";
+
+import {
+ Disclosure,
+ DisclosureGroup,
+ type DisclosureGroupProps,
+ DisclosurePanel,
+ DisclosureProps,
+ DisclosureTrigger,
+ DisclosureTriggerProps,
+ Flex,
+ Heading,
+ Text,
+ View,
+} from "@phoenix/components";
+
+import { ThemeWrapper } from "./components/ThemeWrapper";
+
+const meta: Meta = {
+ title: "Disclosure",
+};
+
+export default meta;
+
+const Template: StoryFn = (args) => (
+
+
+
+
+ First Item Title
+
+ First Item Content
+
+
+
+ Second Item Title
+
+ Second Item Content
+
+
+
+
+
+);
+
+export const Default: Meta = {
+ render: Template,
+ args: { allowsMultipleExpanded: false, isDisabled: false },
+};
+
+const SingleItemStory: StoryFn = (args) => (
+
+
+
+ Content Title
+
+ Content
+
+
+
+
+);
+
+export const SingleItem: Meta = {
+ render: SingleItemStory,
+ args: {
+ defaultExpanded: true,
+ isExpanded: undefined,
+ isDisabled: false,
+ size: "L",
+ },
+ argTypes: {
+ isExpanded: {
+ control: { type: "boolean" },
+ },
+ size: {
+ control: { type: "radio" },
+ options: ["M", "L"],
+ },
+ },
+};
+
+const ExtraTitleContentStory: StoryFn = (args) => (
+
+
+
+
+
+ Content Title
+
+ 1
+
+
+
+ Content
+
+
+
+
+
+);
+
+export const ExtraTitleContent: Meta = {
+ render: ExtraTitleContentStory,
+ args: {
+ justifyContent: "start",
+ arrowPosition: "end",
+ },
+ argTypes: {
+ arrowPosition: {
+ control: { type: "radio" },
+ options: ["start", "end"],
+ },
+ justifyContent: {
+ control: { type: "radio" },
+ options: ["space-between", "start"],
+ },
+ },
+};
+
+const MigrationStory: StoryFn = (args) => (
+
+
+
+ Disclosure
+
+
+ Content Title
+
+ Content
+
+
+
+ Content Title 2
+
+ Content 2
+
+
+
+ Legacy Accordion
+
+
+ Content Legacy
+
+
+ Content Legacy 2
+
+
+
+
+
+);
+export const Migration: Meta = {
+ render: MigrationStory,
+ args: {
+ defaultExpandedKeys: ["content", "content-2"],
+ allowsMultipleExpanded: true,
+ isDisabled: false,
+ },
+};
diff --git a/app/stories/Gallery.stories.tsx b/app/stories/Gallery.stories.tsx
index d628f47199..95827b97ad 100644
--- a/app/stories/Gallery.stories.tsx
+++ b/app/stories/Gallery.stories.tsx
@@ -5,6 +5,10 @@ import { Item, Picker } from "@arizeai/components";
import {
Button,
+ Disclosure,
+ DisclosureGroup,
+ DisclosurePanel,
+ DisclosureTrigger,
Flex,
Input,
Label,
@@ -37,6 +41,7 @@ const Template: StoryFn> = () => (
borderColor="dark"
padding="size-200"
borderRadius="medium"
+ marginTop="size-800"
>
@@ -149,6 +154,22 @@ const Template: StoryFn> = () => (
+
+
+
+ Nutrition Facts
+
+ Ice cream is sooooo good for you!
+
+
+
+
);