Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8f4963f
feat: responses api support epam/ai-dial-admin-evaluation-framework-b…
buhaiovos Sep 4, 2026
cbdd207
feat: responses api support epam/ai-dial-admin-evaluation-framework-b…
buhaiovos Sep 7, 2026
9c4cbb5
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 8, 2026
bba0cbf
comment pollution cleanup
buhaiovos Sep 8, 2026
a56b1e0
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 8, 2026
8bc8543
update interface management
buhaiovos Sep 9, 2026
b01c179
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 9, 2026
6bf720d
feat(TestSuites): add Anthropic Messages as a method group
vladyslavchuhai-spec Sep 9, 2026
bfb1aa9
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 10, 2026
4c8670b
update labels and methods endpoint
buhaiovos Sep 10, 2026
c57feac
archive opsx change
buhaiovos Sep 10, 2026
9ef272b
local self-review
buhaiovos Sep 10, 2026
676bcc3
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 10, 2026
3683767
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 10, 2026
fd971aa
moved reseeding logic to target, cleaned up properties
buhaiovos Sep 11, 2026
efdb187
Merge remote-tracking branch 'origin/development' into feat/eval-20-r…
buhaiovos Sep 11, 2026
db28481
Merge remote-tracking branch 'origin/development' into feat/eval-20-r…
buhaiovos Sep 11, 2026
45adb06
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 11, 2026
a3720ee
Merge branch 'development' into feat/eval-20-responses-api
buhaiovos Sep 11, 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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Dispatch, FC, SetStateAction, useCallback, useMemo, useState } from 'react';
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';

