diff --git a/_evo-output/implementation-artifacts/EVO-995-agent-creation-ux.md b/_evo-output/implementation-artifacts/EVO-995-agent-creation-ux.md new file mode 100644 index 00000000..db01bac4 --- /dev/null +++ b/_evo-output/implementation-artifacts/EVO-995-agent-creation-ux.md @@ -0,0 +1,84 @@ +# Story EVO-995: Agent Creation UX — Role Dropdown & Onboarding Flow + +Status: in-review + +## Story + +As an end user creating an AI agent, +I want the wizard to validate required fields inline and let me create custom tools without losing my progress, +so that I can complete agent setup without hitting silent errors or navigation dead-ends. + +## Acceptance Criteria + +1. Role field is required in the LLM wizard flow; empty submission is blocked with inline error message. +2. Instructions field has an optional AI helper (Generate / Review) that drafts or improves the prompt. +3. "Add tool" dialog has an inline "Create new tool" sub-dialog that preserves wizard state. +4. Agent-type cards show clear differentiators per type across all supported locales. +5. Field-level validation is surfaced inline in all wizard steps. + +## Tasks / Subtasks + +- [x] AC #1 + #5 — Add required validation to `Step4_RoleGoal.tsx` (AC: 1, 5) + - [x] Inline error message on Continue click when role is empty + - [x] Continue button disabled while role is blank + - [x] Remove skip button that allowed empty role submission +- [x] AC #3 — Rework "Create new tool" in `CustomToolsSelectionDialog.tsx` (AC: 3) + - [x] Add `showCreateDialog` state + - [x] Open `CustomToolForm` in a sub-dialog on click + - [x] On successful creation: reload tool list, auto-select new tool + - [x] Remove all `navigate('/agents/custom-tools')` calls +- [x] AC #4 — Fix translation inconsistencies (AC: 4) + - [x] pt-BR + pt: "chata" → "conversa" in LLM card description + - [x] pt-BR, pt, es, fr, it: translate Sequential/Parallel labels +- [x] Accessibility — `Step5_Instructions.tsx` + - [x] Remove `` wrappers around disabled buttons + - [x] Add `aria-disabled` directly to Button elements + - [x] Remove redundant `onClick` guard (dead code) +- [x] Tests + - [x] `Step5_Instructions.spec.tsx` — disabled/aria-disabled state per OpenAI config + - [x] `CustomToolsSelectionDialog.spec.tsx` — header button presence + inline sub-dialog flow + +## Dev Notes + +- Role is always a free-text `` across all agent creation entry points (`Step4_RoleGoal`, `ProfileSection`, `BasicInfoForm`). No dropdown exists; the original 500 was caused by empty-string `role` reaching the backend. +- `CustomToolForm` already existed at `src/components/customTools/CustomToolForm.tsx` with `onSubmit: (data: CustomToolFormData) => void`. `CustomToolFormData extends CustomToolCreate`, so it is directly compatible with `createCustomTool()` from `customToolsService`. +- Toast messages for tool creation use the `customTools` i18n namespace (`messages.createSuccess`, `messages.createError`), loaded via a second `useLanguage('customTools')` call aliased as `tTools`. +- Loop label kept as "Loop" across all locales — consistent with existing usage throughout the codebase (e.g., "Agente Loop", "Agentes em Loop"). +- Test runner: Vitest v2.1.8 with jsdom. Radix UI Dialog renders correctly in jsdom without mocking the design system. + +### Project Structure Notes + +- Wizard steps: `src/pages/Customer/Agents/Agent/wizard/` +- Custom tools dialog: `src/components/ai_agents/Dialogs/` +- Custom tool form: `src/components/customTools/` +- i18n: `src/i18n/locales/{en,pt-BR,pt,es,fr,it}/aiAgents.json` + +### References + +- Linear issue: EVO-995 +- PR: https://github.com/evolution-foundation/evo-ai-frontend-community/pull/45 +- Review comment: daniel.paes@etus.com.br, 2026-05-11 + +## Dev Agent Record + +### Agent Model Used + +claude-sonnet-4-6 + +### Completion Notes List + +- Investigated all role field usages — confirmed no dropdown exists anywhere; text input is the canonical implementation. +- All 5 ACs covered in this delivery (previous PR #45 covered ACs 2 and 4; this delivery covers 1, 3, 5 plus all review corrections). + +### File List + +- `src/pages/Customer/Agents/Agent/wizard/Step4_RoleGoal.tsx` +- `src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.tsx` +- `src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.spec.tsx` +- `src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.tsx` +- `src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.spec.tsx` +- `src/i18n/locales/pt-BR/aiAgents.json` +- `src/i18n/locales/pt/aiAgents.json` +- `src/i18n/locales/es/aiAgents.json` +- `src/i18n/locales/fr/aiAgents.json` +- `src/i18n/locales/it/aiAgents.json` diff --git a/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.spec.tsx b/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.spec.tsx new file mode 100644 index 00000000..42d9ce17 --- /dev/null +++ b/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.spec.tsx @@ -0,0 +1,89 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent } from '@testing-library/react'; +import CustomToolsSelectionDialog from './CustomToolsSelectionDialog'; + +vi.mock('@/hooks/useLanguage', () => ({ + useLanguage: () => ({ t: (key: string) => key }), +})); + +const mockListCustomTools = vi.fn(); +vi.mock('@/services/agents/customToolsService', () => ({ + listCustomTools: (...args: unknown[]) => mockListCustomTools(...args), + createCustomTool: vi.fn(), +})); + +vi.mock('@/components/customTools/CustomToolForm', () => ({ + default: ({ onCancel }: { onCancel: () => void }) => ( +
+ +
+ ), +})); + +vi.mock('sonner', () => ({ + toast: { success: vi.fn(), error: vi.fn() }, +})); + +const makeProps = (overrides = {}) => ({ + open: true, + onOpenChange: vi.fn(), + onSave: vi.fn(), + initialSelectedTools: [], + ...overrides, +}); + +describe('CustomToolsSelectionDialog', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockListCustomTools.mockResolvedValue([]); + }); + + it('renders "Create new tool" button in the header alongside search', async () => { + render(); + + // Wait for tools to load and empty state to render + await waitFor(() => { + const buttons = screen.getAllByText('tools.customTools.create'); + // Header button is always present regardless of tool count + expect(buttons.length).toBeGreaterThanOrEqual(1); + }); + }); + + it('opens inline create sub-dialog without closing selection dialog when header button is clicked', async () => { + const onOpenChange = vi.fn(); + render(); + + await waitFor(() => { + expect(screen.getAllByText('tools.customTools.create').length).toBeGreaterThanOrEqual(1); + }); + + // Click the first occurrence (header button) + const createButtons = screen.getAllByText('tools.customTools.create'); + fireEvent.click(createButtons[0].closest('button')!); + + // Inline form should appear without closing the parent dialog + expect(screen.getByTestId('custom-tool-form')).toBeTruthy(); + expect(onOpenChange).not.toHaveBeenCalled(); + }); + + it('closes the create sub-dialog when form is cancelled, keeping selection dialog open', async () => { + render(); + + await waitFor(() => { + expect(screen.getAllByText('tools.customTools.create').length).toBeGreaterThanOrEqual(1); + }); + + const createButtons = screen.getAllByText('tools.customTools.create'); + fireEvent.click(createButtons[0].closest('button')!); + expect(screen.getByTestId('custom-tool-form')).toBeTruthy(); + + // Cancel the form + fireEvent.click(screen.getByText('cancel-form')); + + // Form should be gone + await waitFor(() => { + expect(screen.queryByTestId('custom-tool-form')).toBeNull(); + }); + }); +}); diff --git a/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.tsx b/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.tsx index c9910c0f..28059e3f 100644 --- a/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.tsx +++ b/src/components/ai_agents/Dialogs/CustomToolsSelectionDialog.tsx @@ -12,10 +12,11 @@ import { Checkbox, } from '@evoapi/design-system'; import { Search, Code, Tag, Plus, ExternalLink } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; -import { listCustomTools } from '@/services/agents/customToolsService'; -import { CustomTool } from '@/types/ai'; +import { listCustomTools, createCustomTool } from '@/services/agents/customToolsService'; +import { CustomTool, CustomToolFormData } from '@/types/ai'; import { useLanguage } from '@/hooks/useLanguage'; +import CustomToolForm from '@/components/customTools/CustomToolForm'; +import { toast } from 'sonner'; interface CustomToolsSelectionDialogProps { open: boolean; @@ -31,11 +32,13 @@ const CustomToolsSelectionDialog = ({ initialSelectedTools = [], }: CustomToolsSelectionDialogProps) => { const { t } = useLanguage('aiAgents'); - const navigate = useNavigate(); + const { t: tTools } = useLanguage('customTools'); const [customTools, setCustomTools] = useState([]); const [selectedTools, setSelectedTools] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [loading, setLoading] = useState(false); + const [showCreateDialog, setShowCreateDialog] = useState(false); + const [isCreatingTool, setIsCreatingTool] = useState(false); const hasLoadedRef = useRef(false); const initialSelectionSetRef = useRef(false); @@ -55,6 +58,7 @@ const CustomToolsSelectionDialog = ({ setSelectedTools([]); setSearchTerm(''); setLoading(false); + setShowCreateDialog(false); hasLoadedRef.current = false; initialSelectionSetRef.current = false; } @@ -62,10 +66,7 @@ const CustomToolsSelectionDialog = ({ // Load all tools when dialog opens useEffect(() => { - if ( - !open || - hasLoadedRef.current - ) { + if (!open || hasLoadedRef.current) { return; } @@ -88,6 +89,31 @@ const CustomToolsSelectionDialog = ({ loadTools(); }, [open]); + const reloadTools = async () => { + try { + const tools = await listCustomTools({ skip: 0, limit: 100 }); + setCustomTools(tools); + } catch (error) { + console.error('Error reloading custom tools:', error); + } + }; + + const handleCreateTool = async (data: CustomToolFormData) => { + setIsCreatingTool(true); + try { + const newTool = await createCustomTool(data); + await reloadTools(); + setSelectedTools(prev => [...prev, newTool]); + setShowCreateDialog(false); + toast.success(tTools('messages.createSuccess')); + } catch (error) { + console.error('Error creating custom tool:', error); + toast.error(tTools('messages.createError')); + } finally { + setIsCreatingTool(false); + } + }; + const toggleTool = (tool: CustomTool) => { const isSelected = selectedTools.some(t => t.id === tool.id); if (isSelected) { @@ -124,167 +150,182 @@ const CustomToolsSelectionDialog = ({ }); return ( - - - - - - {t('dialogs.customTools.title')} - - - {t('tools.customTools.subtitle')} - - + <> + + + + + + {t('dialogs.customTools.title')} + + + {t('tools.customTools.subtitle')} + + -
- {/* Search */} -
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> +
+ {/* Search + Create */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
-
- {/* Tools List */} -
- {loading ? ( -
-
-
- ) : filteredTools.length > 0 ? ( -
- {filteredTools.map(tool => { - const isSelected = selectedTools.some(t => t.id === tool.id); - return ( -
toggleTool(tool)} - > -
- toggleTool(tool)} - className="mt-1 data-[state=checked]:bg-purple-500 data-[state=checked]:border-purple-500" - onClick={e => e.stopPropagation()} - /> + {/* Tools List */} +
+ {loading ? ( +
+
+
+ ) : filteredTools.length > 0 ? ( +
+ {filteredTools.map(tool => { + const isSelected = selectedTools.some(t => t.id === tool.id); + return ( +
toggleTool(tool)} + > +
+ toggleTool(tool)} + className="mt-1 data-[state=checked]:bg-purple-500 data-[state=checked]:border-purple-500" + onClick={e => e.stopPropagation()} + /> -
-
- -

{tool.name}

- - {tool.method.toUpperCase()} - -
+
+
+ +

{tool.name}

+ + {tool.method.toUpperCase()} + +
- {tool.description && ( -

- {tool.description} -

- )} + {tool.description && ( +

+ {tool.description} +

+ )} -
- - - {tool.endpoint} - -
- - {Array.isArray(tool.tags) && tool.tags.length > 0 && ( -
- {tool.tags.slice(0, 3).map((tag: string) => ( - - - {tag} - - ))} - {tool.tags.length > 3 && ( - - +{tool.tags.length - 3} {t('dialogs.customTools.more')} - - )} +
+ + + {tool.endpoint} +
- )} + + {Array.isArray(tool.tags) && tool.tags.length > 0 && ( +
+ {tool.tags.slice(0, 3).map((tag: string) => ( + + + {tag} + + ))} + {tool.tags.length > 3 && ( + + +{tool.tags.length - 3} {t('dialogs.customTools.more')} + + )} +
+ )} +
-
- ); - })} -
- ) : ( -
- -

- {searchTerm - ? t('messages.noResults') - : t('tools.customTools.noTools')} -

-

- {!searchTerm && t('dialogs.customTools.createFirst')} + ); + })} +

+ ) : ( +
+ +

+ {searchTerm + ? t('messages.noResults') + : t('tools.customTools.noTools')} +

+

+ {!searchTerm && t('dialogs.customTools.createFirst')} +

+ {!searchTerm && ( + + )} +
+ )} +
+ + {/* Selected Count */} + {selectedTools.length > 0 && ( +
+

+ {t('dialogs.customTools.toolsSelected', { count: selectedTools.length })}

- {!searchTerm && ( - - )}
)}
- {/* Selected Count */} - {selectedTools.length > 0 && ( -
-

- {t('dialogs.customTools.toolsSelected', { count: selectedTools.length })} -

-
- )} -
+ + + + + +
- - - {customTools.length > 0 && ( - - )} - - -
-
+ + + setShowCreateDialog(false)} + /> + + + ); }; diff --git a/src/i18n/locales/en/aiAgents.json b/src/i18n/locales/en/aiAgents.json index 45a32904..5b05328e 100644 --- a/src/i18n/locales/en/aiAgents.json +++ b/src/i18n/locales/en/aiAgents.json @@ -1265,27 +1265,27 @@ "types": { "llm": { "label": "LLM (Language Model)", - "description": "Intelligent assistant with generative AI" + "description": "Conversational AI — reasons, answers questions and chats using a language model" }, "task": { "label": "Task", - "description": "Automation of specific tasks" + "description": "Orchestrates tasks across sub-agents, each with a defined output" }, "sequential": { "label": "Sequential", - "description": "Executes agents in sequence" + "description": "Runs sub-agents one after another, passing results down the chain" }, "parallel": { "label": "Parallel", - "description": "Executes agents in parallel" + "description": "Runs multiple sub-agents simultaneously to speed up independent work" }, "loop": { "label": "Loop", - "description": "Executes agents in loop" + "description": "Repeats sub-agents until a condition is met or iterations complete" }, "external": { "label": "External Integrations", - "description": "Connects and runs external integrations" + "description": "Delegates to Flowise, n8n, Typebot, Dify or OpenAI workflows" } }, "badges": { @@ -1361,7 +1361,8 @@ "validationError": "Briefly describe what your agent does (minimum 10 characters)", "tip": "Tip:", "tipContent": "Be clear and specific. The more details you provide, the better the agent will understand its role and respond accordingly.", - "generateWithAI": "Generate with AI" + "generateWithAI": "Generate with AI", + "aiNotConfigured": "OpenAI integration not configured. Go to Settings → Integrations to enable AI features." }, "promptGenerator": { "title": "AI Prompt Generator", diff --git a/src/i18n/locales/es/aiAgents.json b/src/i18n/locales/es/aiAgents.json index 384fdbea..035caf13 100644 --- a/src/i18n/locales/es/aiAgents.json +++ b/src/i18n/locales/es/aiAgents.json @@ -1237,27 +1237,27 @@ "types": { "llm": { "label": "LLM (Modelo de Lenguaje)", - "description": "Asistente inteligente con IA generativa" + "description": "IA conversacional — razona, responde preguntas y chatea usando un modelo de lenguaje" }, "task": { "label": "Tarea", - "description": "Automatización de tareas específicas" + "description": "Orquesta tareas entre sub-agentes, cada una con una salida definida" }, "sequential": { - "label": "Sequential", - "description": "Ejecuta agentes en secuencia" + "label": "Secuencial", + "description": "Ejecuta sub-agentes en cadena, pasando el resultado de cada etapa a la siguiente" }, "parallel": { - "label": "Parallel", - "description": "Ejecuta agentes en paralelo" + "label": "Paralelo", + "description": "Ejecuta múltiples sub-agentes al mismo tiempo para tareas independientes" }, "loop": { "label": "Loop", - "description": "Ejecuta agentes en bucle" + "description": "Repite sub-agentes hasta alcanzar una condición o número de iteraciones" }, "external": { "label": "Integraciones Externas", - "description": "Conecta y ejecuta integraciones externas" + "description": "Delega a flujos de Flowise, n8n, Typebot, Dify u OpenAI" } }, "badges": { @@ -1333,7 +1333,8 @@ "validationError": "Describa brevemente qué hace su agente (mínimo 10 caracteres)", "tip": "Consejo:", "tipContent": "Sea claro y específico. Cuantos más detalles proporcione, mejor el agente entenderá su rol y responderá en consecuencia.", - "generateWithAI": "Generar con IA" + "generateWithAI": "Generar con IA", + "aiNotConfigured": "Integración con OpenAI no configurada. Ve a Configuración → Integraciones para habilitar las funciones de IA." }, "promptGenerator": { "title": "Generador de Prompt con IA", diff --git a/src/i18n/locales/fr/aiAgents.json b/src/i18n/locales/fr/aiAgents.json index 7c0d8f62..2d162c49 100644 --- a/src/i18n/locales/fr/aiAgents.json +++ b/src/i18n/locales/fr/aiAgents.json @@ -1203,27 +1203,27 @@ "types": { "llm": { "label": "LLM (Modèle de Langage)", - "description": "Assistant intelligent avec IA générative" + "description": "IA conversationnelle — raisonne, répond aux questions et discute avec un modèle de langage" }, "task": { "label": "Tâche", - "description": "Automatisation de tâches spécifiques" + "description": "Orchestre des tâches entre sous-agents, chacune avec une sortie définie" }, "sequential": { - "label": "Sequential", - "description": "Exécute les agents en séquence" + "label": "Séquentiel", + "description": "Exécute les sous-agents en chaîne, passant le résultat de chaque étape à la suivante" }, "parallel": { - "label": "Parallel", - "description": "Exécute les agents en parallèle" + "label": "Parallèle", + "description": "Exécute plusieurs sous-agents simultanément pour des tâches indépendantes" }, "loop": { "label": "Loop", - "description": "Exécute les agents en boucle" + "description": "Répète les sous-agents jusqu'à atteindre une condition ou un nombre d'itérations" }, "external": { "label": "Intégrations Externes", - "description": "Connecte et exécute des intégrations externes" + "description": "Délègue aux flux Flowise, n8n, Typebot, Dify ou OpenAI" } }, "badges": { @@ -1299,7 +1299,8 @@ "validationError": "Décrivez brièvement ce que fait votre agent (minimum 10 caractères)", "tip": "Conseil :", "tipContent": "Soyez clair et spécifique. Plus vous fournissez de détails, mieux l'agent comprendra son rôle et répondra en conséquence.", - "generateWithAI": "Générer avec IA" + "generateWithAI": "Générer avec IA", + "aiNotConfigured": "Intégration OpenAI non configurée. Allez dans Paramètres → Intégrations pour activer les fonctionnalités IA." }, "promptGenerator": { "title": "Générateur de Prompt avec IA", diff --git a/src/i18n/locales/it/aiAgents.json b/src/i18n/locales/it/aiAgents.json index 326a1f68..9705b4cd 100644 --- a/src/i18n/locales/it/aiAgents.json +++ b/src/i18n/locales/it/aiAgents.json @@ -1201,27 +1201,27 @@ "types": { "llm": { "label": "LLM (Modello Linguistico)", - "description": "Assistente intelligente con IA generativa" + "description": "IA conversazionale — ragiona, risponde a domande e chatta usando un modello linguistico" }, "task": { "label": "Attività", - "description": "Automazione di attività specifiche" + "description": "Orchestra attività tra sotto-agenti, ognuna con un output definito" }, "sequential": { - "label": "Sequential", - "description": "Esegue agenti in sequenza" + "label": "Sequenziale", + "description": "Esegue sotto-agenti in catena, passando il risultato di ogni fase alla successiva" }, "parallel": { - "label": "Parallel", - "description": "Esegue agenti in parallelo" + "label": "Parallelo", + "description": "Esegue più sotto-agenti contemporaneamente per attività indipendenti" }, "loop": { "label": "Loop", - "description": "Esegue agenti in ciclo" + "description": "Ripete i sotto-agenti fino al raggiungimento di una condizione o numero di iterazioni" }, "external": { "label": "Integrazioni Esterne", - "description": "Connette ed esegue integrazioni esterne" + "description": "Delega ai flussi di Flowise, n8n, Typebot, Dify o OpenAI" } }, "badges": { @@ -1297,7 +1297,8 @@ "validationError": "Descrivi brevemente cosa fa il tuo agente (minimo 10 caratteri)", "tip": "Consiglio:", "tipContent": "Sii chiaro e specifico. Più dettagli fornisci, meglio l'agente comprenderà il suo ruolo e risponderà di conseguenza.", - "generateWithAI": "Genera con IA" + "generateWithAI": "Genera con IA", + "aiNotConfigured": "Integrazione OpenAI non configurata. Vai in Impostazioni → Integrazioni per abilitare le funzionalità IA." }, "promptGenerator": { "title": "Generatore di Prompt con IA", diff --git a/src/i18n/locales/pt-BR/aiAgents.json b/src/i18n/locales/pt-BR/aiAgents.json index 14171d6c..bb700771 100644 --- a/src/i18n/locales/pt-BR/aiAgents.json +++ b/src/i18n/locales/pt-BR/aiAgents.json @@ -1282,27 +1282,27 @@ "types": { "llm": { "label": "LLM (Modelo de Linguagem)", - "description": "Assistente inteligente com IA generativa" + "description": "IA conversacional — raciocina, responde perguntas e conversa usando modelo de linguagem" }, "task": { "label": "Tarefa", - "description": "Automação de tarefas específicas" + "description": "Orquestra tarefas entre sub-agentes, cada uma com uma saída definida" }, "sequential": { - "label": "Sequential", - "description": "Executa agentes em sequência" + "label": "Sequencial", + "description": "Executa sub-agentes em cadeia, passando o resultado de cada etapa para a próxima" }, "parallel": { - "label": "Parallel", - "description": "Executa agentes em paralelo" + "label": "Paralelo", + "description": "Executa múltiplos sub-agentes ao mesmo tempo para tarefas independentes" }, "loop": { "label": "Loop", - "description": "Executa agentes em loop" + "description": "Repete sub-agentes até atingir uma condição ou número de iterações" }, "external": { "label": "Integrações Externas", - "description": "Conecta e executa integrações externas" + "description": "Delega para fluxos do Flowise, n8n, Typebot, Dify ou OpenAI" } }, "badges": { @@ -1378,7 +1378,8 @@ "validationError": "Descreva brevemente o que seu agente faz (mínimo 10 caracteres)", "tip": "Dica:", "tipContent": "Seja claro e específico. Quanto mais detalhes você fornecer, melhor o agente entenderá seu papel e responderá de acordo.", - "generateWithAI": "Gerar com IA" + "generateWithAI": "Gerar com IA", + "aiNotConfigured": "Integração com OpenAI não configurada. Acesse Configurações → Integrações para habilitar os recursos de IA." }, "promptGenerator": { "title": "Gerador de Prompt com IA", diff --git a/src/i18n/locales/pt/aiAgents.json b/src/i18n/locales/pt/aiAgents.json index 59a9eac6..42229764 100644 --- a/src/i18n/locales/pt/aiAgents.json +++ b/src/i18n/locales/pt/aiAgents.json @@ -1265,27 +1265,27 @@ "types": { "llm": { "label": "LLM (Modelo de Linguagem)", - "description": "Assistente inteligente com IA generativa" + "description": "IA conversacional — raciocina, responde perguntas e conversa usando modelo de linguagem" }, "task": { "label": "Tarefa", - "description": "Automação de tarefas específicas" + "description": "Orquestra tarefas entre sub-agentes, cada uma com uma saída definida" }, "sequential": { - "label": "Sequential", - "description": "Executa agentes em sequência" + "label": "Sequencial", + "description": "Executa sub-agentes em cadeia, passando o resultado de cada etapa para a próxima" }, "parallel": { - "label": "Parallel", - "description": "Executa agentes em paralelo" + "label": "Paralelo", + "description": "Executa múltiplos sub-agentes ao mesmo tempo para tarefas independentes" }, "loop": { "label": "Loop", - "description": "Executa agentes em loop" + "description": "Repete sub-agentes até atingir uma condição ou número de iterações" }, "external": { "label": "Integrações Externas", - "description": "Conecta e executa integrações externas" + "description": "Delega para fluxos do Flowise, n8n, Typebot, Dify ou OpenAI" } }, "badges": { @@ -1361,7 +1361,8 @@ "validationError": "Descreva brevemente o que seu agente faz (mínimo 10 caracteres)", "tip": "Dica:", "tipContent": "Seja claro e específico. Quanto mais detalhes você fornecer, melhor o agente entenderá seu papel e responderá de acordo.", - "generateWithAI": "Gerar com IA" + "generateWithAI": "Gerar com IA", + "aiNotConfigured": "Integração com OpenAI não configurada. Aceda a Definições → Integrações para ativar os recursos de IA." }, "promptGenerator": { "title": "Gerador de Prompt com IA", diff --git a/src/pages/Customer/Agents/Agent/wizard/Step4_RoleGoal.tsx b/src/pages/Customer/Agents/Agent/wizard/Step4_RoleGoal.tsx index 11c75abc..c79b9250 100644 --- a/src/pages/Customer/Agents/Agent/wizard/Step4_RoleGoal.tsx +++ b/src/pages/Customer/Agents/Agent/wizard/Step4_RoleGoal.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Input, Label, Button } from '@evoapi/design-system'; import { ArrowRight, ArrowLeft, Target, User } from 'lucide-react'; import { useLanguage } from '@/hooks/useLanguage'; @@ -11,6 +12,16 @@ interface Step4Props { const Step4_RoleGoal = ({ data, onChange, onNext, onBack }: Step4Props) => { const { t } = useLanguage('aiAgents'); + const [roleError, setRoleError] = useState(''); + + const handleNext = () => { + if (!data.role || !data.role.trim()) { + setRoleError(t('validation.required')); + return; + } + setRoleError(''); + onNext(); + }; return (
@@ -20,15 +31,19 @@ const Step4_RoleGoal = ({ data, onChange, onNext, onBack }: Step4Props) => {
onChange({ ...data, role: e.target.value })} + onChange={(e) => { + onChange({ ...data, role: e.target.value }); + if (e.target.value.trim()) setRoleError(''); + }} placeholder={t('wizard.step4.rolePlaceholder')} - className="h-10" + className={`h-10 ${roleError ? 'border-red-500' : ''}`} /> + {roleError &&

{roleError}

}

{t('wizard.step4.roleHelp')}

@@ -59,15 +74,14 @@ const Step4_RoleGoal = ({ data, onChange, onNext, onBack }: Step4Props) => { {t('actions.back')} -
- - -
+
); diff --git a/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.spec.tsx b/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.spec.tsx new file mode 100644 index 00000000..2f658716 --- /dev/null +++ b/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.spec.tsx @@ -0,0 +1,71 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import Step5_Instructions from './Step5_Instructions'; + +vi.mock('@/hooks/useLanguage', () => ({ + useLanguage: () => ({ t: (key: string) => key }), +})); + +vi.mock('@/services/integrations/openaiService', () => ({ + openaiService: { processEvent: vi.fn() }, +})); + +vi.mock('@/components/agents/wizard/PromptGeneratorModal', () => ({ + default: () => null, +})); + +const mockConfig = { openaiConfigured: false }; + +vi.mock('@/contexts/GlobalConfigContext', () => ({ + useGlobalConfig: () => mockConfig, +})); + +const makeProps = (instruction = '') => ({ + data: { instruction }, + onChange: vi.fn(), + onNext: vi.fn(), + onBack: vi.fn(), +}); + +describe('Step5_Instructions', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockConfig.openaiConfigured = false; + }); + + it('renders Generate button disabled with aria-disabled when OpenAI is not configured', () => { + render(); + const btn = screen.getByText('wizard.step5.generateWithAI').closest('button'); + expect(btn).toBeTruthy(); + expect(btn?.hasAttribute('disabled')).toBe(true); + expect(btn?.getAttribute('aria-disabled')).toBe('true'); + }); + + it('renders Generate button enabled when OpenAI is configured', () => { + mockConfig.openaiConfigured = true; + render(); + const btn = screen.getByText('wizard.step5.generateWithAI').closest('button'); + expect(btn?.hasAttribute('disabled')).toBe(false); + }); + + it('does not render Review button when instruction is empty', () => { + render(); + expect(screen.queryByText('wizard.promptGenerator.buttons.review')).toBeNull(); + }); + + it('renders Review button disabled with aria-disabled when instruction has content but OpenAI is not configured', () => { + render(); + const reviewBtn = screen.getByText('wizard.promptGenerator.buttons.review').closest('button'); + expect(reviewBtn).toBeTruthy(); + expect(reviewBtn?.hasAttribute('disabled')).toBe(true); + expect(reviewBtn?.getAttribute('aria-disabled')).toBe('true'); + }); + + it('renders Review button enabled when instruction has content and OpenAI is configured', () => { + mockConfig.openaiConfigured = true; + render(); + const reviewBtn = screen.getByText('wizard.promptGenerator.buttons.review').closest('button'); + expect(reviewBtn).toBeTruthy(); + expect(reviewBtn?.hasAttribute('disabled')).toBe(false); + }); +}); diff --git a/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.tsx b/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.tsx index ad57e855..89658010 100644 --- a/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.tsx +++ b/src/pages/Customer/Agents/Agent/wizard/Step5_Instructions.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Textarea, Label, Button } from '@evoapi/design-system'; +import { Textarea, Label, Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@evoapi/design-system'; import { ArrowRight, ArrowLeft, Sparkles, Wand2, Loader2 } from 'lucide-react'; import { useLanguage } from '@/hooks/useLanguage'; import { useGlobalConfig } from '@/contexts/GlobalConfigContext'; @@ -74,42 +74,63 @@ const Step5_Instructions = ({ data, onChange, onNext, onBack }: Step5Props) => { - {showAIActions && ( +
{data.instruction && data.instruction.trim() && ( - + + {!showAIActions && ( + +

{t('wizard.step5.aiNotConfigured')}

+
)} - + )} - + + + + + {!showAIActions && ( + +

{t('wizard.step5.aiNotConfigured')}

+
+ )} +
- )} +