diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..84d27d8 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_ESTABELECIMENTO_ID=27 +VITE_USER_EMAIL=your-email@example.com +VITE_API_BASE_URL=https://prod-hgtx-intelligence-n8n.hgtx.com.br diff --git a/.gitignore b/.gitignore index a547bf3..d82c70b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ dist dist-ssr *.local +# Environment variables +.env +.env.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/package-lock.json b/package-lock.json index 2c21943..e6188e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -3324,6 +3325,12 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -3362,6 +3369,17 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3436,6 +3454,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3583,6 +3614,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3792,6 +3835,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -3820,6 +3872,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3867,6 +3933,51 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -4253,6 +4364,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4269,6 +4400,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -4306,6 +4453,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -4315,6 +4486,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4384,6 +4568,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4401,6 +4597,33 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5174,6 +5397,15 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5196,6 +5428,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5631,6 +5884,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index af4d93f..8d78e9b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/src/components/agents/CreateAgentDialog.tsx b/src/components/agents/CreateAgentDialog.tsx index c2f06e3..2d74988 100644 --- a/src/components/agents/CreateAgentDialog.tsx +++ b/src/components/agents/CreateAgentDialog.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Dialog, DialogContent, @@ -18,6 +19,8 @@ import { SelectValue, } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; +import { createAgent } from "@/lib/api/agents"; +import { AI_MODELS, getModelIdByValue } from "@/lib/constants/aiModels"; interface CreateAgentDialogProps { open: boolean; @@ -27,36 +30,52 @@ interface CreateAgentDialogProps { export function CreateAgentDialog({ open, onOpenChange }: CreateAgentDialogProps) { const navigate = useNavigate(); const { toast } = useToast(); + const queryClient = useQueryClient(); const [name, setName] = useState(""); const [selectedModel, setSelectedModel] = useState(""); - const aiModels = [ - { value: "google/gemini-2.5-pro", label: "Google Gemini 2.5 Pro" }, - { value: "google/gemini-2.5-flash", label: "Google Gemini 2.5 Flash" }, - { value: "google/gemini-2.5-flash-lite", label: "Google Gemini 2.5 Flash Lite" }, - { value: "openai/gpt-5", label: "OpenAI GPT-5" }, - { value: "openai/gpt-5-mini", label: "OpenAI GPT-5 Mini" }, - { value: "openai/gpt-5-nano", label: "OpenAI GPT-5 Nano" }, - ]; + const createAgentMutation = useMutation({ + mutationFn: ({ name, modelId }: { name: string; modelId: number }) => + createAgent(name, modelId), + onSuccess: (data) => { + toast({ + title: "Agente criado", + description: `${data.agent_name} foi criado com sucesso.`, + }); + + // Invalidate agents query to refetch the list + queryClient.invalidateQueries({ queryKey: ["agents"] }); + + onOpenChange(false); + + // Navegar para a tela de detalhes do novo agente + navigate(`/agents/${data.agent_id}`); + + // Reset form + setName(""); + setSelectedModel(""); + }, + onError: (error: any) => { + toast({ + title: "Erro ao criar agente", + description: error.response?.data?.message || "Ocorreu um erro ao criar o agente.", + variant: "destructive", + }); + }, + }); const handleCreate = () => { - // TODO: Implementar criação no backend - const newAgentId = Math.floor(Math.random() * 10000); // Gerar ID temporário - console.log("Criando agente:", { name, model: selectedModel }); - - toast({ - title: "Agente criado", - description: "O agente foi criado com sucesso.", - }); + const modelId = getModelIdByValue(selectedModel); + if (!modelId) { + toast({ + title: "Erro", + description: "Modelo de IA inválido.", + variant: "destructive", + }); + return; + } - onOpenChange(false); - - // Navegar para a tela de detalhes do novo agente - navigate(`/agents/${newAgentId}`); - - // Reset form - setName(""); - setSelectedModel(""); + createAgentMutation.mutate({ name, modelId }); }; return ( @@ -87,9 +106,9 @@ export function CreateAgentDialog({ open, onOpenChange }: CreateAgentDialogProps - {aiModels.map((model) => ( + {AI_MODELS.map((model) => ( - {model.label} + {model.name} ))} @@ -100,11 +119,11 @@ export function CreateAgentDialog({ open, onOpenChange }: CreateAgentDialogProps - diff --git a/src/lib/api/agents.ts b/src/lib/api/agents.ts new file mode 100644 index 0000000..60a235c --- /dev/null +++ b/src/lib/api/agents.ts @@ -0,0 +1,150 @@ +import api from "./axios"; +import type { + Agent, + AgentDetails, + AgentVersion, + CreateAgentRequest, + CreateAgentResponse, + EditAgentRequest, + EditAgentResponse, + EditAgentNameRequest, + EditAgentNameResponse, + ToggleAgentStatusRequest, + ToggleAgentStatusResponse, + ListAgentsResponse, +} from "../types/agent"; + +const estabelecimentoId = import.meta.env.VITE_ESTABELECIMENTO_ID; +const userEmail = import.meta.env.VITE_USER_EMAIL; + +/** + * Cria um novo agente + */ +export const createAgent = async ( + nome: string, + model_id: number +): Promise => { + const payload: CreateAgentRequest = { + estabelecimento_id: Number(estabelecimentoId), + user_email: userEmail, + nome, + model_id, + }; + + const response = await api.post( + "/webhook/promptlab/agente", + payload + ); + return response.data; +}; + +/** + * Lista todos os agentes de um estabelecimento + */ +export const listAgents = async ( + name?: string, + page: number = 1, + per_page: number = 6 +): Promise => { + const params = new URLSearchParams({ + page: page.toString(), + per_page: per_page.toString(), + }); + + if (name) { + params.append("name", name); + } + + const response = await api.get( + `/webhook/9041673e-cc46-4e3c-9127-6b5000efcc2d/promptlab/agentes/${estabelecimentoId}?${params.toString()}` + ); + return response.data; +}; + +/** + * Obtém detalhes de um agente específico + */ +export const getAgentDetails = async ( + agente_id: string +): Promise => { + const response = await api.get( + `/webhook/54eb2d09-6d59-4abe-89c9-661b12de5608/promptlab/detalhes-agente/${agente_id}` + ); + // A API retorna um array, mas só tem um elemento + return response.data[0]; +}; + +/** + * Edita um agente (cria uma nova versão) + */ +export const editAgent = async ( + agent_id: string, + model_id: number, + system_prompt: string, + version_type: "major" | "minor" | "patch", + notes: string +): Promise => { + const payload: EditAgentRequest = { + user_email: userEmail, + model_id, + system_prompt, + version_type, + notes, + }; + + const response = await api.put( + `/webhook/ef647dd5-6ebd-4e8e-83a3-c867595f4f8e/promptlab/agente/${agent_id}/edit`, + payload + ); + return response.data; +}; + +/** + * Edita apenas o nome do agente + */ +export const editAgentName = async ( + agent_id: string, + nome: string +): Promise => { + const payload: EditAgentNameRequest = { + nome, + user_email: userEmail, + }; + + const response = await api.put( + `/webhook/6c95305b-07e7-4a6f-aa64-e60f1f19ae54/promptlab/agente/${agent_id}/nome`, + payload + ); + return response.data; +}; + +/** + * Alterna o status do agente (ativo/inativo) + */ +export const toggleAgentStatus = async ( + agent_id: string, + status: "ativo" | "inativo" +): Promise => { + const payload: ToggleAgentStatusRequest = { + status, + user_email: userEmail, + }; + + const response = await api.put( + `/webhook/6c95305b-07e7-4a6f-aa64-e60f1f19ae54/promptlab/agente/${agent_id}/status`, + payload + ); + return response.data; +}; + +/** + * Obtém o histórico de versões de um agente + */ +export const getAgentVersions = async ( + agent_id: string +): Promise => { + const response = await api.get( + `/webhook/c7296b1d-e1d9-4cad-bbf4-2041281f3f47/promptlab/agente/${agent_id}/versions` + ); + return response.data; +}; diff --git a/src/lib/api/axios.ts b/src/lib/api/axios.ts new file mode 100644 index 0000000..456b8e6 --- /dev/null +++ b/src/lib/api/axios.ts @@ -0,0 +1,35 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +// Interceptor para adicionar credenciais em todas as requisições +api.interceptors.request.use((config) => { + // As credenciais são enviadas no body de acordo com a API N8N + return config; +}); + +// Interceptor para tratamento de erros +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response) { + // O servidor respondeu com um código de status fora do range 2xx + console.error("API Error:", error.response.data); + console.error("Status:", error.response.status); + } else if (error.request) { + // A requisição foi feita mas não houve resposta + console.error("Network Error:", error.request); + } else { + // Algo aconteceu ao configurar a requisição + console.error("Error:", error.message); + } + return Promise.reject(error); + } +); + +export default api; diff --git a/src/lib/api/chat.ts b/src/lib/api/chat.ts new file mode 100644 index 0000000..321c3ec --- /dev/null +++ b/src/lib/api/chat.ts @@ -0,0 +1,63 @@ +import api from "./axios"; +import type { + Chat, + ChatMessage, + TestAgentRequest, + TestAgentResponse, +} from "../types/chat"; + +const estabelecimentoId = import.meta.env.VITE_ESTABELECIMENTO_ID; +const userEmail = import.meta.env.VITE_USER_EMAIL; + +/** + * Envia uma mensagem para testar um agente + */ +export const testAgent = async ( + agent_id: string, + chat_id: string, + message: string +): Promise => { + const payload: TestAgentRequest = { + estabelecimento_id: Number(estabelecimentoId), + user_email: userEmail, + chat_id, + message, + }; + + const response = await api.post( + `/webhook/2c06f0b6-af09-4826-82dd-d1fc9fcd8227/promptlab/agente/${agent_id}/test`, + payload + ); + return response.data; +}; + +/** + * Lista todos os chats de teste de um usuário + */ +export const listChats = async (): Promise => { + const response = await api.get( + `/webhook/29b613e6-92dd-4922-a1a7-b1d54238c271/promptlab/listar-agente-chats/${estabelecimentoId}/${encodeURIComponent( + userEmail + )}` + ); + return response.data; +}; + +/** + * Lista todas as mensagens de um chat específico + */ +export const listMessages = async (chat_id: string): Promise => { + const response = await api.get( + `/webhook/eb7aff49-8b7e-488b-ab79-3e52bd51c1f6/promptlab/messages/${chat_id}` + ); + return response.data; +}; + +/** + * Deleta um chat (soft delete) + */ +export const deleteChat = async (chat_id: string): Promise => { + await api.delete( + `/webhook/8099ad7b-0879-4249-8b2a-fb8e1f243772/promptlab/agente-chat/${chat_id}` + ); +}; diff --git a/src/lib/constants/aiModels.ts b/src/lib/constants/aiModels.ts new file mode 100644 index 0000000..826bcf0 --- /dev/null +++ b/src/lib/constants/aiModels.ts @@ -0,0 +1,33 @@ +import { AIModel } from "../types/agent"; + +// Mapeamento de modelos de IA conforme a API +// Nota: Os IDs precisam ser confirmados com a API real +export const AI_MODELS: AIModel[] = [ + { id: 1, name: "Google Gemini 2.5 Pro", value: "google/gemini-2.5-pro" }, + { id: 2, name: "Google Gemini 2.5 Flash", value: "google/gemini-2.5-flash" }, + { id: 3, name: "Google Gemini 2.5 Flash Lite", value: "google/gemini-2.5-flash-lite" }, + { id: 4, name: "OpenAI GPT-5", value: "openai/gpt-5" }, + { id: 5, name: "OpenAI GPT-5 Mini", value: "openai/gpt-5-mini" }, + { id: 6, name: "OpenAI GPT-5 Nano", value: "openai/gpt-5-nano" }, + { id: 7, name: "Anthropic Claude Sonnet 4.5", value: "anthropic/claude-sonnet-4.5" }, + { id: 8, name: "Anthropic Claude Haiku 4.5", value: "anthropic/claude-haiku-4.5" }, + { id: 9, name: "Anthropic Claude Opus 4.5", value: "anthropic/claude-opus-4.5" }, + { id: 10, name: "Claude Haiku 4.5", value: "claude/haiku-4.5" }, +]; + +// Função auxiliar para obter o nome do modelo pelo ID +export const getModelNameById = (id: number): string => { + const model = AI_MODELS.find((m) => m.id === id); + return model?.name || "Modelo Desconhecido"; +}; + +// Função auxiliar para obter o modelo pelo value +export const getModelByValue = (value: string): AIModel | undefined => { + return AI_MODELS.find((m) => m.value === value); +}; + +// Função auxiliar para obter o ID pelo value +export const getModelIdByValue = (value: string): number | undefined => { + const model = getModelByValue(value); + return model?.id; +}; diff --git a/src/lib/types/agent.ts b/src/lib/types/agent.ts new file mode 100644 index 0000000..fc5d751 --- /dev/null +++ b/src/lib/types/agent.ts @@ -0,0 +1,120 @@ +export interface Agent { + agente_id: string; + nome_agente: string; + versao_agente: string; + modelo_ia: string; + status: "ativo" | "inativo"; + created_at: string; +} + +export interface AgentDetails { + id: string; + estabelecimento_id: number; + model_id: number; + name: string; + system_prompt: string; + status: "ativo" | "inativo"; + version_id: string; + created_by: string; + updated_by: string; + created_at: string; + updated_at: string; +} + +export interface AgentVersion { + versao_id: string; + numero_versao: string; + notas_versao: string; + modelo_ia_nome: string; + system_prompt: string; + criado_por: string; + data_criacao: string; +} + +export interface CreateAgentRequest { + estabelecimento_id: number; + user_email: string; + nome: string; + model_id: number; +} + +export interface CreateAgentResponse { + success: boolean; + agent_id: string; + agent_name: string; + version_id: string; + version_number: string; + version_type: string; + model_id: string; + system_prompt: string; +} + +export interface EditAgentRequest { + user_email: string; + model_id: number; + system_prompt: string; + version_type: "major" | "minor" | "patch"; + notes: string; +} + +export interface EditAgentResponse { + success: boolean; + agent_id: string; + agent_name: string; + version_id: string; + version_number: string; + version_type: string; + model_id: string; + system_prompt: string; +} + +export interface EditAgentNameRequest { + nome: string; + user_email: string; +} + +export interface EditAgentNameResponse { + success: boolean; + agent_id: string; + estabelecimento_id: string; + agent_name: string; + model_id: string; + status: string; + updated_by: string; + updated_at: string; +} + +export interface ToggleAgentStatusRequest { + status: "ativo" | "inativo"; + user_email: string; +} + +export interface ToggleAgentStatusResponse { + success: boolean; + agent_id: string; + estabelecimento_id: string; + model_id: string; + agent_name: string; + system_prompt: string; + status: "ativo" | "inativo"; + version_id: string; + created_by: string; + updated_by: string; + created_at: string; + updated_at: string; +} + +export interface ListAgentsResponse { + success: boolean; + page: number; + per_page: number; + total: number; + total_pages: number; + data: Agent[]; +} + +export interface AIModel { + id: number; + name: string; + value: string; +} diff --git a/src/lib/types/chat.ts b/src/lib/types/chat.ts new file mode 100644 index 0000000..356aa8c --- /dev/null +++ b/src/lib/types/chat.ts @@ -0,0 +1,35 @@ +export interface ChatMessage { + id: string; + agent_chat_id: string; + role: "user" | "assistant"; + content: string; + input_tokens: number; + output_tokens: number; + total_tokens: number; + cost_usd: string; + created_at: string; +} + +export interface Chat { + id: string; + agent_id: string; + estabelecimento_id: number; + user_email: string; + title: string; + created_at: string; + updated_at: string; + deleted_at: string | null; +} + +export interface TestAgentRequest { + estabelecimento_id: number; + user_email: string; + chat_id: string; + message: string; +} + +export interface TestAgentResponse { + success: boolean; + chat_id: string; + response: string; +} diff --git a/src/main.tsx b/src/main.tsx index 68176a7..84e7e14 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,20 @@ import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./App.tsx"; import "./index.css"; -createRoot(document.getElementById("root")!).render(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 5 * 60 * 1000, // 5 minutos + }, + }, +}); + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/src/pages/AgentDetails.tsx b/src/pages/AgentDetails.tsx index cb52dfc..ad504a3 100644 --- a/src/pages/AgentDetails.tsx +++ b/src/pages/AgentDetails.tsx @@ -1,11 +1,12 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ArrowLeft, Save, History, Power, Pencil, Check, X } from "lucide-react"; +import { ArrowLeft, Save, History, Power, Pencil, Check, X, Loader2 } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { VersionHistoryDialog } from "@/components/agents/VersionHistoryDialog"; import { SaveVersionDialog } from "@/components/agents/SaveVersionDialog"; @@ -20,126 +21,214 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { + getAgentDetails, + editAgent, + editAgentName, + toggleAgentStatus, + getAgentVersions, +} from "@/lib/api/agents"; +import { AI_MODELS, getModelIdByValue } from "@/lib/constants/aiModels"; export default function AgentDetails() { const { id } = useParams(); const navigate = useNavigate(); const { toast } = useToast(); + const queryClient = useQueryClient(); const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isSaveVersionOpen, setIsSaveVersionOpen] = useState(false); const [isDeactivateDialogOpen, setIsDeactivateDialogOpen] = useState(false); const [isEditingName, setIsEditingName] = useState(false); const [tempName, setTempName] = useState(""); + const [selectedModel, setSelectedModel] = useState(""); + const [systemPrompt, setSystemPrompt] = useState(""); - // Mock data - substituir por dados reais do backend - const [agent, setAgent] = useState<{ - id: string | undefined; - name: string; - status: "active" | "inactive"; - model: string; - systemPrompt: string; - }>({ - id: id, - name: "Agente de Atendimento", - status: "active", - model: "google/gemini-2.5-flash", - systemPrompt: "Você é um assistente de atendimento ao cliente. Seja educado, prestativo e objetivo nas suas respostas. Sempre mantenha um tom profissional e cordial.", + // Fetch agent details + const { + data: agentDetails, + isLoading, + isError, + refetch, + } = useQuery({ + queryKey: ["agent", id], + queryFn: () => getAgentDetails(id!), + enabled: !!id, }); - const [versions] = useState([ - { - id: 1, - version: "2.1", - type: "major" as const, - description: "Melhorias na clareza das respostas", - model: "Google Gemini 2.5 Flash", - prompt: "Você é um assistente de atendimento ao cliente. Seja educado, prestativo e objetivo nas suas respostas. Sempre mantenha um tom profissional e cordial.", - createdAt: "2024-01-15 14:30", - author: "João Silva", - }, - { - id: 2, - version: "2.0", - type: "major" as const, - description: "Reformulação completa do prompt", - model: "Google Gemini 2.5 Pro", - prompt: "Você é um assistente de atendimento. Seja cordial e ajude o cliente.", - createdAt: "2024-01-10 09:15", - author: "Maria Santos", - }, - { - id: 3, - version: "1.5", - type: "minor" as const, - description: "Ajustes de tom", - model: "OpenAI GPT-5 Mini", - prompt: "Você é um assistente. Seja educado.", - createdAt: "2024-01-05 16:45", - author: "João Silva", - }, - ]); + // Fetch agent versions for history + const { + data: versions, + isLoading: isLoadingVersions, + } = useQuery({ + queryKey: ["agent-versions", id], + queryFn: () => getAgentVersions(id!), + enabled: !!id && isHistoryOpen, + }); - const aiModels = [ - { value: "google/gemini-2.5-pro", label: "Google Gemini 2.5 Pro" }, - { value: "google/gemini-2.5-flash", label: "Google Gemini 2.5 Flash" }, - { value: "google/gemini-2.5-flash-lite", label: "Google Gemini 2.5 Flash Lite" }, - { value: "openai/gpt-5", label: "OpenAI GPT-5" }, - { value: "openai/gpt-5-mini", label: "OpenAI GPT-5 Mini" }, - { value: "openai/gpt-5-nano", label: "OpenAI GPT-5 Nano" }, - ]; + // Update local state when agent details are loaded + useEffect(() => { + if (agentDetails) { + const model = AI_MODELS.find((m) => m.id === agentDetails.model_id); + setSelectedModel(model?.value || ""); + setSystemPrompt(agentDetails.system_prompt); + setTempName(agentDetails.name); + } + }, [agentDetails]); + + // Edit agent mutation (save new version) + const editAgentMutation = useMutation({ + mutationFn: ({ + model_id, + system_prompt, + version_type, + notes, + }: { + model_id: number; + system_prompt: string; + version_type: "major" | "minor" | "patch"; + notes: string; + }) => editAgent(id!, model_id, system_prompt, version_type, notes), + onSuccess: (data) => { + toast({ + title: "Agente atualizado", + description: `Nova versão ${data.version_type} salva com sucesso.`, + }); + queryClient.invalidateQueries({ queryKey: ["agent", id] }); + queryClient.invalidateQueries({ queryKey: ["agent-versions", id] }); + }, + onError: (error: any) => { + toast({ + title: "Erro ao atualizar agente", + description: error.response?.data?.message || "Ocorreu um erro ao atualizar o agente.", + variant: "destructive", + }); + }, + }); + + // Edit agent name mutation + const editNameMutation = useMutation({ + mutationFn: (nome: string) => editAgentName(id!, nome), + onSuccess: () => { + toast({ + title: "Nome atualizado", + description: "O nome do agente foi alterado com sucesso.", + }); + queryClient.invalidateQueries({ queryKey: ["agent", id] }); + queryClient.invalidateQueries({ queryKey: ["agents"] }); + setIsEditingName(false); + }, + onError: (error: any) => { + toast({ + title: "Erro ao atualizar nome", + description: error.response?.data?.message || "Ocorreu um erro ao atualizar o nome.", + variant: "destructive", + }); + }, + }); + + // Toggle status mutation + const toggleStatusMutation = useMutation({ + mutationFn: (status: "ativo" | "inativo") => toggleAgentStatus(id!, status), + onSuccess: (data) => { + toast({ + title: "Status atualizado", + description: `Agente ${data.status === "ativo" ? "ativado" : "desativado"} com sucesso.`, + }); + queryClient.invalidateQueries({ queryKey: ["agent", id] }); + queryClient.invalidateQueries({ queryKey: ["agents"] }); + setIsDeactivateDialogOpen(false); + }, + onError: (error: any) => { + toast({ + title: "Erro ao atualizar status", + description: error.response?.data?.message || "Ocorreu um erro ao atualizar o status.", + variant: "destructive", + }); + }, + }); const handleSave = (versionType: "major" | "minor" | "patch", notes: string) => { - // TODO: Implementar salvamento no backend - console.log("Saving agent:", agent, "Version type:", versionType, "Notes:", notes); - toast({ - title: "Agente atualizado", - description: `Nova versão ${versionType} salva com sucesso.`, + const modelId = getModelIdByValue(selectedModel); + if (!modelId) { + toast({ + title: "Erro", + description: "Modelo de IA inválido.", + variant: "destructive", + }); + return; + } + + editAgentMutation.mutate({ + model_id: modelId, + system_prompt: systemPrompt, + version_type: versionType, + notes, }); }; const handleToggleStatus = () => { - if (agent.status === "active") { + if (agentDetails?.status === "ativo") { setIsDeactivateDialogOpen(true); } else { - // Ativar diretamente sem modal - setAgent({ ...agent, status: "active" }); - toast({ - title: "Status atualizado", - description: "Agente ativado com sucesso.", - }); + toggleStatusMutation.mutate("ativo"); } }; const handleConfirmDeactivate = () => { - setAgent({ ...agent, status: "inactive" }); - toast({ - title: "Status atualizado", - description: "Agente desativado com sucesso. Esta ação foi registrada.", - }); - setIsDeactivateDialogOpen(false); + toggleStatusMutation.mutate("inativo"); }; const handleStartEditName = () => { - setTempName(agent.name); + setTempName(agentDetails?.name || ""); setIsEditingName(true); }; const handleSaveName = () => { if (tempName.trim()) { - setAgent({ ...agent, name: tempName.trim() }); - setIsEditingName(false); - toast({ - title: "Nome atualizado", - description: "O nome do agente foi alterado com sucesso.", - }); + editNameMutation.mutate(tempName.trim()); } }; const handleCancelEditName = () => { setIsEditingName(false); - setTempName(""); + setTempName(agentDetails?.name || ""); }; + // Format versions for the history dialog + const formattedVersions = versions?.map((v, index) => ({ + id: index + 1, + version: v.numero_versao, + type: (v.notas_versao.toLowerCase().includes("major") + ? "major" + : v.notas_versao.toLowerCase().includes("minor") + ? "minor" + : "patch") as "major" | "minor" | "patch", + description: v.notas_versao, + model: v.modelo_ia_nome, + prompt: v.system_prompt, + createdAt: v.data_criacao, + author: v.criado_por, + })) || []; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError || !agentDetails) { + return ( +
+

Erro ao carregar detalhes do agente.

+ +
+ ); + } + return (
{/* Header */} @@ -172,6 +261,7 @@ export default function AgentDetails() { variant="ghost" className="h-8 w-8 p-0" onClick={handleSaveName} + disabled={editNameMutation.isPending} > @@ -186,7 +276,7 @@ export default function AgentDetails() {
) : ( <> -

{agent.name}

+

{agentDetails.name}

-