diff --git a/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorPlaygroundHeader.tsx b/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorPlaygroundHeader.tsx index 1dd5f55414..b0b970eb4d 100644 --- a/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorPlaygroundHeader.tsx +++ b/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorPlaygroundHeader.tsx @@ -1,48 +1,22 @@ /** * EvaluatorPlaygroundHeader * - * Simplified playground header for the evaluator configuration page. - * Shows evaluator name, app workflow selector, and testset dropdown. - * Reads evaluator info from playground nodes (URL-driven, no props needed). + * Header for the evaluator configuration page: the evaluator name plus the + * shared run controls. The controls (run-on selector, app picker, testset) + * live in `EvaluatorRunControls` so the page and the creation drawer share one + * implementation. Reads evaluator info from playground nodes (URL-driven). */ -import {useCallback, useMemo} from "react" +import {useMemo} from "react" import {workflowMolecule} from "@agenta/entities/workflow" -import {EntityPicker} from "@agenta/entity-ui" -import type { - EntitySelectionAdapter, - WorkflowRevisionSelectionResult, -} from "@agenta/entity-ui/selection" import {playgroundController} from "@agenta/playground" -import {X} from "@phosphor-icons/react" -import {Button, Tooltip, Typography} from "antd" -import {useAtomValue, useSetAtom} from "jotai" -import dynamic from "next/dynamic" +import {Typography} from "antd" +import {useAtomValue} from "jotai" -import { - disconnectAppFromEvaluatorAtom, - effectiveRunOnModeAtom, - runOnModeAtom, - selectedAppLabelAtom, - type RunOnMode, -} from "./atoms" -import RunOnSelector from "./RunOnSelector" +import EvaluatorRunControls from "./EvaluatorRunControls" -const TestsetDropdown = dynamic( - () => import("@/oss/components/Playground/Components/TestsetDropdown"), - {ssr: false}, -) - -interface EvaluatorPlaygroundHeaderProps { - appWorkflowAdapter: EntitySelectionAdapter - onAppSelect: (selection: WorkflowRevisionSelectionResult) => void -} - -const EvaluatorPlaygroundHeader: React.FC = ({ - appWorkflowAdapter, - onAppSelect, -}) => { +const EvaluatorPlaygroundHeader: React.FC = () => { // Read evaluator node from playground nodes // Phase 1: evaluator is at depth 0 (primary) // Phase 2: evaluator is at depth 1 (downstream) @@ -77,44 +51,6 @@ const EvaluatorPlaygroundHeader: React.FC = ({ evaluatorData?.slug?.trim() || "Evaluator" - // Selected app label for display in the picker trigger - const selectedAppLabel = useAtomValue(selectedAppLabelAtom) - const disconnectApp = useSetAtom(disconnectAppFromEvaluatorAtom) - const handleDisconnect = useCallback(() => { - disconnectApp() - }, [disconnectApp]) - - // Run-on mode — drives which loaders are surfaced. A connected app forces - // "app" mode (see effectiveRunOnModeAtom); the stored mode only matters when - // nothing is connected. - const runOnMode = useAtomValue(effectiveRunOnModeAtom) - const setRunOnMode = useSetAtom(runOnModeAtom) - const handlePickRunOn = useCallback( - (next: RunOnMode) => { - if (next === "trace") return // disabled, not selectable - // Leaving "app" mode means dropping the connected app so the graph - // returns to standalone-evaluator shape. - if (next === "data") disconnectApp() - setRunOnMode(next) - }, - [disconnectApp, setRunOnMode], - ) - const isAppMode = runOnMode === "app" - - // Check if we have an app node (depth-0 with a different entity than evaluator) - const hasAppSelected = nodes.some((n) => n.depth === 0 && n.entityId !== evaluatorEntityId) - - // Footer inside the picker popover — only when an app is currently connected. - // Mirrors the "Disconnect all" pattern used by the evaluator picker in - // `Playground/Components/PlaygroundHeader/index.tsx`. - const popupFooter = hasAppSelected ? ( -
- -
- ) : undefined - return (
@@ -123,37 +59,7 @@ const EvaluatorPlaygroundHeader: React.FC = ({
-
- - {isAppMode && ( - - variant="popover-cascader" - adapter={appWorkflowAdapter} - onSelect={onAppSelect} - size="small" - placeholder={selectedAppLabel ?? "Select app"} - popupFooter={popupFooter} - /> - )} - {isAppMode && hasAppSelected && ( - -
+
) } diff --git a/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorRunControls.tsx b/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorRunControls.tsx new file mode 100644 index 0000000000..b52c0271ac --- /dev/null +++ b/web/oss/src/components/Evaluators/components/ConfigureEvaluator/EvaluatorRunControls.tsx @@ -0,0 +1,83 @@ +/** + * EvaluatorRunControls + * + * The run-on + app + testset control cluster, shared by the evaluator + * playground page header and the evaluator-creation drawer header so the two + * stay identical. Reads everything from `useEvaluatorRunControls` (atom-backed), + * so it takes no props — drop it next to a title and it works on either surface. + * + * - Run-on selector (test case / app output / trace). + * - App picker — only in "app" mode, with a disconnect affordance once connected. + * - Test set dropdown — always available: it's the data source in test-case + * mode and feeds the app in app mode. + */ + +import {EntityPicker} from "@agenta/entity-ui" +import type {WorkflowRevisionSelectionResult} from "@agenta/entity-ui/selection" +import {X} from "@phosphor-icons/react" +import {Button, Tooltip} from "antd" +import dynamic from "next/dynamic" + +import RunOnSelector from "./RunOnSelector" +import {useEvaluatorRunControls} from "./useEvaluatorRunControls" + +const TestsetDropdown = dynamic( + () => import("@/oss/components/Playground/Components/TestsetDropdown"), + {ssr: false}, +) + +const EvaluatorRunControls = () => { + const { + appWorkflowAdapter, + handleAppSelect, + disconnectApp, + runOnMode, + handlePickRunOn, + hasAppConnected, + selectedAppLabel, + } = useEvaluatorRunControls() + + const isAppMode = runOnMode === "app" + + // Footer inside the picker popover — only when an app is currently connected. + const popupFooter = hasAppConnected ? ( +
+ +
+ ) : undefined + + return ( +
+ + + {isAppMode && ( + + variant="popover-cascader" + adapter={appWorkflowAdapter} + onSelect={handleAppSelect} + size="small" + placeholder={selectedAppLabel ?? "Select app"} + popupFooter={popupFooter} + /> + )} + + {isAppMode && hasAppConnected && ( + +
+ ) +} + +export default EvaluatorRunControls diff --git a/web/oss/src/components/Evaluators/components/ConfigureEvaluator/index.tsx b/web/oss/src/components/Evaluators/components/ConfigureEvaluator/index.tsx index 80b2952e17..f4db283381 100644 --- a/web/oss/src/components/Evaluators/components/ConfigureEvaluator/index.tsx +++ b/web/oss/src/components/Evaluators/components/ConfigureEvaluator/index.tsx @@ -14,11 +14,6 @@ import {useCallback, useEffect, useMemo} from "react" import {loadableController} from "@agenta/entities/loadable" import {testcaseMolecule} from "@agenta/entities/testcase" -import { - createWorkflowRevisionAdapter, - type WorkflowRevisionSelectionResult, -} from "@agenta/entity-ui/selection" -import {playgroundController} from "@agenta/playground" import {type PlaygroundUIProviders} from "@agenta/playground-ui" import {preloadEditorPlugins, SyncStateTag} from "@agenta/ui" import {useAtomValue, useSetAtom} from "jotai" @@ -30,15 +25,10 @@ import {OSSPlaygroundShell} from "@/oss/components/Playground/OSSPlaygroundShell import SharedGenerationResultUtils from "@/oss/components/SharedGenerationResultUtils" import {playgroundSyncAtom} from "@/oss/state/url/playground" -import { - connectAppToEvaluatorAtom, - effectiveRunOnModeAtom, - evaluatorConfigEntityIdsAtom, - hasAppConnectedAtom, - selectedAppLabelAtom, -} from "./atoms" +import {evaluatorConfigEntityIdsAtom} from "./atoms" import EvaluatorPlaygroundHeader from "./EvaluatorPlaygroundHeader" import SelectAppEmptyState from "./SelectAppEmptyState" +import {useEvaluatorRunControls} from "./useEvaluatorRunControls" const PlaygroundMainView = dynamic( () => import("@/oss/components/Playground/Components/MainLayout"), @@ -77,63 +67,17 @@ const ConfigureEvaluatorPageInner = () => { useAtomValue(playgroundSyncAtom) const configEntityIds = useAtomValue(evaluatorConfigEntityIdsAtom) - const connectApp = useSetAtom(connectAppToEvaluatorAtom) - const selectedAppLabel = useAtomValue(selectedAppLabelAtom) - const hasAppConnected = useAtomValue(hasAppConnectedAtom) - const runOnMode = useAtomValue(effectiveRunOnModeAtom) - - // In "Run on an app" mode with no app connected yet, the run panel surfaces - // the app selector (mirrors the evaluator-creation drawer) so the default - // path — pick an app → run against it — is the obvious next step. - const runDisabled = runOnMode === "app" && !hasAppConnected - - // Read the current evaluator entity from playground nodes - // Phase 1: evaluator is at depth 0 (primary, standalone run) - // Phase 2: evaluator is at depth 1 (downstream of a connected app — chain run) - const nodes = useAtomValue(useMemo(() => playgroundController.selectors.nodes(), [])) - const evaluatorNode = useMemo(() => { - const downstream = nodes.find((n) => n.depth > 0) - if (downstream) return downstream - return nodes[0] ?? null - }, [nodes]) + + // Shared run controls (app adapter, app-select, run-on mode, run gate) — the + // same hook the header and the creation drawer use, so all surfaces agree. + const {appWorkflowAdapter, handleAppSelect, selectedAppLabel, runDisabled} = + useEvaluatorRunControls() // Preload editor plugins useEffect(() => { void preloadEditorPlugins() }, []) - // App workflow picker — opt-in for chain-mode execution. The evaluator can - // also run standalone: the user fills the testcase row's template variables - // (e.g. `{{inputs}}`, `{{outputs}}` for LLM-as-a-judge) directly. The - // header surfaces this picker; we never block the run panel on it. - const appWorkflowAdapter = useMemo( - () => - createWorkflowRevisionAdapter({ - skipVariantLevel: true, - excludeRevisionZero: true, - flags: {is_evaluator: false, is_feedback: false}, - // The picker on the evaluator playground header is picking an - // upstream *app* workflow to connect to — without this the - // search bar would say "Search evaluator…" (the adapter's - // historical default) while the user is choosing an app. - parentLabel: "Application", - }), - [], - ) - - const handleAppSelect = useCallback( - (selection: WorkflowRevisionSelectionResult) => { - if (!evaluatorNode) return - connectApp({ - appRevisionId: selection.id, - appLabel: selection.label, - evaluatorRevisionId: evaluatorNode.entityId, - evaluatorLabel: evaluatorNode.label ?? "Evaluator", - }) - }, - [connectApp, evaluatorNode], - ) - const runDisabledContent = useMemo( () => ( { * (`Playground.tsx`). With a plain `h-full` here the chain collapses * to content height and the empty state sticks to the top. */}
- + playgroundController.selectors.nodes(), [])) + const evaluatorNode = useMemo(() => { + const downstream = nodes.find((n) => n.depth > 0) + if (downstream) return downstream + return nodes[0] ?? null + }, [nodes]) + + // App picker — picks an upstream *app* workflow to attach to the evaluator. + // `parentLabel: "Application"` keeps the search bar saying "Search app…" + // rather than the adapter's historical "Search evaluator…" default. + const appWorkflowAdapter = useMemo( + () => + createWorkflowRevisionAdapter({ + skipVariantLevel: true, + excludeRevisionZero: true, + flags: {is_evaluator: false, is_feedback: false}, + parentLabel: "Application", + }), + [], + ) + + const connectApp = useSetAtom(connectAppToEvaluatorAtom) + const disconnectApp = useSetAtom(disconnectAppFromEvaluatorAtom) + + const handleAppSelect = useCallback( + (selection: WorkflowRevisionSelectionResult) => { + if (!evaluatorNode) return + connectApp({ + appRevisionId: selection.id, + appLabel: selection.label, + evaluatorRevisionId: evaluatorNode.entityId, + evaluatorLabel: evaluatorNode.label ?? "Evaluator", + }) + }, + [connectApp, evaluatorNode], + ) + + // Run-on mode. A connected app forces effective "app" mode (the node graph + // is the source of truth); the stored preference only applies when nothing + // is connected. + const runOnMode = useAtomValue(effectiveRunOnModeAtom) + const setRunOnMode = useSetAtom(runOnModeAtom) + const handlePickRunOn = useCallback( + (next: RunOnMode) => { + if (next === "trace") return // disabled, not selectable + // Leaving "app" mode drops the connected app so the graph returns to + // standalone-evaluator shape. + if (next === "data") disconnectApp() + setRunOnMode(next) + }, + [disconnectApp, setRunOnMode], + ) + + const hasAppConnected = useAtomValue(hasAppConnectedAtom) + const selectedAppLabel = useAtomValue(selectedAppLabelAtom) + + // In "app" mode with no app connected yet, the evaluator can't run — the run + // panel surfaces the app selector instead of the testcase rows. In test-case + // mode the evaluator runs standalone, so it's never blocked on an app. Only + // takes effect where the run panel renders (the page and expanded drawers). + const runDisabled = runOnMode === "app" && !hasAppConnected + + return { + appWorkflowAdapter, + handleAppSelect, + disconnectApp, + runOnMode, + handlePickRunOn, + hasAppConnected, + selectedAppLabel, + runDisabled, + } +} diff --git a/web/oss/src/components/WorkflowRevisionDrawerWrapper/index.tsx b/web/oss/src/components/WorkflowRevisionDrawerWrapper/index.tsx index 7349e251d2..2b236c2243 100644 --- a/web/oss/src/components/WorkflowRevisionDrawerWrapper/index.tsx +++ b/web/oss/src/components/WorkflowRevisionDrawerWrapper/index.tsx @@ -20,12 +20,7 @@ import { workflowMolecule, discardLocalServerDataAtom, } from "@agenta/entities/workflow" -import {EntityPicker} from "@agenta/entity-ui" import {PlaygroundConfigSection} from "@agenta/entity-ui/drill-in" -import { - createWorkflowRevisionAdapter, - type WorkflowRevisionSelectionResult, -} from "@agenta/entity-ui/selection" import {VariantDetailsWithStatus, VariantNameCell} from "@agenta/entity-ui/variant" import {playgroundController} from "@agenta/playground" import { @@ -52,7 +47,7 @@ import { } from "@agenta/playground-ui/workflow-revision-drawer" import {EnvironmentTag} from "@agenta/ui" import {Rocket} from "@phosphor-icons/react" -import {Button, Typography, message} from "antd" +import {Button, message} from "antd" import {getDefaultStore, useAtom, useAtomValue, useSetAtom} from "jotai" import dynamic from "next/dynamic" import {useRouter} from "next/router" @@ -63,9 +58,10 @@ import { connectAppToEvaluatorAtom, persistedAppSelectionAtom, persistedTestsetSelectionAtom, - selectedAppLabelAtom, } from "@/oss/components/Evaluators/components/ConfigureEvaluator/atoms" import EvaluatorPlaygroundHeader from "@/oss/components/Evaluators/components/ConfigureEvaluator/EvaluatorPlaygroundHeader" +import SelectAppEmptyState from "@/oss/components/Evaluators/components/ConfigureEvaluator/SelectAppEmptyState" +import {useEvaluatorRunControls} from "@/oss/components/Evaluators/components/ConfigureEvaluator/useEvaluatorRunControls" import {clearEvaluatorWorkflowCache} from "@/oss/components/Evaluators/store/evaluatorsPaginatedStore" import {invalidateAppManagementWorkflowQueries} from "@/oss/components/pages/app-management/store" import {invalidatePromptsWorkflowQueries} from "@/oss/components/pages/prompts/store" @@ -311,64 +307,28 @@ const DrawerEvaluatorPlayground = memo(({entityId}: {entityId: string}) => { }) }, [connectedTestset, setPersistedTestset]) - const selectedAppLabel = useAtomValue(selectedAppLabelAtom) + // Shared run controls — the same hook the full page and the creation drawer + // use, so every evaluator surface gates runs identically (run-on aware) and + // can't drift apart again. (This drawer previously hardcoded + // `runDisabled={!hasAppConnected}`, which ignored the run-on mode and forced + // an app even in test-case mode.) + const {appWorkflowAdapter, handleAppSelect, selectedAppLabel, runDisabled} = + useEvaluatorRunControls() const nodes = useAtomValue(useMemo(() => playgroundController.selectors.nodes(), [])) - const evaluatorNode = useMemo(() => { - const downstream = nodes.find((n) => n.depth > 0) - if (downstream) return downstream - return nodes[0] ?? null - }, [nodes]) - - // Derive from nodes directly (single source of truth, no atom indirection) - const hasAppConnected = useMemo(() => nodes.some((n) => n.depth > 0), [nodes]) const configEntityIds = useMemo(() => { const downstream = nodes.filter((n) => n.depth > 0) if (downstream.length > 0) return downstream.map((n) => n.entityId) return nodes.map((n) => n.entityId) }, [nodes]) - const appWorkflowAdapter = useMemo( - () => - createWorkflowRevisionAdapter({ - skipVariantLevel: true, - excludeRevisionZero: true, - flags: {is_evaluator: false, is_feedback: false}, - // Picking an *app* to connect upstream of the evaluator — the - // adapter's default "Evaluator" label would make the search - // bar say "Search evaluator…" which is wrong here. - parentLabel: "Application", - }), - [], - ) - - const handleAppSelect = useCallback( - (selection: WorkflowRevisionSelectionResult) => { - if (!evaluatorNode) return - connectApp({ - appRevisionId: selection.id, - appLabel: selection.label, - evaluatorRevisionId: evaluatorNode.entityId, - evaluatorLabel: evaluatorNode.label ?? "Evaluator", - }) - }, - [connectApp, evaluatorNode], - ) - const runDisabledContent = useMemo( () => ( - <> - - Select an app to run the evaluator chain - - - variant="popover-cascader" - adapter={appWorkflowAdapter} - onSelect={handleAppSelect} - size="middle" - placeholder={selectedAppLabel ?? "Select app"} - /> - + ), [appWorkflowAdapter, handleAppSelect, selectedAppLabel], ) @@ -386,12 +346,7 @@ const DrawerEvaluatorPlayground = memo(({entityId}: {entityId: string}) => { return (
- {isExpanded && ( - - )} + {isExpanded && } { configViewMode={configViewMode} onConfigViewModeChange={setConfigViewMode} configEntityIdsOverride={configEntityIds} - runDisabled={!hasAppConnected} + runDisabled={runDisabled} runDisabledContent={runDisabledContent} />
diff --git a/web/oss/src/components/pages/evaluations/NewEvaluation/Components/CreateEvaluatorDrawer/index.tsx b/web/oss/src/components/pages/evaluations/NewEvaluation/Components/CreateEvaluatorDrawer/index.tsx index 43d25653a9..9bb079e6f7 100644 --- a/web/oss/src/components/pages/evaluations/NewEvaluation/Components/CreateEvaluatorDrawer/index.tsx +++ b/web/oss/src/components/pages/evaluations/NewEvaluation/Components/CreateEvaluatorDrawer/index.tsx @@ -20,12 +20,6 @@ import { registerWorkflowCommitCallbacks, getWorkflowCommitCallbacks, } from "@agenta/entities/workflow" -import {EntityPicker} from "@agenta/entity-ui" -import { - createWorkflowRevisionAdapter, - type WorkflowRevisionSelectionResult, -} from "@agenta/entity-ui/selection" -import {playgroundController} from "@agenta/playground" import {type PlaygroundUIProviders} from "@agenta/playground-ui" import {ArrowsIn, ArrowsOut} from "@phosphor-icons/react" import {Button, Typography} from "antd" @@ -34,13 +28,10 @@ import dynamic from "next/dynamic" import SimpleSharedEditor from "@/oss/components/EditorViews/SimpleSharedEditor" import EnhancedDrawer from "@/oss/components/EnhancedUIs/Drawer" -import { - connectAppToEvaluatorAtom, - evaluatorConfigEntityIdsAtom, - hasAppConnectedAtom, - selectedAppLabelAtom, -} from "@/oss/components/Evaluators/components/ConfigureEvaluator/atoms" +import {evaluatorConfigEntityIdsAtom} from "@/oss/components/Evaluators/components/ConfigureEvaluator/atoms" +import EvaluatorRunControls from "@/oss/components/Evaluators/components/ConfigureEvaluator/EvaluatorRunControls" import SelectAppEmptyState from "@/oss/components/Evaluators/components/ConfigureEvaluator/SelectAppEmptyState" +import {useEvaluatorRunControls} from "@/oss/components/Evaluators/components/ConfigureEvaluator/useEvaluatorRunControls" import {clearEvaluatorWorkflowCache} from "@/oss/components/Evaluators/store/evaluatorsPaginatedStore" import PlaygroundTestcaseEditor from "@/oss/components/Playground/Components/PlaygroundTestcaseEditor" import {OSSPlaygroundShell} from "@/oss/components/Playground/OSSPlaygroundShell" @@ -53,11 +44,6 @@ const PlaygroundMainView = dynamic( {ssr: false}, ) -const TestsetDropdown = dynamic( - () => import("@/oss/components/Playground/Components/TestsetDropdown"), - {ssr: false}, -) - interface CreateEvaluatorDrawerProps { /** Callback after successful evaluator creation. Called with the new revision ID. */ onEvaluatorCreated?: (configId?: string) => void @@ -71,57 +57,11 @@ const DrawerHeader = ({entityId, onClose}: {entityId: string; onClose: () => voi ) const name = entityData?.name?.trim() || entityData?.slug?.trim() || "New Evaluator" - const hasAppConnected = useAtomValue(hasAppConnectedAtom) - const selectedAppLabel = useAtomValue(selectedAppLabelAtom) - const connectApp = useSetAtom(connectAppToEvaluatorAtom) - - // Read current evaluator node (same logic as evaluator playground page) - const nodes = useAtomValue(useMemo(() => playgroundController.selectors.nodes(), [])) - const evaluatorNode = useMemo(() => { - const downstream = nodes.find((n) => n.depth > 0) - if (downstream) return downstream - return nodes[0] ?? null - }, [nodes]) - - const appWorkflowAdapter = useMemo( - () => - createWorkflowRevisionAdapter({ - skipVariantLevel: true, - excludeRevisionZero: true, - flags: {is_evaluator: false, is_feedback: false}, - // Picking an *app* to attach to the evaluator — without this - // the search bar would say "Search evaluator…" (the adapter's - // historical default in skip-variant mode). - parentLabel: "Application", - }), - [], - ) - - const handleAppSelect = useCallback( - (selection: WorkflowRevisionSelectionResult) => { - if (!evaluatorNode) return - connectApp({ - appRevisionId: selection.id, - appLabel: selection.label, - evaluatorRevisionId: evaluatorNode.entityId, - evaluatorLabel: evaluatorNode.label ?? "Evaluator", - }) - }, - [connectApp, evaluatorNode], - ) - return (
{name}
- - variant="popover-cascader" - adapter={appWorkflowAdapter} - onSelect={handleAppSelect} - size="small" - placeholder={selectedAppLabel ?? "Select app"} - /> - {hasAppConnected && } +