import { DialInput, DialNoDataContent } from '@epam/ai-dial-ui-kit';
import { ColDef } from 'ag-grid-community';
Expand Down Expand Up @@ -32,7 +32,6 @@ const MethodInfo: FC<Props> = ({ testSuite, onChangeTestSuite }) => {
const PARAMETERS_COLUMNS: ColDef[] = useMemo(() => PARAMETERS_SCHEMA_COLUMNS(t), [t]);

const [view, setView] = useState(ParamsView.TABLE);
const [finalPathError, setFinalPathError] = useState<string | undefined>(undefined);

const inputSchema = useMemo(() => {
return convertSchemaToTable(testSuite?.endpointRef?.requestBodySchema?.schema);
Expand All @@ -46,31 +45,31 @@ const MethodInfo: FC<Props> = ({ testSuite, onChangeTestSuite }) => {
return testSuite?.endpointRef?.parameters || [];
}, [testSuite?.endpointRef?.parameters]);

const validateFinalPath = useCallback(
(urlTemplate?: string) => {
const relativeUrlPattern = testSuite.endpointRef?.relativeUrlPattern;

if (!urlTemplate || !relativeUrlPattern) {
return undefined;
}

if (isContainRegexSymbols(relativeUrlPattern)) {
try {
const regex = new RegExp(relativeUrlPattern);

if (!regex.test(urlTemplate)) {
dispatch({ type: ValidationActionType.SetField, field: 'urlTemplate', isValid: false });
return `Not matches with ${relativeUrlPattern} regex`;
}
} catch (error) {
console.error('Invalid regex pattern:', error);
}
}
dispatch({ type: ValidationActionType.SetField, field: 'urlTemplate', isValid: true });
/**
* Derived rather than stored, so switching method re-validates the freshly seeded path instead of
* leaving the previous method's error — and its disabled Save — standing.
*/
const finalPathError = useMemo(() => {
const relativeUrlPattern = testSuite.endpointRef?.relativeUrlPattern;
const urlTemplate = testSuite.requestTemplate?.urlTemplate;

if (!urlTemplate || !relativeUrlPattern || !isContainRegexSymbols(relativeUrlPattern)) {
return undefined;
},
[dispatch, testSuite.endpointRef?.relativeUrlPattern],
);
}

try {
return new RegExp(relativeUrlPattern).test(urlTemplate)
? undefined
: `Not matches with ${relativeUrlPattern} regex`;
} catch (error) {
console.error('Invalid regex pattern:', error);
return undefined;
}
}, [testSuite.endpointRef?.relativeUrlPattern, testSuite.requestTemplate?.urlTemplate]);

useEffect(() => {
dispatch({ type: ValidationActionType.SetField, field: 'urlTemplate', isValid: !finalPathError });
}, [dispatch, finalPathError]);

const onChangeEndpointRef = useCallback(
(endpointRef: TestSuiteEndpointRef) => {
Expand All @@ -88,9 +87,8 @@ const MethodInfo: FC<Props> = ({ testSuite, onChangeTestSuite }) => {
...testSuite,
requestTemplate: { ...testSuite.requestTemplate, urlTemplate: finalPath },
});
setFinalPathError(validateFinalPath(finalPath));
},
[onChangeTestSuite, testSuite, validateFinalPath],
[onChangeTestSuite, testSuite],
);

return (testSuite?.endpointRef && !!Object.keys(testSuite?.endpointRef).length) || testSuite ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,29 @@ interface Props {
item: TestSuiteEndpointRef;
isActive: boolean;
onClick: (index: number) => void;
label?: string;
}

const MethodItem: FC<Props> = ({ index, item, isActive, onClick }) => {
const MethodItem: FC<Props> = ({ index, item, isActive, onClick, label }) => {
const onMethodClick = useCallback(() => {
onClick(index);
}, [onClick, index]);

return (
<div
<button
type="button"
aria-current={isActive}
className={classNames(
'flex flex-row gap-x-3 items-center h-8 px-3 rounded border-l-2 border-transparent hover:bg-accent-primary-alpha cursor-pointer',
'flex flex-row gap-x-3 items-center h-8 px-3 rounded border-l-2 border-transparent text-left w-full hover:bg-accent-primary-alpha focus-visible:bg-accent-primary-alpha cursor-pointer',
isActive && 'bg-accent-primary-alpha border-l-accent-primary',
)}
onClick={onMethodClick}
>
<span className="tiny bg-layer-3 rounded p-1 border border-primary whitespace-nowrap max-w-[200px] overflow-hidden">
{item.method}
</span>
<DialEllipsisTooltip text={item.relativeUrlPattern} />
</div>
</span>{' '}
<DialEllipsisTooltip text={label ?? item.relativeUrlPattern} />
</button>
);
};

Expand Down
148 changes: 90 additions & 58 deletions apps/ai-dial-admin/src/components/TestSuites/Methods/Methods.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
'use client';

import { Dispatch, FC, ReactNode, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Dispatch,
FC,
ReactNode,
SetStateAction,
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
} from 'react';

import { DialCollapsibleSidebar, DialConditionalResizableContainer, DialLoader } from '@epam/ai-dial-ui-kit';

import { getDeployment } from '@/src/app/[lang]/test-suites/actions';
import { CHAT_COMPLETION_METHOD } from '@/src/components/TestSuites/constants/chat-completion-method';
import { CHAT_COMPLETION_SUITE, DEFAULT_SUITE } from '@/src/components/TestSuites/constants/methods';
import { generateMethodPathCombinations } from '@/src/components/TestSuites/utils/method';
import { getDeploymentById } from '@/src/app/[lang]/test-suites/actions';
import { buildMethodGroups, flattenMethodGroups } from '@/src/components/TestSuites/utils/method-groups';
import { TestSuitesI18nKey } from '@/src/constants/i18n';
import { useI18n } from '@/src/locales/client';
import { Deployment } from '@/src/models/evaluation/deployment';
import { TestSuite, TestSuiteEndpointRef } from '@/src/models/evaluation/test-suite';
import { uniquifyResponseColumns } from '@/src/utils/evaluation/request-chain';
import { TestSuite } from '@/src/models/evaluation/test-suite';
import MethodInfo from './MethodInfo';
import MethodItem from './MethodItem';

Expand All @@ -27,56 +35,69 @@ interface Props {

const Methods: FC<Props> = ({ testSuite, selectedTarget, onChange, isCreate, takenColumnNames = [], children }) => {
const t = useI18n();
const groupHeadingId = useId();

const [activeMethodIndex, setActiveMethodIndex] = useState<number | null>();
const [fullApplication, setFullApplication] = useState<Deployment | null>();
const [methods, setMethods] = useState<TestSuiteEndpointRef[]>([]);
const [isLoading, setIsLoading] = useState(true);

const buildGroupsFor = useCallback(
(deployment?: Deployment | null) =>
buildMethodGroups({ deployment, endpointRef: testSuite.endpointRef, takenColumnNames }),
// `takenColumnNames` is a fresh array each render; its contents are what matter here.
// eslint-disable-next-line react-hooks/exhaustive-deps
[testSuite.endpointRef, takenColumnNames.join(',')],
);

const groups = useMemo(() => buildGroupsFor(fullApplication), [buildGroupsFor, fullApplication]);

const options = useMemo(() => flattenMethodGroups(groups), [groups]);

/** Each group's first index into `options`, so items stay addressable by a single flat index. */
const groupOffsets = useMemo(
() =>
groups.reduce<number[]>((offsets, group, groupIndex) => {
offsets.push(groupIndex === 0 ? 0 : offsets[groupIndex - 1] + groups[groupIndex - 1].options.length);
return offsets;
}, []),
[groups],
);

const methodInfo = useMemo(() => {
if (activeMethodIndex == null) return {};
if (activeMethodIndex === 0) return CHAT_COMPLETION_METHOD;
return methods[activeMethodIndex - 1] ?? {};
}, [activeMethodIndex, methods]);
return options[activeMethodIndex]?.ref ?? {};
}, [activeMethodIndex, options]);

const onMethodClick = useCallback(
(index: number) => {
if (index === activeMethodIndex) {
return;
}

const option = options[index];
if (!option) return;

setActiveMethodIndex(index);
if (index === 0) {
onChange((prev: TestSuite) => ({
...prev,
...CHAT_COMPLETION_SUITE,
responseColumns: uniquifyResponseColumns(CHAT_COMPLETION_SUITE.responseColumns, takenColumnNames),
}));
} else {
const route = methods[index - 1];
if (!route) return;
onChange((prev: TestSuite) => ({
...prev,
...DEFAULT_SUITE(route),
}));
}
onChange((prev: TestSuite) => ({
...prev,
...option.seed,
}));
},
[activeMethodIndex, methods, onChange, takenColumnNames],
[activeMethodIndex, options, onChange],
);

useEffect(() => {
if (!fullApplication && selectedTarget) {
const { deploymentId, $type } = selectedTarget;
getDeployment(deploymentId, $type)
const { deploymentId } = selectedTarget;
getDeploymentById(deploymentId)
.then((data) => {
setFullApplication(data);
const loadedMethods = generateMethodPathCombinations(data?.routes);
setMethods(loadedMethods);

const selectedIndex = [CHAT_COMPLETION_METHOD, ...loadedMethods].findIndex(
(method) =>
method.method === testSuite.endpointRef?.method &&
method.relativeUrlPattern === testSuite.endpointRef?.relativeUrlPattern,
const loadedOptions = flattenMethodGroups(buildGroupsFor(data));
const selectedIndex = loadedOptions.findIndex(
({ ref }) =>
ref.method === testSuite.endpointRef?.method &&
ref.relativeUrlPattern === testSuite.endpointRef?.relativeUrlPattern,
);
if (selectedIndex !== -1) {
setActiveMethodIndex(selectedIndex);
Expand All @@ -102,6 +123,7 @@ const Methods: FC<Props> = ({ testSuite, selectedTarget, onChange, isCreate, tak
});
}
};

return isLoading ? (
<DialLoader size={40} />
) : (
Expand All @@ -125,30 +147,40 @@ const Methods: FC<Props> = ({ testSuite, selectedTarget, onChange, isCreate, tak
onToggle={setIsSidebarOpened}
>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1">
<span className="dial-tiny text-secondary block">{t(TestSuitesI18nKey.ChatInterface)}</span>
<MethodItem
key="chat-completion"
item={CHAT_COMPLETION_METHOD}
index={0}
isActive={activeMethodIndex === 0}
onClick={onMethodClick}
/>
</div>
<div className="flex flex-col gap-y-1">
{!!methods.length && (
<span className="dial-tiny text-secondary block">{t(TestSuitesI18nKey.Other)}</span>
)}
{methods.map((method, routeIndex) => (
<MethodItem
key={(method?.relativeUrlPattern || '') + method.method}
item={method}
index={routeIndex + 1}
isActive={activeMethodIndex === routeIndex + 1}
onClick={onMethodClick}
/>
))}
</div>
{groups.map((group, groupIndex) => {
if (!group.options.length) {
return null;
}

const headingId = `${groupHeadingId}-${group.titleKey}`;

return (
<div
key={group.titleKey}
className="flex flex-col gap-y-1"
role="group"
aria-labelledby={headingId}
>
<span id={headingId} className="dial-tiny text-secondary block">
{t(group.titleKey)}
</span>
{group.options.map((option, optionIndex) => {
const index = groupOffsets[groupIndex] + optionIndex;

return (
<MethodItem
key={`${option.ref.method}-${option.displayUrl}`}
item={option.ref}
label={option.displayUrl}
index={index}
isActive={activeMethodIndex === index}
onClick={onMethodClick}
/>
);
})}
</div>
);
})}
</div>
</DialCollapsibleSidebar>
</DialConditionalResizableContainer>
Expand Down
Loading
Loading