From ce8cd70e83f80ff5ad4200da695972b02bb0abea Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:12:37 +0200 Subject: [PATCH 01/14] feat: add agent page tabs --- src/components/forms/Autocomplete.tsx | 12 +- .../agents/create-edit/AgentPlayground.tsx | 330 ++++++++++++++++ .../agents/create-edit/AgentResources.tsx | 284 ++++++++++++++ .../create-edit/AgentsCreateEditPage.tsx | 356 +++++++++++++----- .../create-edit-agents.validator.ts | 34 ++ src/redux/otomiApi.ts | 60 ++- 6 files changed, 968 insertions(+), 108 deletions(-) create mode 100644 src/pages/agents/create-edit/AgentPlayground.tsx create mode 100644 src/pages/agents/create-edit/AgentResources.tsx diff --git a/src/components/forms/Autocomplete.tsx b/src/components/forms/Autocomplete.tsx index 4bf97f4f4..adae94560 100644 --- a/src/components/forms/Autocomplete.tsx +++ b/src/components/forms/Autocomplete.tsx @@ -37,6 +37,8 @@ export interface EnhancedAutocompleteProps< disableSelectAll?: boolean textFieldProps?: Partial width?: 'small' | 'medium' | 'large' + /** Hide placeholder and minimize input width when values are selected (for cleaner multi-select UX) */ + compactMultiSelect?: boolean } export function Autocomplete< @@ -68,11 +70,15 @@ export function Autocomplete< value, onChange, width = 'medium', + compactMultiSelect = false, ...rest } = props const [inPlaceholder, setInPlaceholder] = useState('') + // Check if there are selected values (for hiding placeholder when values exist) + const hasValues = multiple ? Array.isArray(value) && value.length > 0 : !!value + // --- select-all logic --- const isSelectAllActive = multiple && Array.isArray(value) && value.length === options.length @@ -121,7 +127,7 @@ export function Autocomplete< label={label} width={width} loading={loading} - placeholder={inPlaceholder || placeholder || 'Select an option'} + placeholder={compactMultiSelect && hasValues ? '' : inPlaceholder || placeholder || 'Select an option'} {...params} error={!!errorText} helperText={helperText} @@ -133,6 +139,10 @@ export function Autocomplete< flexWrap: 'wrap', gap: 1, paddingRight: '44px', + '& input': { + minWidth: compactMultiSelect && hasValues && multiple ? '30px !important' : undefined, + width: compactMultiSelect && hasValues && multiple ? '30px !important' : undefined, + }, }, }} InputLabelProps={{ diff --git a/src/pages/agents/create-edit/AgentPlayground.tsx b/src/pages/agents/create-edit/AgentPlayground.tsx new file mode 100644 index 000000000..1a2235cf6 --- /dev/null +++ b/src/pages/agents/create-edit/AgentPlayground.tsx @@ -0,0 +1,330 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Alert, Box, IconButton, TextField, Typography, keyframes } from '@mui/material' +import SendIcon from '@mui/icons-material/Send' +import DeleteIcon from '@mui/icons-material/Delete' +import StopIcon from '@mui/icons-material/StopCircle' +import Markdown from 'components/Markdown' +import { Paper } from 'components/Paper' +import Iconify from 'components/Iconify' + +const thinkingAnimation = keyframes` + 0%, 60%, 100% { + opacity: 0.3; + } + 30% { + opacity: 1; + } +` + +interface Message { + role: 'user' | 'assistant' + content: string + id: string +} + +interface AgentPlaygroundProps { + teamId: string + agentName: string +} + +export default function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): React.ReactElement { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const messagesEndRef = useRef(null) + const messagesContainerRef = useRef(null) + const abortControllerRef = useRef(null) + + const scrollToBottom = () => { + if (messagesContainerRef.current) messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight + } + + useEffect(() => { + scrollToBottom() + }, [messages]) + + const handleStop = () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort() + abortControllerRef.current = null + setLoading(false) + // Remove the empty assistant message if it exists + setMessages((prev) => { + const lastMessage = prev[prev.length - 1] + if (lastMessage?.role === 'assistant' && !lastMessage.content) return prev.slice(0, -1) + + return prev + }) + } + } + + const handleSend = async () => { + if (!input.trim() || loading) return + + const userMessage: Message = { role: 'user', content: input.trim(), id: `user-${Date.now()}` } + const assistantId = `assistant-${Date.now()}` + const newMessages = [...messages, userMessage] + + // Immediately add user message and empty assistant message for thinking animation + setMessages([...newMessages, { role: 'assistant', content: '', id: assistantId }]) + setInput('') + setLoading(true) + setError(null) + + // Create new abort controller for this request + abortControllerRef.current = new AbortController() + + try { + // Call agent service through nginx proxy to handle http and mixed-content issues + // In development: use /agent proxy (port-forward to localhost:9099) + // In cluster: use /teams/{teamId}/agents/{agentName} proxy (nginx routes to internal service) + const isDev = process.env.NODE_ENV === 'development' + const agentServiceUrl = isDev + ? `/agent/v1/chat/completions` + : `/teams/${teamId}/agents/${agentName}/v1/chat/completions` + + const requestBody = { + messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), + stream: true, + model: 'rag-pipeline', + } + + const response = await fetch(agentServiceUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + signal: abortControllerRef.current.signal, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + const errorMessage = errorData?.error?.message || `Error: ${response.status}` + throw new Error(typeof errorMessage === 'string' ? errorMessage : 'Unknown error') + } + + // Handle streaming response + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + if (reader) { + // Process streaming response + const processStream = async () => { + let accumulatedContent = '' + // eslint-disable-next-line no-constant-condition + while (true) { + // eslint-disable-next-line no-await-in-loop + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n') + + // eslint-disable-next-line no-loop-func + lines.forEach((line) => { + if (line.startsWith('data: ')) { + const data = line.slice(6) + if (data === '[DONE]') return + + try { + const parsed = JSON.parse(data) + const content = parsed.choices?.[0]?.delta?.content || '' + if (content) { + accumulatedContent += content + const currentContent = accumulatedContent + setMessages((prev) => { + const updated = [...prev] + const lastIndex = updated.length - 1 + if (lastIndex >= 0 && updated[lastIndex].id === assistantId) + updated[lastIndex] = { ...updated[lastIndex], content: currentContent } + return updated + }) + } + } catch { + // Ignore JSON parse errors for malformed chunks + } + } + }) + } + } + + await processStream() + } else { + // Non-streaming response + const data = await response.json() + const assistantContent = data.choices?.[0]?.message?.content || 'No response' + setMessages([...newMessages, { role: 'assistant', content: assistantContent, id: assistantId }]) + } + } catch (err) { + // Don't show error if request was aborted by user + if (err instanceof Error && err.name === 'AbortError') return + + const errorMessage = err instanceof Error ? err.message : 'Failed to send message' + setError(errorMessage) + } finally { + setLoading(false) + abortControllerRef.current = null + } + } + + const handleClear = () => { + setMessages([]) + setError(null) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + return ( + + + {/* Header */} + + Agent Playground + + + + + + {/* Messages Area */} + + {messages.length === 0 ? ( + + + + Ask your agent a question to start evaluating. + + + ) : ( + messages.map((message) => ( + + + + {message.role === 'user' ? 'You' : 'Agent'} + + {(() => { + // Show thinking animation for empty assistant message while loading + if (message.role === 'assistant' && !message.content && loading) { + return ( + + + + + + ) + } + + // Render assistant message with markdown + if (message.role === 'assistant') { + return ( + + ) + } + + return ( + + {message.content} + + ) + })()} + + + )) + )} +
+ + + {/* Error Display */} + {error && ( + setError(null)}> + {error} + + )} + + {/* Input Area */} + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder='Type your message...' + disabled={loading} + variant='outlined' + size='small' + /> + {loading ? ( + + + + ) : ( + + + + )} + + + + ) +} diff --git a/src/pages/agents/create-edit/AgentResources.tsx b/src/pages/agents/create-edit/AgentResources.tsx new file mode 100644 index 000000000..92a44c1d4 --- /dev/null +++ b/src/pages/agents/create-edit/AgentResources.tsx @@ -0,0 +1,284 @@ +import React from 'react' +import { Box, Button, IconButton } from '@mui/material' +import { TextField } from 'components/forms/TextField' +import { makeStyles } from 'tss-react/mui' +import { Theme } from '@mui/material/styles' +import font from 'theme/font' +import { Add, Clear } from '@mui/icons-material' +import { InputLabel } from 'components/InputLabel' +import { useFieldArray, useFormContext } from 'react-hook-form' +import FormRow from 'components/forms/FormRow' +import { FormHelperText } from 'components/FormHelperText' + +const useStyles = makeStyles()((theme: Theme) => ({ + container: { + padding: '16px', + backgroundColor: '#424242', + borderRadius: '8px', + }, + itemRow: { + marginBottom: '20px', + display: 'flex', + alignItems: 'center', + }, + addItemButton: { + marginLeft: '-10px', + display: 'flex', + alignItems: 'center', + textTransform: 'none', + }, + errorText: { + alignItems: 'center', + color: '#d63c42', + display: 'flex', + left: 5, + top: 42, + width: '100%', + }, + helperTextTop: { + color: theme.palette.cl.text.subTitle, + marginTop: 0, + }, + inputLabel: { + color: theme.palette.cl.text.title, + fontFamily: font.bold, + fontWeight: 700, + fontSize: '0.875rem', + lineHeight: '1.5rem', + }, + label: { + fontFamily: 'sans-serif', + }, +})) + +export interface AgentRouteItem { + agent: string + condition: string + apiUrl: string + apiKey?: string +} + +export interface AgentToolItem { + type: string + name: string + description: string + apiUrl: string + apiKey?: string +} + +type AgentResourceItem = AgentRouteItem | AgentToolItem + +interface AgentResourcesProps { + title: string + helperText?: string + helperTextPosition?: 'bottom' | 'top' + showLabel?: boolean + addLabel?: string + label?: string + error?: boolean + name: string + compressed?: boolean + disabled?: boolean + frozen?: boolean + errorText?: string + hideWhenEmpty?: boolean + filterFn?: (item: any, index: number) => boolean + mode: 'route' | 'tool' + toolType?: 'mcpServer' | 'subWorkflow' | 'function' +} + +export default function AgentResources(props: AgentResourcesProps) { + const { classes, cx } = useStyles() + const { control, register } = useFormContext() + const [focusedApiKeyIndex, setFocusedApiKeyIndex] = React.useState(null) + + const { + title, + addLabel, + compressed = false, + disabled = false, + frozen = false, + name, + label, + helperText, + helperTextPosition, + error, + errorText, + showLabel = true, + hideWhenEmpty = false, + filterFn, + mode, + toolType, + } = props + + const { fields, append, remove } = useFieldArray({ control, name }) + const typedFields = fields as Array + + const mappedFields = typedFields.map((field, index) => ({ field, index })) + const filteredFields = filterFn ? mappedFields.filter(({ field, index }) => filterFn(field, index)) : mappedFields + + if (filterFn && hideWhenEmpty && filteredFields.length === 0) return null + + const handleAddItem = () => { + if (mode === 'route') append({ agent: '', condition: '', apiUrl: '', apiKey: '' }) + else append({ type: toolType, name: '', description: '', apiUrl: '', apiKey: '' }) + } + + const getFieldLabels = () => { + if (mode === 'route') { + return { + field1: 'Agent', + field2: 'Condition', + field3: 'API URL', + field4: 'API Key (optional)', + } + } + return { + field1: 'Name', + field2: 'Description', + field3: 'API URL', + field4: 'API Key (optional)', + } + } + + const getFieldNames = () => { + if (mode === 'route') { + return { + field1: 'agent', + field2: 'condition', + field3: 'apiUrl', + field4: 'apiKey', + } + } + return { + field1: 'name', + field2: 'description', + field3: 'apiUrl', + field4: 'apiKey', + } + } + + const labels = getFieldLabels() + const fieldNames = getFieldNames() + + const errorScrollClassName = 'error-for-scroll' + return ( + + + {title} + + + {filteredFields.map(({ field, index }, localIndex) => { + const clearButtonMarginTop = () => { + // eslint-disable-next-line no-nested-ternary + if (compressed) return localIndex === 0 ? (showLabel ? '32px' : '12px') : showLabel ? '12px' : '14px' + + return localIndex === 0 ? '48px' : '28px' + } + + return ( + + + + + + setFocusedApiKeyIndex(index)} + onBlur={() => setFocusedApiKeyIndex(null)} + InputProps={{ + readOnly: frozen, + }} + /> + + {addLabel && !disabled && ( + remove(index)}> + + + )} + + ) + })} + {addLabel && !disabled && ( + + )} + {errorText && ( + + {errorText} + + )} + {helperText && (helperTextPosition === 'bottom' || !helperTextPosition) && ( + {helperText} + )} + + ) +} diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index 80116f6ba..3d166ca9e 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -1,10 +1,10 @@ -import { Grid } from '@mui/material' +import { AppBar, Box, Grid, Tab, Tabs, Typography } from '@mui/material' import { TextField } from 'components/forms/TextField' import { Autocomplete } from 'components/forms/Autocomplete' import { AutoResizableTextarea } from 'components/forms/TextArea' import { LandingHeader } from 'components/LandingHeader' import PaperLayout from 'layouts/Paper' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { FormProvider, Resolver, useForm } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { Redirect, RouteComponentProps } from 'react-router-dom' @@ -25,7 +25,9 @@ import Section from 'components/Section' import DeleteButton from 'components/DeleteButton' import { LoadingButton } from '@mui/lab' import { Divider } from 'components/Divider' -import { AgentPlayground } from 'components/AgentPlayground' +import TabPanel from 'components/TabPanel' +import AgentPlayground from './AgentPlayground' +import AgentResources from './AgentResources' import { agentSchema } from './create-edit-agents.validator' interface Params { @@ -56,6 +58,11 @@ export default function AgentsCreateEditPage({ if (!isFetching) refetch() }, [isDirty]) + const [tab, setTab] = useState(0) + const handleTabChange = (event, newTab) => { + setTab(newTab) + } + type FormType = CreateAplAgentApiArg['body'] const defaultValues: FormType = { @@ -67,6 +74,7 @@ export default function AgentsCreateEditPage({ foundationModel: '', agentInstructions: '', tools: [], + routes: [], }, } @@ -112,114 +120,260 @@ export default function AgentsCreateEditPage({ title={agentName ? `${data?.metadata.name}` : 'Create'} hideCrumbX={[0, 1]} /> + + + + + + +
-
- - { - const value = e.target.value - setValue('metadata.name', value) - }} - error={!!errors.metadata?.name} - helperText={errors.metadata?.name?.message?.toString()} - disabled={!!agentName} + +
+ + { + const value = e.target.value + setValue('metadata.name', value) + }} + error={!!errors.metadata?.name} + helperText={errors.metadata?.name?.message?.toString()} + disabled={!!agentName} + /> + + + + + + model.spec.modelType === 'foundation') + .map((model) => model.metadata.name) || [] + } + getOptionLabel={(option) => { + const model = aiModels?.find((m) => m.metadata.name === option) + return model?.spec.displayName || option + }} + value={watch('spec.foundationModel') || null} + onChange={(_, value) => { + setValue('spec.foundationModel', value || '') + }} + errorText={errors.spec?.foundationModel?.message?.toString()} + helperText={errors.spec?.foundationModel?.message?.toString()} + /> + +
+ + {agentName && ( + del({ teamId, agentName })} + resourceName={watch('metadata.name')} + resourceType='agent' + data-cy='button-delete-agent' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} /> -
- - - - - model.spec.modelType === 'foundation') - .map((model) => model.metadata.name) || [] + )} + + {agentName ? 'Save Changes' : 'Create Agent'} + + + + +
+ + { + const value = e.target.value + setValue('spec.agentInstructions', value) + }} + error={!!errors.spec?.agentInstructions} + value={watch('spec.agentInstructions') || ''} + /> + +
+ + + {agentName && ( + del({ teamId, agentName })} + resourceName={watch('metadata.name')} + resourceType='agent' + data-cy='button-delete-agent' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} + /> + )} + { - const model = aiModels?.find((m) => m.metadata.name === option) - return model?.spec.displayName || option - }} - value={watch('spec.foundationModel') || null} - onChange={(_, value) => { - setValue('spec.foundationModel', value || '') + > + {agentName ? 'Save Changes' : 'Create Agent'} + + + + {agentName ? ( + + + + ) : ( + + + Playground will be available here after creating the agent. + + + )} +
+ + +
+ + + label='Knowledge base(s)' + width='large' + placeholder='Select knowledge base(s)' + options={knowledgeBases?.map((kb) => kb.metadata.name) || []} + multiple + limitTags={-1} + compactMultiSelect + value={ + watch('spec.tools') + ?.filter((tool) => tool.type === 'knowledgeBase') + .map((tool) => tool.name) || [] + } + onChange={(_, value) => { + const currentTools = watch('spec.tools') || [] + const nonKbTools = currentTools.filter((tool) => tool.type !== 'knowledgeBase') + const kbTools = (value as string[]).map((name) => ({ type: 'knowledgeBase' as const, name })) + const updatedTools = [...nonKbTools, ...kbTools] + setValue('spec.tools', updatedTools) + }} + /> + + + + - - - - - - kb.metadata.name) || []} - value={watch('spec.tools')?.find((tool) => tool.type === 'knowledgeBase')?.name || ''} - onChange={(_, value) => { - const currentTools = watch('spec.tools') || [] - const nonKbTools = currentTools.filter((tool) => tool.type !== 'knowledgeBase') - const updatedTools = value ? [...nonKbTools, { type: 'knowledgeBase', name: value }] : nonKbTools - setValue('spec.tools', updatedTools) - }} + + tool.type === 'mcpServer'} /> - - - - - - { - const value = e.target.value - setValue('spec.agentInstructions', value) - }} - error={!!errors.spec?.agentInstructions} - value={watch('spec.agentInstructions') || ''} + + tool.type === 'subWorkflow'} + /> + + tool.type === 'function'} + /> +
+ + {agentName && ( + del({ teamId, agentName })} + resourceName={watch('metadata.name')} + resourceType='agent' + data-cy='button-delete-agent' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} /> -
-
- - {agentName && ( - del({ teamId, agentName })} - resourceName={watch('metadata.name')} - resourceType='agent' - data-cy='button-delete-agent' - sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} - loading={isLoadingDelete} - disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} - /> - )} - - {agentName ? 'Save Changes' : 'Create Agent'} - + )} + + {agentName ? 'Save Changes' : 'Create Agent'} + +
- {agentName && } ) diff --git a/src/pages/agents/create-edit/create-edit-agents.validator.ts b/src/pages/agents/create-edit/create-edit-agents.validator.ts index 44a6c15f8..88bd6bba4 100644 --- a/src/pages/agents/create-edit/create-edit-agents.validator.ts +++ b/src/pages/agents/create-edit/create-edit-agents.validator.ts @@ -17,5 +17,39 @@ export const agentSchema = yup.object({ foundationModel: yup.string().required('Please select a foundation model'), knowledgeBase: yup.string().optional(), agentInstructions: yup.string().required('Please enter agent instructions'), + routes: yup + .array() + .of( + yup.object({ + agent: yup.string().required('Agent name is required'), + condition: yup.string().required('Condition is required'), + apiUrl: yup.string().required('API URL is required').url('Must be a valid URL'), + apiKey: yup.string().optional(), + }), + ) + .optional(), + tools: yup + .array() + .of( + yup.object({ + type: yup + .string() + .oneOf(['knowledgeBase', 'mcpServer', 'subWorkflow', 'function'], 'Invalid tool type') + .required('Tool type is required'), + name: yup.string().required('Tool name is required'), + description: yup.string().when('type', { + is: (val: string) => ['mcpServer', 'subWorkflow', 'function'].includes(val), + then: (schema) => schema.required('Description is required for this tool type'), + otherwise: (schema) => schema.optional(), + }), + apiUrl: yup.string().when('type', { + is: (val: string) => ['mcpServer', 'subWorkflow', 'function'].includes(val), + then: (schema) => schema.required('API URL is required for this tool type').url('Must be a valid URL'), + otherwise: (schema) => schema.optional(), + }), + apiKey: yup.string().optional(), + }), + ) + .optional(), }), }) diff --git a/src/redux/otomiApi.ts b/src/redux/otomiApi.ts index 5c538b644..ee9d44580 100644 --- a/src/redux/otomiApi.ts +++ b/src/redux/otomiApi.ts @@ -6439,12 +6439,20 @@ export type GetAplAgentsApiResponse = /** status 200 Successfully obtained agent kind: 'AkamaiAgent' spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: { + agent: string + condition: string + apiUrl: string + apiKey?: string + }[] tools?: { type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }[] } } & { @@ -6474,12 +6482,20 @@ export type CreateAplAgentApiResponse = /** status 200 Successfully stored agent kind: 'AkamaiAgent' spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: { + agent: string + condition: string + apiUrl: string + apiKey?: string + }[] tools?: { type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }[] } } & { @@ -6509,12 +6525,20 @@ export type CreateAplAgentApiArg = { kind: 'AkamaiAgent' spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: { + agent: string + condition: string + apiUrl: string + apiKey?: string + }[] tools?: { type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }[] } } & { @@ -6527,12 +6551,20 @@ export type GetAplAgentApiResponse = /** status 200 Successfully obtained agent kind: 'AkamaiAgent' spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: { + agent: string + condition: string + apiUrl: string + apiKey?: string + }[] tools?: { type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }[] } } & { @@ -6564,12 +6596,20 @@ export type EditAplAgentApiResponse = /** status 200 Successfully edited a team kind: 'AkamaiAgent' spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: { + agent: string + condition: string + apiUrl: string + apiKey?: string + }[] tools?: { type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }[] } } & { @@ -6601,12 +6641,20 @@ export type EditAplAgentApiArg = { kind: 'AkamaiAgent' spec: { foundationModel: string + foundationModelEndpoint?: string agentInstructions: string + routes?: { + agent: string + condition: string + apiUrl: string + apiKey?: string + }[] tools?: { type: string name: string description?: string - endpoint?: string + apiUrl?: string + apiKey?: string }[] } } & { From 2d978832097aa12cee4cb514394fa735466016e9 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:30:13 +0200 Subject: [PATCH 02/14] chore: remove AgentPlayground form components folder --- src/components/AgentPlayground.tsx | 331 ----------------------------- 1 file changed, 331 deletions(-) delete mode 100644 src/components/AgentPlayground.tsx diff --git a/src/components/AgentPlayground.tsx b/src/components/AgentPlayground.tsx deleted file mode 100644 index a0895c9aa..000000000 --- a/src/components/AgentPlayground.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react' -import { Alert, Box, IconButton, TextField, Typography, keyframes } from '@mui/material' -import SendIcon from '@mui/icons-material/Send' -import DeleteIcon from '@mui/icons-material/Delete' -import StopIcon from '@mui/icons-material/StopCircle' -import Markdown from './Markdown' -import { Paper } from './Paper' -import Iconify from './Iconify' - -const thinkingAnimation = keyframes` - 0%, 60%, 100% { - opacity: 0.3; - } - 30% { - opacity: 1; - } -` - -interface Message { - role: 'user' | 'assistant' - content: string - id: string -} - -interface AgentPlaygroundProps { - teamId: string - agentName: string -} - -export function AgentPlayground({ teamId, agentName }: AgentPlaygroundProps): React.ReactElement { - const [messages, setMessages] = useState([]) - const [input, setInput] = useState('') - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const messagesEndRef = useRef(null) - const messagesContainerRef = useRef(null) - const abortControllerRef = useRef(null) - - const scrollToBottom = () => { - if (messagesContainerRef.current) messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight - } - - useEffect(() => { - scrollToBottom() - }, [messages]) - - const handleStop = () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort() - abortControllerRef.current = null - setLoading(false) - // Remove the empty assistant message if it exists - setMessages((prev) => { - const lastMessage = prev[prev.length - 1] - if (lastMessage?.role === 'assistant' && !lastMessage.content) return prev.slice(0, -1) - - return prev - }) - } - } - - const handleSend = async () => { - if (!input.trim() || loading) return - - const userMessage: Message = { role: 'user', content: input.trim(), id: `user-${Date.now()}` } - const assistantId = `assistant-${Date.now()}` - const newMessages = [...messages, userMessage] - - // Immediately add user message and empty assistant message for thinking animation - setMessages([...newMessages, { role: 'assistant', content: '', id: assistantId }]) - setInput('') - setLoading(true) - setError(null) - - // Create new abort controller for this request - abortControllerRef.current = new AbortController() - - try { - // Call agent service through nginx proxy to handle http and mixed-content issues - // In development: use /agent proxy (port-forward to localhost:9099) - // In cluster: use /teams/{teamId}/agents/{agentName} proxy (nginx routes to internal service) - const isDev = process.env.NODE_ENV === 'development' - const agentServiceUrl = isDev - ? `/agent/v1/chat/completions` - : `/teams/${teamId}/agents/${agentName}/v1/chat/completions` - - const requestBody = { - messages: newMessages.map((msg) => ({ role: msg.role, content: msg.content })), - stream: true, - model: 'rag-pipeline', - } - - const response = await fetch(agentServiceUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - signal: abortControllerRef.current.signal, - }) - - if (!response.ok) { - const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) - const errorMessage = errorData?.error?.message || `Error: ${response.status}` - throw new Error(typeof errorMessage === 'string' ? errorMessage : 'Unknown error') - } - - // Handle streaming response - const reader = response.body?.getReader() - const decoder = new TextDecoder() - - if (reader) { - // Process streaming response - const processStream = async () => { - let accumulatedContent = '' - // eslint-disable-next-line no-constant-condition - while (true) { - // eslint-disable-next-line no-await-in-loop - const { done, value } = await reader.read() - if (done) break - - const chunk = decoder.decode(value, { stream: true }) - const lines = chunk.split('\n') - - // eslint-disable-next-line no-loop-func - lines.forEach((line) => { - if (line.startsWith('data: ')) { - const data = line.slice(6) - if (data === '[DONE]') return - - try { - const parsed = JSON.parse(data) - const content = parsed.choices?.[0]?.delta?.content || '' - if (content) { - accumulatedContent += content - const currentContent = accumulatedContent - setMessages((prev) => { - const updated = [...prev] - const lastIndex = updated.length - 1 - if (lastIndex >= 0 && updated[lastIndex].id === assistantId) - updated[lastIndex] = { ...updated[lastIndex], content: currentContent } - return updated - }) - } - } catch { - // Ignore JSON parse errors for malformed chunks - } - } - }) - } - } - - await processStream() - } else { - // Non-streaming response - const data = await response.json() - const assistantContent = data.choices?.[0]?.message?.content || 'No response' - setMessages([...newMessages, { role: 'assistant', content: assistantContent, id: assistantId }]) - } - } catch (err) { - // Don't show error if request was aborted by user - if (err instanceof Error && err.name === 'AbortError') return - - const errorMessage = err instanceof Error ? err.message : 'Failed to send message' - setError(errorMessage) - } finally { - setLoading(false) - abortControllerRef.current = null - } - } - - const handleClear = () => { - setMessages([]) - setError(null) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSend() - } - } - - return ( - - - {/* Header */} - - Agent Playground - - - - - - {/* Messages Area */} - - {messages.length === 0 ? ( - - - - Ask your agent a question to start evaluating. - - - ) : ( - messages.map((message) => ( - - - - {message.role === 'user' ? 'You' : 'Agent'} - - {(() => { - // Show thinking animation for empty assistant message while loading - if (message.role === 'assistant' && !message.content && loading) { - return ( - - - - - - ) - } - - // Render assistant message with markdown - if (message.role === 'assistant') { - return ( - - ) - } - - return ( - - {message.content} - - ) - })()} - - - )) - )} -
- - - {/* Error Display */} - {error && ( - setError(null)}> - {error} - - )} - - {/* Input Area */} - - setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder='Type your message...' - disabled={loading} - variant='outlined' - size='small' - /> - {loading ? ( - - - - ) : ( - - - - )} - - - - ) -} From c793c9a1264e792324182c6263fc4cd527b758d8 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:41:41 +0200 Subject: [PATCH 03/14] feat: improve error handling --- .../agents/create-edit/AgentResources.tsx | 30 ++- .../create-edit/AgentsCreateEditPage.tsx | 205 ++++++++++-------- 2 files changed, 144 insertions(+), 91 deletions(-) diff --git a/src/pages/agents/create-edit/AgentResources.tsx b/src/pages/agents/create-edit/AgentResources.tsx index 92a44c1d4..12fa1737c 100644 --- a/src/pages/agents/create-edit/AgentResources.tsx +++ b/src/pages/agents/create-edit/AgentResources.tsx @@ -89,7 +89,11 @@ interface AgentResourcesProps { export default function AgentResources(props: AgentResourcesProps) { const { classes, cx } = useStyles() - const { control, register } = useFormContext() + const { + control, + register, + formState: { errors }, + } = useFormContext() const [focusedApiKeyIndex, setFocusedApiKeyIndex] = React.useState(null) const { @@ -181,6 +185,18 @@ export default function AgentResources(props: AgentResourcesProps) { return localIndex === 0 ? '48px' : '28px' } + // Get errors for this specific index + const getFieldError = (fieldName: string) => { + const fieldPath = name.split('.') + const errorObj = fieldPath.reduce((acc: any, path: string) => acc?.[path], errors) + return errorObj?.[index]?.[fieldName] + } + + const field1Error = getFieldError(fieldNames.field1) + const field2Error = getFieldError(fieldNames.field2) + const field3Error = getFieldError(fieldNames.field3) + const field4Error = getFieldError(fieldNames.field4) + return ( setFocusedApiKeyIndex(index)} onBlur={() => setFocusedApiKeyIndex(null)} diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index 3d166ca9e..6f3186582 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -1,4 +1,4 @@ -import { AppBar, Box, Grid, Tab, Tabs, Typography } from '@mui/material' +import { AppBar, Box, Grid, Tab, Tabs, Tooltip, Typography } from '@mui/material' import { TextField } from 'components/forms/TextField' import { Autocomplete } from 'components/forms/Autocomplete' import { AutoResizableTextarea } from 'components/forms/TextArea' @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react' import { FormProvider, Resolver, useForm } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { Redirect, RouteComponentProps } from 'react-router-dom' -import { isEqual } from 'lodash' +import { isEmpty, isEqual } from 'lodash' import { CreateAplAgentApiArg, useCreateAplAgentMutation, @@ -60,9 +60,24 @@ export default function AgentsCreateEditPage({ const [tab, setTab] = useState(0) const handleTabChange = (event, newTab) => { - setTab(newTab) + setTab(newTab as number) } + const tabHasErrors = (tabIndex: number): boolean => { + switch (tabIndex) { + case 0: // Settings tab + return !!(errors.metadata?.name || errors.spec?.foundationModel) + case 1: // Playground tab + return !!errors.spec?.agentInstructions + case 2: // Workflow resources tab + return !!(errors.spec?.tools || errors.spec?.routes) + default: + return false + } + } + + const DEFAULT_AGENT_INSTRUCTIONS = 'You are a helpful assistant. Give clear answers to the users.' + type FormType = CreateAplAgentApiArg['body'] const defaultValues: FormType = { @@ -90,6 +105,7 @@ export default function AgentsCreateEditPage({ watch, formState: { errors }, setValue, + trigger, } = methods useEffect(() => { @@ -103,6 +119,17 @@ export default function AgentsCreateEditPage({ else create({ teamId, body }) } + const handleFormSubmit = async () => { + const formData = watch() + if (isEmpty(formData.spec.agentInstructions)) { + setValue('spec.agentInstructions', DEFAULT_AGENT_INSTRUCTIONS) + await trigger('spec.agentInstructions') + } + handleSubmit(onSubmit)() + } + + const requiredFieldsFilled = !!watch('metadata.name') && !!watch('spec.foundationModel') + const mutating = isLoadingCreate || isLoadingUpdate || isLoadingDelete if (!mutating && (isSuccessCreate || isSuccessUpdate || isSuccessDelete)) return @@ -111,6 +138,48 @@ export default function AgentsCreateEditPage({ if (loading) return + const formButtons = ( + + {agentName && ( + del({ teamId, agentName })} + resourceName={watch('metadata.name')} + resourceType='agent' + data-cy='button-delete-agent' + sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} + loading={isLoadingDelete} + disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} + /> + )} + + + + {agentName ? 'Save Changes' : 'Create Agent'} + + + + + ) + return ( @@ -130,18 +199,54 @@ export default function AgentsCreateEditPage({ }, }} > - - - + + + -
+
- {agentName && ( - del({ teamId, agentName })} - resourceName={watch('metadata.name')} - resourceType='agent' - data-cy='button-delete-agent' - sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} - loading={isLoadingDelete} - disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} - /> - )} - - {agentName ? 'Save Changes' : 'Create Agent'} - + {formButtons}
- - {agentName && ( - del({ teamId, agentName })} - resourceName={watch('metadata.name')} - resourceType='agent' - data-cy='button-delete-agent' - sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} - loading={isLoadingDelete} - disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} - /> - )} - - {agentName ? 'Save Changes' : 'Create Agent'} - - + {formButtons} {agentName ? ( @@ -303,7 +359,6 @@ export default function AgentsCreateEditPage({ }} /> - - {agentName && ( - del({ teamId, agentName })} - resourceName={watch('metadata.name')} - resourceType='agent' - data-cy='button-delete-agent' - sx={{ float: 'right', textTransform: 'capitalize', ml: 2 }} - loading={isLoadingDelete} - disabled={isLoadingDelete || isLoadingCreate || isLoadingUpdate} - /> - )} - - {agentName ? 'Save Changes' : 'Create Agent'} - + {formButtons}
From a342c7b3ee0ca67a7f16e6221337d6866a4e7d90 Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:28:46 +0200 Subject: [PATCH 04/14] feat: improve knowledge base fields with description --- src/components/Section.tsx | 7 +- src/components/forms/Autocomplete.tsx | 3 + .../agents/create-edit/AgentResources.tsx | 211 ++++++++++++------ .../create-edit/AgentsCreateEditPage.tsx | 38 ++-- .../create-edit-agents.validator.ts | 4 +- 5 files changed, 169 insertions(+), 94 deletions(-) diff --git a/src/components/Section.tsx b/src/components/Section.tsx index d9af7f8cc..afe1ed985 100644 --- a/src/components/Section.tsx +++ b/src/components/Section.tsx @@ -55,10 +55,11 @@ interface Props { children?: React.ReactNode noPaddingTop?: boolean noMarginTop?: boolean + sx?: object } export default function Section(props: Props) { - const { title, description, collapsable, children, noPaddingTop, noMarginTop } = props + const { title, description, collapsable, children, noPaddingTop, noMarginTop, sx } = props const [expanded, setExpanded] = useState(true) const handleAccordionChange = () => { @@ -67,7 +68,7 @@ export default function Section(props: Props) { if (collapsable) { return ( - + @@ -89,7 +90,7 @@ export default function Section(props: Props) { } return ( - + {title && {title}} {description && {description}} {children} diff --git a/src/components/forms/Autocomplete.tsx b/src/components/forms/Autocomplete.tsx index adae94560..78cb95d94 100644 --- a/src/components/forms/Autocomplete.tsx +++ b/src/components/forms/Autocomplete.tsx @@ -60,6 +60,7 @@ export function Autocomplete< loadingText, multiple, disableSelectAll = false, + noMarginTop = false, noOptionsText, onBlur, options, @@ -127,8 +128,10 @@ export function Autocomplete< label={label} width={width} loading={loading} + noMarginTop={noMarginTop} placeholder={compactMultiSelect && hasValues ? '' : inPlaceholder || placeholder || 'Select an option'} {...params} + {...textFieldProps} error={!!errorText} helperText={helperText} InputProps={{ diff --git a/src/pages/agents/create-edit/AgentResources.tsx b/src/pages/agents/create-edit/AgentResources.tsx index 12fa1737c..3ccb607d7 100644 --- a/src/pages/agents/create-edit/AgentResources.tsx +++ b/src/pages/agents/create-edit/AgentResources.tsx @@ -1,13 +1,13 @@ import React from 'react' import { Box, Button, IconButton } from '@mui/material' import { TextField } from 'components/forms/TextField' +import { Autocomplete } from 'components/forms/Autocomplete' import { makeStyles } from 'tss-react/mui' import { Theme } from '@mui/material/styles' import font from 'theme/font' import { Add, Clear } from '@mui/icons-material' import { InputLabel } from 'components/InputLabel' import { useFieldArray, useFormContext } from 'react-hook-form' -import FormRow from 'components/forms/FormRow' import { FormHelperText } from 'components/FormHelperText' const useStyles = makeStyles()((theme: Theme) => ({ @@ -70,6 +70,7 @@ type AgentResourceItem = AgentRouteItem | AgentToolItem interface AgentResourcesProps { title: string + noMarginTop?: boolean helperText?: string helperTextPosition?: 'bottom' | 'top' showLabel?: boolean @@ -83,8 +84,10 @@ interface AgentResourcesProps { errorText?: string hideWhenEmpty?: boolean filterFn?: (item: any, index: number) => boolean - mode: 'route' | 'tool' - toolType?: 'mcpServer' | 'subWorkflow' | 'function' + mode: 'route' | 'tool' | 'knowledgeBase' + toolType?: 'mcpServer' | 'subWorkflow' | 'function' | 'knowledgeBase' + dropdownOptions?: string[] + useDropdownForFirstField?: boolean } export default function AgentResources(props: AgentResourcesProps) { @@ -93,13 +96,15 @@ export default function AgentResources(props: AgentResourcesProps) { control, register, formState: { errors }, + setValue, + watch, } = useFormContext() const [focusedApiKeyIndex, setFocusedApiKeyIndex] = React.useState(null) const { title, + noMarginTop = false, addLabel, - compressed = false, disabled = false, frozen = false, name, @@ -113,6 +118,8 @@ export default function AgentResources(props: AgentResourcesProps) { filterFn, mode, toolType, + dropdownOptions = [], + useDropdownForFirstField = false, } = props const { fields, append, remove } = useFieldArray({ control, name }) @@ -125,6 +132,7 @@ export default function AgentResources(props: AgentResourcesProps) { const handleAddItem = () => { if (mode === 'route') append({ agent: '', condition: '', apiUrl: '', apiKey: '' }) + else if (mode === 'knowledgeBase') append({ type: 'knowledgeBase', name: '', description: '' }) else append({ type: toolType, name: '', description: '', apiUrl: '', apiKey: '' }) } @@ -137,6 +145,12 @@ export default function AgentResources(props: AgentResourcesProps) { field4: 'API Key (optional)', } } + if (mode === 'knowledgeBase') { + return { + field1: 'Knowledge base', + field2: 'Description', + } + } return { field1: 'Name', field2: 'Description', @@ -154,6 +168,14 @@ export default function AgentResources(props: AgentResourcesProps) { field4: 'apiKey', } } + if (mode === 'knowledgeBase') { + return { + field1: 'name', + field2: 'description', + field3: '', + field4: '', + } + } return { field1: 'name', field2: 'description', @@ -162,13 +184,19 @@ export default function AgentResources(props: AgentResourcesProps) { } } + const getFieldCount = () => { + if (mode === 'knowledgeBase') return 2 + return 4 + } + const labels = getFieldLabels() const fieldNames = getFieldNames() + const fieldCount = getFieldCount() const errorScrollClassName = 'error-for-scroll' return ( - {filteredFields.map(({ field, index }, localIndex) => { - const clearButtonMarginTop = () => { - // eslint-disable-next-line no-nested-ternary - if (compressed) return localIndex === 0 ? (showLabel ? '32px' : '12px') : showLabel ? '12px' : '14px' - - return localIndex === 0 ? '48px' : '28px' - } + {/* Render label row for first item */} + {showLabel && filteredFields.length > 0 && ( + + + + {labels.field1} + + + {labels.field2} + + {fieldCount >= 3 && ( + + {labels.field3} + + )} + {fieldCount >= 4 && ( + + {labels.field4} + + )} + + {/* Spacer for delete button */} + + )} - // Get errors for this specific index + {filteredFields.map(({ field, index }) => { const getFieldError = (fieldName: string) => { const fieldPath = name.split('.') const errorObj = fieldPath.reduce((acc: any, path: string) => acc?.[path], errors) @@ -194,77 +246,106 @@ export default function AgentResources(props: AgentResourcesProps) { const field1Error = getFieldError(fieldNames.field1) const field2Error = getFieldError(fieldNames.field2) - const field3Error = getFieldError(fieldNames.field3) - const field4Error = getFieldError(fieldNames.field4) + const field3Error = fieldCount >= 3 ? getFieldError(fieldNames.field3) : null + const field4Error = fieldCount >= 4 ? getFieldError(fieldNames.field4) : null + + const renderFirstField = () => { + if (useDropdownForFirstField && dropdownOptions.length > 0) { + return ( + { + setValue(`${name}.${index}.${fieldNames.field1}`, value || '') + }} + disabled={disabled || frozen} + label='' + hideLabel + noMarginTop + errorText={field1Error?.message?.toString()} + helperText={field1Error?.message?.toString()} + /> + ) + } + return ( + + ) + } return ( - - + - + {renderFirstField()} - - setFocusedApiKeyIndex(index)} - onBlur={() => setFocusedApiKeyIndex(null)} - InputProps={{ - readOnly: frozen, - }} - /> - + {fieldCount >= 3 && ( + + )} + {fieldCount >= 4 && ( + setFocusedApiKeyIndex(index)} + onBlur={() => setFocusedApiKeyIndex(null)} + InputProps={{ + readOnly: frozen, + }} + /> + )} + {addLabel && !disabled && ( - remove(index)}> + remove(index)}> )} diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index 6f3186582..cab3a18bf 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -335,30 +335,20 @@ export default function AgentsCreateEditPage({ -
- - - label='Knowledge base(s)' - width='large' - placeholder='Select knowledge base(s)' - options={knowledgeBases?.map((kb) => kb.metadata.name) || []} - multiple - limitTags={-1} - compactMultiSelect - value={ - watch('spec.tools') - ?.filter((tool) => tool.type === 'knowledgeBase') - .map((tool) => tool.name) || [] - } - onChange={(_, value) => { - const currentTools = watch('spec.tools') || [] - const nonKbTools = currentTools.filter((tool) => tool.type !== 'knowledgeBase') - const kbTools = (value as string[]).map((name) => ({ type: 'knowledgeBase' as const, name })) - const updatedTools = [...nonKbTools, ...kbTools] - setValue('spec.tools', updatedTools) - }} - /> - +
+ tool.type === 'knowledgeBase'} + useDropdownForFirstField + dropdownOptions={knowledgeBases?.map((kb) => kb.metadata.name) || []} + /> ['mcpServer', 'subWorkflow', 'function'].includes(val), - then: (schema) => schema.required('Description is required for this tool type'), + is: (val: string) => ['knowledgeBase', 'mcpServer', 'subWorkflow', 'function'].includes(val), + then: (schema) => schema.required('Description is required'), otherwise: (schema) => schema.optional(), }), apiUrl: yup.string().when('type', { From d535c76f2eaa20425a3dea2e08a6aa785b44191e Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:29:10 +0200 Subject: [PATCH 05/14] feat: improve agent routes fields with dropdown --- src/pages/agents/create-edit/AgentsCreateEditPage.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx index cab3a18bf..cfd39dc3b 100644 --- a/src/pages/agents/create-edit/AgentsCreateEditPage.tsx +++ b/src/pages/agents/create-edit/AgentsCreateEditPage.tsx @@ -16,6 +16,7 @@ import { useEditAplAgentMutation, useGetAiModelsQuery, useGetAplAgentQuery, + useGetAplAgentsQuery, useGetAplKnowledgeBasesQuery, } from 'redux/otomiApi' import { useTranslation } from 'react-i18next' @@ -51,6 +52,7 @@ export default function AgentsCreateEditPage({ ) const { data: aiModels } = useGetAiModelsQuery() const { data: knowledgeBases } = useGetAplKnowledgeBasesQuery({ teamId }) + const { data: agents } = useGetAplAgentsQuery({ teamId }) const isDirty = useAppSelector(({ global: { isDirty } }) => isDirty) useEffect(() => { @@ -357,6 +359,8 @@ export default function AgentsCreateEditPage({ showLabel compressed addLabel='add agent route' + useDropdownForFirstField + dropdownOptions={agents?.map((agent) => agent.metadata.name) || []} /> Date: Thu, 23 Oct 2025 09:55:20 +0200 Subject: [PATCH 06/14] fix: agent resources alignment --- .../agents/create-edit/AgentResources.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/pages/agents/create-edit/AgentResources.tsx b/src/pages/agents/create-edit/AgentResources.tsx index 3ccb607d7..7f09f858f 100644 --- a/src/pages/agents/create-edit/AgentResources.tsx +++ b/src/pages/agents/create-edit/AgentResources.tsx @@ -252,21 +252,23 @@ export default function AgentResources(props: AgentResourcesProps) { const renderFirstField = () => { if (useDropdownForFirstField && dropdownOptions.length > 0) { return ( - { - setValue(`${name}.${index}.${fieldNames.field1}`, value || '') - }} - disabled={disabled || frozen} - label='' - hideLabel - noMarginTop - errorText={field1Error?.message?.toString()} - helperText={field1Error?.message?.toString()} - /> + + { + setValue(`${name}.${index}.${fieldNames.field1}`, value || '') + }} + disabled={disabled || frozen} + label='' + hideLabel + noMarginTop + errorText={field1Error?.message?.toString()} + helperText={field1Error?.message?.toString()} + /> + ) } return ( @@ -291,7 +293,7 @@ export default function AgentResources(props: AgentResourcesProps) { {addLabel && !disabled && ( - remove(index)}> + remove(index)}> )} From 0a6a3dab14c9399069529076059d193e0e0323ba Mon Sep 17 00:00:00 2001 From: Ferruh Cihan <63190600+ferruhcihan@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:31:05 +0200 Subject: [PATCH 07/14] feat: improve agent resources tab --- src/components/forms/Autocomplete.tsx | 2 +- src/components/forms/TextArea.tsx | 26 +-- .../agents/create-edit/AgentResources.tsx | 176 +++++++++++------- .../create-edit/AgentsCreateEditPage.tsx | 30 +-- .../create-edit-agents.validator.ts | 4 +- 5 files changed, 142 insertions(+), 96 deletions(-) diff --git a/src/components/forms/Autocomplete.tsx b/src/components/forms/Autocomplete.tsx index 78cb95d94..454e5d828 100644 --- a/src/components/forms/Autocomplete.tsx +++ b/src/components/forms/Autocomplete.tsx @@ -36,7 +36,7 @@ export interface EnhancedAutocompleteProps< /** Removes the "select all" option for multiselect */ disableSelectAll?: boolean textFieldProps?: Partial - width?: 'small' | 'medium' | 'large' + width?: 'small' | 'medium' | 'large' | 'fullwidth' /** Hide placeholder and minimize input width when values are selected (for cleaner multi-select UX) */ compactMultiSelect?: boolean } diff --git a/src/components/forms/TextArea.tsx b/src/components/forms/TextArea.tsx index 854cadbf3..3dc7073a0 100644 --- a/src/components/forms/TextArea.tsx +++ b/src/components/forms/TextArea.tsx @@ -175,23 +175,27 @@ export function AutoResizableTextarea({ return ( - - {label} - + {label && ( + + {label} + + )}