Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
527bea5
[OPIK-7042] [FE] feat: Optimization new-run sidebar + metric/schema c…
awkoy Jun 24, 2026
49bf5ce
feat(optimizations): new-run form as a sidebar over the runs list
awkoy Jun 24, 2026
4eb15c1
refactor(optimizations): on-demand dataset name lookup + clean schema…
awkoy Jun 24, 2026
6750d0a
refactor(optimizations): split new-run god-hook into per-section hooks
awkoy Jun 24, 2026
5190709
[OPIK-7042] [FE] feat: new-run sidebar — non-blocking dataset prep, i…
awkoy Jun 30, 2026
2865dff
[OPIK-7042] [FE] fix: address Baz review on new-run sidebar
awkoy Jun 30, 2026
fc32aad
[OPIK-7042] [FE] fix: address Baz re-review (round 2) on new-run sidebar
awkoy Jun 30, 2026
b8e85d7
[OPIK-7042] [FE] refactor: move optimization icon accents to theme to…
awkoy Jun 30, 2026
ce4a5fd
[OPIK-7042] [FE] fix: validate new-run form on submit + align referen…
awkoy Jun 30, 2026
eb84c94
[OPIK-7042] [FE] fix(new-run): drive submit + lazy validation through…
awkoy Jul 1, 2026
c6fa3cb
[OPIK-7042] [FE] fix(new-run): swallow already-toasted submit rejection
awkoy Jul 1, 2026
6cf06cf
[OPIK-7042] [FE] refactor(new-run): use idiomatic <form onSubmit> submit
awkoy Jul 1, 2026
b989c49
[OPIK-7042] [FE] chore(optimizations): drop untested custom-code metr…
awkoy Jul 1, 2026
7298d7e
[OPIK-7042] [FE] refactor(new-run): prune over-explanatory comments a…
awkoy Jul 1, 2026
e581b36
[OPIK-7042] [FE] refactor(new-run): move optimizer/metric icon maps t…
awkoy Jul 1, 2026
49d6a55
[OPIK-7042] [FE] fix(new-run): stop form buttons from submitting; met…
awkoy Jul 1, 2026
5c12cee
[OPIK-7042] [FE] fix(new-run): algorithm-model inheritance, metric de…
awkoy Jul 7, 2026
ec870fe
[OPIK-7042] [FE] revert(new-run): drop SelectItem size prop (to be do…
awkoy Jul 7, 2026
dacd0a7
Merge origin/main into awkoy/opik-7042-new-run-sidebar-local
awkoy Jul 7, 2026
da2b23c
Merge remote-tracking branch 'origin/main' into awkoy/opik-7042-new-r…
awkoy Jul 8, 2026
f062644
[OPIK-7042] [FE] fix(optimizations): per-field message errors + Algor…
awkoy Jul 8, 2026
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
44 changes: 44 additions & 0 deletions apps/opik-frontend/src/constants/optimizations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
import {
Braces,
Code,
Dna,
Equal,
GitBranch,
Network,
Sigma,
Sparkles,
SpellCheck,
type LucideIcon,
} from "lucide-react";
import {
OPTIMIZER_TYPE,
METRIC_TYPE,
Expand Down Expand Up @@ -167,6 +179,38 @@ export const OPTIMIZATION_METRIC_OPTIONS = [
},
];

export type IconConfig = { icon: LucideIcon; color: string };

export const ALGORITHM_ICON_MAP: Partial<Record<OPTIMIZER_TYPE, IconConfig>> = {
[OPTIMIZER_TYPE.GEPA]: { icon: Dna, color: "var(--optimizer-icon-gepa)" },
[OPTIMIZER_TYPE.HIERARCHICAL_REFLECTIVE]: {
icon: Network,
color: "var(--optimizer-icon-hierarchical)",
},
[OPTIMIZER_TYPE.EVOLUTIONARY]: {
icon: GitBranch,
color: "var(--optimizer-icon-evolutionary)",
},
};

export const METRIC_ICON_MAP: Record<METRIC_TYPE, IconConfig> = {
[METRIC_TYPE.EQUALS]: { icon: Equal, color: "var(--metric-icon-equals)" },
[METRIC_TYPE.JSON_SCHEMA_VALIDATOR]: {
icon: Braces,
color: "var(--metric-icon-json-schema)",
},
[METRIC_TYPE.G_EVAL]: { icon: Sparkles, color: "var(--metric-icon-g-eval)" },
[METRIC_TYPE.LEVENSHTEIN]: {
icon: SpellCheck,
color: "var(--metric-icon-levenshtein)",
},
[METRIC_TYPE.NUMERICAL_SIMILARITY]: {
icon: Sigma,
color: "var(--metric-icon-numerical)",
},
[METRIC_TYPE.CODE]: { icon: Code, color: "var(--metric-icon-code)" },
};

// Type for demo dataset items - keys should match template variables
export type DemoDatasetItem = Record<string, unknown>;

Expand Down
3 changes: 1 addition & 2 deletions apps/opik-frontend/src/lib/optimizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ export const getDefaultMetricConfig = (
switch (metricType) {
case METRIC_TYPE.EQUALS:
return {
reference_key:
DEFAULT_JSON_SCHEMA_VALIDATOR_METRIC_CONFIGS.REFERENCE_KEY,
reference_key: DEFAULT_EQUALS_METRIC_CONFIGS.REFERENCE_KEY,
case_sensitive: DEFAULT_EQUALS_METRIC_CONFIGS.CASE_SENSITIVE,
};
case METRIC_TYPE.JSON_SCHEMA_VALIDATOR:
Expand Down
10 changes: 10 additions & 0 deletions apps/opik-frontend/src/lib/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export const safelyGetPromptMustacheTags = (template: string) => {
}
};

export const safelyGetPromptVariables = (template: string): string[] => {
try {
return getPromptMustacheTags(template)
.map((name) => name.split(".")[0])
.filter((name) => name.length > 0);
Comment on lines +28 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untested prompt parsing branches

src/lib/prompt.test.ts doesn’t cover safelyGetPromptVariables() or the new missingDatasetVariables branch, so regressions in dotted tags, empty input, or malformed templates could slip through — should we add unit coverage here, as .agents/skills/opik-frontend/testing.md suggests?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-frontend/src/lib/prompt.ts around lines 28-32 (the logic that normalizes
mustache tag names via `name.split(".")[0]` before the `missingDatasetVariables` check),
add unit tests that lock in this dotted-tag behavior and edge cases. Extend
apps/opik-frontend/src/lib/prompt.test.ts with cases for: empty template/input, dotted
tags (e.g., `{{dataset.foo}}` should be treated as `dataset`), malformed templates
causing mustache.parse to throw (should return false/handled value), and a focused
assertion that the `missingDatasetVariables` branch triggers (or not) based on the
normalized variables. Follow the existing test patterns in that file and ensure both the
parsing/normalization function output and the downstream missingDatasetVariables
decision are covered.

} catch (error) {
return [];
}
};

export type OpenAIMessage = {
role: string;
content:
Expand Down
11 changes: 11 additions & 0 deletions apps/opik-frontend/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@
--chart-gray-light: #ebf2f5;
--chart-gray-dark: #45575f;

/* Optimization Studio icon accents (per-algorithm / per-metric) */
--optimizer-icon-gepa: #12a594;
--optimizer-icon-hierarchical: #6e56cf;
--optimizer-icon-evolutionary: #30a46c;
--metric-icon-equals: #30a46c;
--metric-icon-json-schema: #0b7cff;
--metric-icon-g-eval: #8e4ec6;
--metric-icon-levenshtein: #f76808;
--metric-icon-numerical: #e5484d;
--metric-icon-code: #d6409f;

/* Template icon colors */
--template-icon-metrics: #5899da;
--template-icon-performance: #ef6868;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const SliderInputControl = ({
</Label>
{tooltip && (
<TooltipWrapper content={tooltip}>
<Info className="ml-1 size-4 text-light-slate" />
<Info className="ml-1 size-3.5 text-light-slate" />
</TooltipWrapper>
)}
</div>
Expand All @@ -114,7 +114,7 @@ const SliderInputControl = ({
onChange={(event) => setLocalValue(event.target.value)}
onBlur={(event) => validateAndHandleChange(event.target.value)}
value={localValue || ""}
dimension="sm"
dimension="xs"
variant="ghost"
max={max}
/>
Expand Down
12 changes: 10 additions & 2 deletions apps/opik-frontend/src/types/optimizations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AggregatedFeedbackScore } from "@/types/shared";
import { PROVIDER_MODEL_TYPE } from "./providers";

export enum OPTIMIZATION_STATUS {
RUNNING = "running",
Expand All @@ -19,7 +18,9 @@ export interface StudioPrompt {
}

export interface StudioLlmModel {
model: PROVIDER_MODEL_TYPE;
// Model ids come from the dynamic backend registry, not the legacy
// PROVIDER_MODEL_TYPE enum (which OPIK-5022 removes), so keep this a string.
model: string;
parameters?: Record<string, unknown>;
}

Expand Down Expand Up @@ -67,6 +68,13 @@ export type MetricParameters =
| NumericalSimilarityMetricParameters
| CodeMetricParameters;

// Per-field validation errors for metric params, keyed by param name
// (e.g. reference_key, task_introduction). Rendered inline by each metric form.
export type MetricParamErrors = Record<
string,
{ message?: string } | undefined
>;

export interface StudioMetric {
type: METRIC_TYPE;
parameters?: MetricParameters;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useMemo, useState } from "react";
import { keepPreviousData } from "@tanstack/react-query";
import { Database } from "lucide-react";

import useProjectDatasetsList from "@/api/datasets/useProjectDatasetsList";
import LoadableSelectBox from "@/shared/LoadableSelectBox/LoadableSelectBox";
Expand All @@ -8,12 +9,23 @@ import { usePermissions } from "@/contexts/PermissionsContext";

const DEFAULT_LOADED_DATASET_ITEMS = 1000;

/** Renders the selected value with the gold dataset icon (matches Figma). */
const renderDatasetTitleWithIcon = (option: DropdownOption<string>) => (
<div className="flex min-w-0 items-center gap-2">
<Database className="size-4 shrink-0 text-[color:var(--chart-yellow)]" />
<span className="truncate">{option.label}</span>
</div>
);

type DatasetSelectBoxProps = {
value: string;
onValueChange: (value: string) => void;
projectId?: string | null;
placeholder?: string;
className?: string;
/** Show the gold dataset icon next to the selected value (matches Figma). */
showIcon?: boolean;
disabled?: boolean;
};

const DatasetSelectBox: React.FC<DatasetSelectBoxProps> = ({
Expand All @@ -22,6 +34,8 @@ const DatasetSelectBox: React.FC<DatasetSelectBoxProps> = ({
projectId,
placeholder = "Select a test suite",
className,
showIcon = false,
disabled,
}) => {
const {
permissions: { canViewDatasets },
Expand Down Expand Up @@ -56,13 +70,15 @@ const DatasetSelectBox: React.FC<DatasetSelectBoxProps> = ({
value={value}
placeholder={placeholder}
onChange={onValueChange}
renderTitle={showIcon ? renderDatasetTitleWithIcon : undefined}
onLoadMore={
total > DEFAULT_LOADED_DATASET_ITEMS && !isLoadedMore
? loadMoreHandler
: undefined
}
buttonClassName={className}
isLoading={isLoading}
disabled={disabled}
optionsCount={DEFAULT_LOADED_DATASET_ITEMS}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ const LLMPromptMessage = forwardRef<
setIsExpanded(true);
}}
className="absolute right-0 top-0 bg-background pl-5 text-muted-slate"
type="button"
>
Show more
</Button>
Expand All @@ -411,6 +412,7 @@ const LLMPromptMessage = forwardRef<
setIsExpanded(false);
}}
className="mt-1 p-0 text-muted-slate"
type="button"
>
Show less
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ const LLMPromptMessageActions: React.FC<LLMPromptLibraryActionsProps> = ({
resetKeyRef.current = resetKeyRef.current + 1;
setOpen("save");
}}
type="button"
>
<Save />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { DropdownOption } from "@/types/shared";
import { ImprovePromptConfig } from "@/v2/pages-shared/llm/LLMPromptMessages/LLMPromptMessageActions";
import { JsonObject } from "@/types/shared";

interface MessageValidationError {
export interface MessageValidationError {
content?: {
message: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const LoadedPromptDisplay: React.FC<LoadedPromptDisplayProps> = ({
size="icon-2xs"
className="shrink-0"
onClick={onClear}
type="button"
>
<XCircle />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,29 @@ import GepaOptimizerConfigs from "@/v2/pages-shared/optimizations/AlgorithmSetti
import EvolutionaryOptimizerConfigs from "@/v2/pages-shared/optimizations/AlgorithmSettings/algorithmConfigs/EvolutionaryOptimizerConfigs";
import HierarchicalReflectiveOptimizerConfigs from "@/v2/pages-shared/optimizations/AlgorithmSettings/algorithmConfigs/HierarchicalReflectiveOptimizerConfigs";
import ExplainerIcon from "@/shared/ExplainerIcon/ExplainerIcon";
import { EXPLAINER_ID, EXPLAINERS_MAP } from "@/v2/constants/explainers";

interface AlgorithmConfigsProps {
optimizerType: OPTIMIZER_TYPE;
configs: Partial<OptimizerParameters>;
onChange: (configs: Partial<OptimizerParameters>) => void;
size?: ButtonProps["size"];
variant?: ButtonProps["variant"];
className?: string;
disabled?: boolean;
// The prompt model, shown as the effective default when no algorithm model
// is explicitly set (the algorithm model defaults to the prompt model).
promptModel?: string;
}

const AlgorithmConfigs = ({
optimizerType,
configs,
onChange,
size = "icon-sm",
variant = "outline",
className,
disabled: disabledProp = false,
promptModel,
}: AlgorithmConfigsProps) => {
const getOptimizerForm = () => {
if (optimizerType === OPTIMIZER_TYPE.GEPA) {
Expand Down Expand Up @@ -76,56 +83,46 @@ const AlgorithmConfigs = ({
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size={size} disabled={disabled}>
<Button
variant={variant}
size={size}
className={className}
disabled={disabled}
>
<Settings2 />
</Button>
</DropdownMenuTrigger>

<DropdownMenuContent
className="max-h-[70vh] overflow-y-auto p-6"
className="max-h-[70vh] overflow-y-auto p-4"
side="bottom"
align="end"
>
<div className="mb-5 w-72">
<div className="mb-1 flex items-center gap-1">
<h3 className="comet-body-s-accented">Algorithm settings</h3>
<ExplainerIcon
{...EXPLAINERS_MAP[EXPLAINER_ID.whats_the_algorithm_settings]}
{/* Single column with one gap so every field (model, then the
optimizer's own fields) is spaced identically. */}
<div className="flex w-72 flex-col gap-3">
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1">
<Label className="text-sm">Algorithm model</Label>
<ExplainerIcon description="The model the optimizer uses for its own reasoning. Defaults to the prompt model." />
</div>
<OptimizationModelSelect
compact
value={(configs.model ?? "") as PROVIDER_MODEL_TYPE | ""}
inheritedModel={(promptModel ?? "") as PROVIDER_MODEL_TYPE | ""}
onChange={(value) => onChange({ ...configs, model: value })}
onClear={() => {
// Clear the explicit model so the optimizer inherits the prompt
// model at runtime (shown as "Same as prompt").
const next = { ...configs };
delete next.model;
delete next.model_parameters;
onChange(next);
}}
/>
</div>
<p className="comet-body-xs text-muted-slate">
Configure parameters for the selected optimization algorithm
</p>
{getOptimizerForm()}
</div>
<div className="mb-6 flex w-72 flex-col gap-2">
<div className="flex items-center justify-between">
<Label className="text-sm">Algorithm model</Label>
{configs.model && (
<Button
variant="link"
size="sm"
className="h-auto p-0"
onClick={() => {
const next = { ...configs };
delete next.model;
delete next.model_parameters;
onChange(next);
}}
>
Use prompt model
</Button>
)}
</div>
<OptimizationModelSelect
value={(configs.model ?? "") as PROVIDER_MODEL_TYPE | ""}
onChange={(value) => onChange({ ...configs, model: value })}
/>
<p className="comet-body-xs text-muted-slate">
The model the optimizer uses for its own reasoning. Defaults to the
prompt model.
</p>
</div>
{getOptimizerForm()}
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
Loading
Loading