diff --git a/app/package-lock.json b/app/package-lock.json
index 2110d5b848..929e15de61 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -31,6 +31,7 @@
"react-dom": "18.2",
"react-error-boundary": "^4.0.3",
"react-hook-form": "^7.43.8",
+ "react-hotkeys-hook": "^4.4.1",
"react-relay": "^15.0.0",
"react-resizable-panels": "^0.0.54",
"react-router": "^6.7.0",
@@ -8383,6 +8384,15 @@
"react": "^16.8.0 || ^17 || ^18"
}
},
+ "node_modules/react-hotkeys-hook": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz",
+ "integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==",
+ "peerDependencies": {
+ "react": ">=16.8.1",
+ "react-dom": ">=16.8.1"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -16137,6 +16147,12 @@
"integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==",
"requires": {}
},
+ "react-hotkeys-hook": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz",
+ "integrity": "sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==",
+ "requires": {}
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/app/package.json b/app/package.json
index e985e9f5ad..1ef1bc1c77 100644
--- a/app/package.json
+++ b/app/package.json
@@ -28,6 +28,7 @@
"react-dom": "18.2",
"react-error-boundary": "^4.0.3",
"react-hook-form": "^7.43.8",
+ "react-hotkeys-hook": "^4.4.1",
"react-relay": "^15.0.0",
"react-resizable-panels": "^0.0.54",
"react-router": "^6.7.0",
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 992faaca46..0af233991e 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -4,6 +4,7 @@ import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
import { Provider, theme } from "@arizeai/components";
+import { FeatureFlagsProvider } from "./contexts/FeatureFlagsContext";
import { NotificationProvider, ThemeProvider, useTheme } from "./contexts";
import { GlobalStyles } from "./GlobalStyles";
import RelayEnvironment from "./RelayEnvironment";
@@ -26,11 +27,13 @@ export function AppContent() {
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/src/contexts/FeatureFlagsContext.tsx b/app/src/contexts/FeatureFlagsContext.tsx
new file mode 100644
index 0000000000..cb588d98a6
--- /dev/null
+++ b/app/src/contexts/FeatureFlagsContext.tsx
@@ -0,0 +1,107 @@
+import React, { createContext, PropsWithChildren, useState } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
+
+import { Dialog, DialogContainer, Switch, View } from "@arizeai/components";
+
+type FeatureFlag = "evals";
+export type FeatureFlagsContextType = {
+ featureFlags: Record;
+ setFeatureFlags: (featureFlags: Record) => void;
+};
+
+export const LOCAL_STORAGE_FEATURE_FLAGS_KEY = "arize-phoenix-feature-flags";
+
+const DEFAULT_FEATURE_FLAGS: Record = {
+ evals: false,
+};
+
+function getFeatureFlags(): Record {
+ const featureFlagsFromLocalStorage = localStorage.getItem(
+ LOCAL_STORAGE_FEATURE_FLAGS_KEY
+ );
+ if (!featureFlagsFromLocalStorage) {
+ return DEFAULT_FEATURE_FLAGS;
+ }
+
+ try {
+ const parsedFeatureFlags = JSON.parse(featureFlagsFromLocalStorage);
+ return Object.assign({}, DEFAULT_FEATURE_FLAGS, parsedFeatureFlags);
+ } catch (e) {
+ return DEFAULT_FEATURE_FLAGS;
+ }
+}
+
+export const FeatureFlagsContext =
+ createContext(null);
+
+export function useFeatureFlags() {
+ const context = React.useContext(FeatureFlagsContext);
+ if (context === null) {
+ throw new Error(
+ "useFeatureFlags must be used within a FeatureFlagsProvider"
+ );
+ }
+ return context;
+}
+
+export function useFeatureFlag(featureFlag: FeatureFlag) {
+ const { featureFlags } = useFeatureFlags();
+ return featureFlags[featureFlag];
+}
+
+export function FeatureFlagsProvider(props: React.PropsWithChildren) {
+ const [featureFlags, _setFeatureFlags] = useState<
+ Record
+ >(getFeatureFlags());
+ const setFeatureFlags = (featureFlags: Record) => {
+ localStorage.setItem(
+ LOCAL_STORAGE_FEATURE_FLAGS_KEY,
+ JSON.stringify(featureFlags)
+ );
+ _setFeatureFlags(featureFlags);
+ };
+
+ return (
+
+ {props.children}
+
+ );
+}
+
+function FeatureFlagsControls(props: PropsWithChildren) {
+ const { children } = props;
+ const { featureFlags, setFeatureFlags } = useFeatureFlags();
+ const [showControls, setShowControls] = useState(false);
+ useHotkeys("ctrl+shift+f", () => setShowControls(true));
+ return (
+ <>
+ {children}
+ setShowControls(false)}
+ >
+ {showControls && (
+
+ )}
+
+ >
+ );
+}
diff --git a/app/src/pages/trace/TracePage.tsx b/app/src/pages/trace/TracePage.tsx
index 9b44abb5bb..628d7bca37 100644
--- a/app/src/pages/trace/TracePage.tsx
+++ b/app/src/pages/trace/TracePage.tsx
@@ -41,6 +41,7 @@ import { SpanItem } from "@phoenix/components/trace/SpanItem";
import { SpanKindIcon } from "@phoenix/components/trace/SpanKindIcon";
import { TraceTree } from "@phoenix/components/trace/TraceTree";
import { useTheme } from "@phoenix/contexts";
+import { useFeatureFlag } from "@phoenix/contexts/FeatureFlagsContext";
import {
DOCUMENT_CONTENT,
DOCUMENT_ID,
@@ -267,6 +268,7 @@ function SelectedSpanDetails({ selectedSpan }: { selectedSpan: Span }) {
const hasExceptions = useMemo(() => {
return spanHasException(selectedSpan);
}, [selectedSpan]);
+ const evalsEnabled = useFeatureFlag("evals");
return (
+ {evalsEnabled ? Evals Tab : null}
);