Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<WorkflowRevisionSelectionResult>
onAppSelect: (selection: WorkflowRevisionSelectionResult) => void
}

const EvaluatorPlaygroundHeader: React.FC<EvaluatorPlaygroundHeaderProps> = ({
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)
Expand Down Expand Up @@ -77,44 +51,6 @@ const EvaluatorPlaygroundHeader: React.FC<EvaluatorPlaygroundHeaderProps> = ({
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 ? (
<div className="border-0 border-t border-solid border-[rgba(5,23,41,0.06)] p-2">
<Button size="small" danger className="w-full" onClick={handleDisconnect}>
Disconnect app
</Button>
</div>
) : undefined

return (
<div className="flex items-center justify-between gap-4 px-2.5 py-2 bg-[var(--ag-rgba-000-02)] border-0 border-b border-solid border-[var(--ag-rgba-051729-06)]">
<div className="flex shrink-0 items-center gap-2 pl-2">
Expand All @@ -123,37 +59,7 @@ const EvaluatorPlaygroundHeader: React.FC<EvaluatorPlaygroundHeaderProps> = ({
</Typography>
</div>

<div className="flex min-w-0 flex-1 items-center justify-end gap-1">
<RunOnSelector mode={runOnMode} onPick={handlePickRunOn} />
{isAppMode && (
<EntityPicker<WorkflowRevisionSelectionResult>
variant="popover-cascader"
adapter={appWorkflowAdapter}
onSelect={onAppSelect}
size="small"
placeholder={selectedAppLabel ?? "Select app"}
popupFooter={popupFooter}
/>
)}
{isAppMode && hasAppSelected && (
<Tooltip title="Disconnect app">
<Button
type="text"
size="small"
icon={<X size={12} />}
onClick={handleDisconnect}
aria-label="Disconnect app"
/>
</Tooltip>
)}
{/* Testset is always connectable, with or without an upstream
* app. The earlier `hasAppSelected` gate matched the
* runDisabled gate we removed in T7 — same regression, same
* fix: standalone evaluator runs need a testset just as much
* as chained ones (the evaluator's prompt template variables
* still come from testcase row fields). */}
<TestsetDropdown />
</div>
<EvaluatorRunControls />
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ? (
<div className="border-0 border-t border-solid border-[var(--ag-rgba-051729-06)] p-2">
<Button size="small" danger className="w-full" onClick={() => disconnectApp()}>
Disconnect app
</Button>
</div>
) : undefined

return (
<div className="flex min-w-0 items-center justify-end gap-1">
<RunOnSelector mode={runOnMode} onPick={handlePickRunOn} />

{isAppMode && (
<EntityPicker<WorkflowRevisionSelectionResult>
variant="popover-cascader"
adapter={appWorkflowAdapter}
onSelect={handleAppSelect}
size="small"
placeholder={selectedAppLabel ?? "Select app"}
popupFooter={popupFooter}
/>
)}

{isAppMode && hasAppConnected && (
<Tooltip title="Disconnect app">
<Button
type="text"
size="small"
icon={<X size={12} />}
onClick={() => disconnectApp()}
aria-label="Disconnect app"
/>
</Tooltip>
)}

<TestsetDropdown />
</div>
)
}

export default EvaluatorRunControls
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"),
Expand Down Expand Up @@ -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(
() => (
<SelectAppEmptyState
Expand Down Expand Up @@ -163,10 +107,7 @@ const ConfigureEvaluatorPageInner = () => {
* (`Playground.tsx`). With a plain `h-full` here the chain collapses
* to content height and the empty state sticks to the top. */}
<div className="flex flex-col w-full h-[calc(100dvh-75px)] overflow-hidden">
<EvaluatorPlaygroundHeader
appWorkflowAdapter={appWorkflowAdapter}
onAppSelect={handleAppSelect}
/>
<EvaluatorPlaygroundHeader />
<PlaygroundMainView
mode="evaluator"
configEntityIdsOverride={configEntityIds}
Expand Down
Loading
Loading