Nova versao codex Parecer, atualizacao completa

This commit is contained in:
Vitex Tecnologia
2026-03-18 01:07:27 -03:00
parent 39032ea621
commit 6bf7e7d42e
49 changed files with 8003 additions and 2486 deletions
+84 -262
View File
@@ -1,9 +1,6 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
/**
* Interface para resposta do POST de parecer técnico
*/
export interface CreateOpinionResponse {
success: boolean;
id?: string;
@@ -11,9 +8,6 @@ export interface CreateOpinionResponse {
file_url_melhoria?: string;
}
/**
* Interface para requisição de criação de parecer
*/
export interface CreateOpinionRequest {
titulo: string;
categoria: string;
@@ -22,14 +16,8 @@ export interface CreateOpinionRequest {
estabelecimentoId?: number;
}
/**
* Status do parecer
*/
export type OpinionStatus = 'processando' | 'concluido' | 'erro';
/**
* Interface para um parecer retornado pela API
*/
export interface OpinionRecord {
id: string;
estabelecimento_id: number;
@@ -40,13 +28,10 @@ export interface OpinionRecord {
file_url: string;
created_at: string;
file_url_melhoria: string;
status?: OpinionStatus; // Status do processamento do parecer (pode vir da API ou ser local)
isLocalPending?: boolean; // Flag para indicar se é um registro local temporário
status?: OpinionStatus;
isLocalPending?: boolean;
}
/**
* Interface para parâmetros de paginação e busca
*/
export interface GetOpinionsParams {
page?: number;
per_page?: number;
@@ -55,9 +40,6 @@ export interface GetOpinionsParams {
estabelecimentoId?: number;
}
/**
* Interface para resposta do GET de pareceres
*/
export interface GetOpinionsResponse {
data: OpinionRecord[];
total: number;
@@ -65,74 +47,53 @@ export interface GetOpinionsResponse {
per_page: number;
}
/**
* Serviço para gerenciamento de pareceres jurídicos
*/
type ApiErrorShape = { message?: string; status?: number };
class AgentService {
private readonly CREATE_OPINION_ENDPOINT = '/webhook/codex/gepam/parecer-tecnico';
private readonly GET_OPINIONS_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_parecer';
private readonly CREATE_ENDPOINT = '/webhook/codex/gepam/parecer-tecnico';
private readonly OPINIONS_BASE = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex';
/**
* Cria um novo parecer técnico
*
* @param request - Dados do parecer
* @returns Promise com a resposta da API
*/
async createOpinion(request: CreateOpinionRequest): Promise<CreateOpinionResponse> {
const {
titulo,
categoria,
instrucoes,
userEmail,
estabelecimentoId
} = request;
// Usa valores do GlobalFunctions se não forem fornecidos
private resolveUserContext(userEmail?: string, estabelecimentoId?: number) {
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
const estabId = estabelecimentoId || GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
return { email, estabId };
}
// Validações
if (!titulo || titulo.trim().length === 0) {
throw {
success: false,
message: 'Título do parecer é obrigatório',
};
}
if (!instrucoes || instrucoes.trim().length === 0) {
throw {
success: false,
message: 'Instruções são obrigatórias',
};
}
private assertUserContext(email: unknown, estabId: unknown) {
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
throw { success: false, message: 'Email do usuário não fornecido' };
}
if (!estabId) {
throw {
success: false,
message: 'ID do estabelecimento não fornecido',
};
throw { success: false, message: 'ID do estabelecimento não fornecido' };
}
}
private toApiError(error: unknown): never {
const e = error as ApiErrorShape;
throw {
success: false,
message: e?.message || 'Erro desconhecido',
status: e?.status,
};
}
async createOpinion(request: CreateOpinionRequest): Promise<CreateOpinionResponse> {
const { titulo, categoria, instrucoes, userEmail, estabelecimentoId } = request;
if (!titulo?.trim()) {
throw { success: false, message: 'Título do parecer é obrigatório' };
}
// Log para debug
console.log('Criando parecer:', {
titulo,
categoria,
instrucoesLength: instrucoes.length,
userEmail: email,
estabelecimentoId: estabId,
});
if (!instrucoes?.trim()) {
throw { success: false, message: 'Instruções são obrigatórias' };
}
const { email, estabId } = this.resolveUserContext(userEmail, estabelecimentoId);
this.assertUserContext(email, estabId);
try {
// Faz a requisição usando o serviço de API com timeout de 5 minutos
const response = await apiService.post<CreateOpinionResponse>(
this.CREATE_OPINION_ENDPOINT,
this.CREATE_ENDPOINT,
{
user_email: email,
estabelecimento_id: estabId,
@@ -140,245 +101,106 @@ class AgentService {
categoria: categoria.trim(),
instrucoes: instrucoes.trim(),
},
{
timeout: 600000, // 5 minutos para geração de parecer (processo demorado)
}
{ timeout: 600_000 }
);
console.log('Resposta da API (criar parecer):', response.data);
return response.data;
} catch (error: any) {
// Trata erros específicos
} catch (error: unknown) {
console.error('Erro ao criar parecer:', error);
throw {
success: false,
message: error.message || 'Erro ao criar parecer',
status: error.status,
};
this.toApiError(error);
}
}
/**
* Busca todos os pareceres do usuário com paginação e busca
*
* @param params - Parâmetros de paginação e busca
* @returns Promise com array de pareceres
*/
async getOpinions(params?: GetOpinionsParams): Promise<OpinionRecord[]> {
const {
page = 1,
per_page = 10,
search = '',
userEmail,
estabelecimentoId
} = params || {};
const { page = 1, per_page = 10, search = '', userEmail, estabelecimentoId } = params || {};
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
const estabId = estabelecimentoId || GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
if (!estabId) {
throw {
success: false,
message: 'ID do estabelecimento não fornecido',
};
}
console.log('Buscando pareceres:', {
userEmail: email,
estabelecimentoId: estabId,
page,
per_page,
search,
});
const { email, estabId } = this.resolveUserContext(userEmail, estabelecimentoId);
this.assertUserContext(email, estabId);
try {
// Constrói a URL com parâmetros de query
const url = `${this.GET_OPINIONS_ENDPOINT}/${email}/${estabId}`;
const url = `${this.OPINIONS_BASE}/get_parecer/${email}/${estabId}`;
const response = await apiService.get<OpinionRecord[]>(url, {
params: {
page,
per_page,
search,
},
const response = await apiService.get<OpinionRecord[] | GetOpinionsResponse>(url, {
params: { page, per_page, search },
});
console.log('Resposta da API (buscar pareceres):', response.data);
const raw = response.data;
// A API retorna diretamente o array de pareceres
// Garante que sempre retorna um array
if (Array.isArray(response.data)) {
return response.data;
} else if (response.data && typeof response.data === 'object') {
// Se a resposta for um objeto, tenta encontrar o array dentro dele
console.warn('API retornou objeto em vez de array:', response.data);
if (Array.isArray(raw)) return raw;
if (Array.isArray((response.data as any).data)) {
return (response.data as any).data;
} else if (Array.isArray((response.data as any).opinions)) {
return (response.data as any).opinions;
if (raw && typeof raw === 'object') {
if (Array.isArray((raw as GetOpinionsResponse).data)) return (raw as GetOpinionsResponse).data;
if (Array.isArray((raw as { opinions?: OpinionRecord[] }).opinions)) {
return (raw as unknown as { opinions: OpinionRecord[] }).opinions;
}
}
// Se não conseguir extrair array, retorna vazio
console.warn('Não foi possível extrair array de pareceres da resposta');
return [];
} catch (error: any) {
} catch (error: unknown) {
console.error('Erro ao buscar pareceres:', error);
throw {
success: false,
message: error.message || 'Erro ao buscar pareceres',
status: error.status,
};
this.toApiError(error);
}
}
/**
* Faz download de um arquivo de parecer
*
* @param fileUrl - URL do arquivo a ser baixado
* @param fileName - Nome do arquivo para download
*/
async downloadOpinion(fileUrl: string, fileName: string): Promise<void> {
if (!fileUrl) {
throw {
success: false,
message: 'URL do arquivo não fornecida',
};
throw { success: false, message: 'URL do arquivo não fornecida' };
}
const triggerDownload = (href: string) => {
const link = document.createElement('a');
link.href = href;
link.download = fileName;
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
try {
console.log('Iniciando download:', { fileUrl, fileName });
const response = await fetch(fileUrl, { method: 'GET', mode: 'cors', cache: 'no-cache' });
// Tenta primeiro fazer o download via fetch (funciona se CORS estiver configurado)
try {
const response = await fetch(fileUrl, {
method: 'GET',
mode: 'cors',
cache: 'no-cache',
});
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
// Converte a resposta em blob
const blob = await response.blob();
// Cria uma URL temporária para o blob
const blobUrl = URL.createObjectURL(blob);
// Cria um elemento <a> temporário para forçar o download
const link = document.createElement('a');
link.href = blobUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
// Remove o elemento e libera a URL temporária
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
console.log('Download via fetch concluído:', { fileUrl, fileName });
return;
} catch (fetchError: any) {
console.warn('Erro no download via fetch, tentando método alternativo:', fetchError.message);
// Se falhar (erro de CORS), usa o método alternativo de abrir em nova aba
// Isso permite que o navegador force o download mesmo com restrições de CORS
const link = document.createElement('a');
link.href = fileUrl;
link.download = fileName;
link.target = '_blank';
link.rel = 'noopener noreferrer';
// Para S3, podemos tentar adicionar parâmetros que forçam o download
const url = new URL(fileUrl);
url.searchParams.set('response-content-disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
link.href = url.toString();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log('Download via link direto iniciado:', { fileUrl, fileName });
if (!response.ok) {
throw new Error(`Erro HTTP: ${response.status}`);
}
} catch (error: any) {
console.error('Erro ao fazer download:', error);
throw {
success: false,
message: error.message || 'Erro ao fazer download do arquivo. Verifique se a URL está acessível.',
};
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
triggerDownload(blobUrl);
URL.revokeObjectURL(blobUrl);
} catch {
const url = new URL(fileUrl);
url.searchParams.set('response-content-disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
triggerDownload(url.toString());
}
}
/**
* Exclui um parecer técnico
*
* @param opinionId - ID do parecer a ser excluído
* @param userEmail - Email do usuário (opcional, usa GlobalFunctions se não fornecido)
* @returns Promise com a resposta da API
*/
async deleteOpinion(opinionId: string, userEmail?: string): Promise<{ success: boolean }> {
// Usa valores do GlobalFunctions se não forem fornecidos
if (!opinionId) {
throw { success: false, message: 'ID do parecer não fornecido' };
}
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!opinionId) {
throw {
success: false,
message: 'ID do parecer não fornecido',
};
}
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
throw { success: false, message: 'Email do usuário não fornecido' };
}
console.log('Excluindo parecer:', {
opinionId,
userEmail: email,
});
try {
// Constrói a URL com o user_email e id
const url = `/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_parecer/${email}/${opinionId}`;
const url = `${this.OPINIONS_BASE}/delete_parecer/${email}/${opinionId}`;
const response = await apiService.delete<{ success: boolean }[]>(url);
console.log('Resposta da API (excluir parecer):', response.data);
// A API retorna um array com { success: true }
if (Array.isArray(response.data) && response.data.length > 0) {
return response.data[0];
}
return { success: true };
} catch (error: any) {
} catch (error: unknown) {
console.error('Erro ao excluir parecer:', error);
throw {
success: false,
message: error.message || 'Erro ao excluir parecer',
status: error.status,
};
this.toApiError(error);
}
}
}
// Exporta instância única (Singleton)
export const agentService = new AgentService();
+17 -75
View File
@@ -1,63 +1,34 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
/**
* Configuração centralizada da API
* Todas as chamadas de API devem usar este serviço para garantir
* autenticação e configuração consistente
*/
class ApiService {
private axiosInstance: AxiosInstance;
private apiKey: string;
private baseURL: string;
private readonly axiosInstance: AxiosInstance;
private readonly apiKey: string;
constructor() {
// Busca configurações das variáveis de ambiente
this.apiKey = import.meta.env.VITE_API_KEY || '';
this.baseURL = import.meta.env.VITE_API_BASE_URL || '';
const baseURL = import.meta.env.VITE_API_BASE_URL || '';
// Validação das variáveis de ambiente
if (!this.apiKey) {
console.error('VITE_API_KEY não configurada no arquivo .env');
}
if (!this.baseURL) {
console.error('VITE_API_BASE_URL não configurada no arquivo .env');
}
// Cria instância do Axios com configurações padrão
this.axiosInstance = axios.create({
baseURL: this.baseURL,
timeout: 60000, // 60 segundos para upload de arquivos
headers: {
'Content-Type': 'application/json',
},
baseURL,
timeout: 60_000,
headers: { 'Content-Type': 'application/json' },
});
// Interceptor para adicionar API Key em todas as requisições
this.axiosInstance.interceptors.request.use(
(config) => {
// Adiciona a API Key no header de todas as requisições
if (this.apiKey) {
config.headers['apikey'] = this.apiKey;
}
return config;
},
(error) => {
return Promise.reject(error);
this.axiosInstance.interceptors.request.use((config) => {
if (this.apiKey) {
config.headers['apikey'] = this.apiKey;
}
);
return config;
});
// Interceptor de resposta para tratamento centralizado de erros
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
// Log de erro para debug
console.error('API Error:', {
message: error.message,
status: error.response?.status,
data: error.response?.data,
});
// Retorna erro formatado
return Promise.reject({
message: error.response?.data?.message || error.message || 'Erro ao comunicar com o servidor',
status: error.response?.status,
@@ -67,61 +38,32 @@ class ApiService {
);
}
/**
* Requisição GET
*/
async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.axiosInstance.get<T>(url, config);
}
/**
* Requisição POST
*/
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
post<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.axiosInstance.post<T>(url, data, config);
}
/**
* Requisição POST com FormData (para upload de arquivos)
*/
async postFormData<T = any>(url: string, formData: FormData, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
postFormData<T = unknown>(url: string, formData: FormData, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.axiosInstance.post<T>(url, formData, {
...config,
headers: {
...config?.headers,
'Content-Type': 'multipart/form-data',
},
headers: { ...config?.headers, 'Content-Type': 'multipart/form-data' },
});
}
/**
* Requisição PUT
*/
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
put<T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.axiosInstance.put<T>(url, data, config);
}
/**
* Requisição DELETE
*/
async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.axiosInstance.delete<T>(url, config);
}
/**
* Retorna a URL base configurada
*/
getBaseURL(): string {
return this.baseURL;
}
/**
* Retorna a instância do Axios (uso avançado)
*/
getInstance(): AxiosInstance {
return this.axiosInstance;
}
}
// Exporta instância única (Singleton)
export const apiService = new ApiService();
+183
View File
@@ -0,0 +1,183 @@
import { apiService } from "./api";
export interface AreaItem {
id: string;
nome: string;
descricao: string;
created_at: string;
updated_at: string;
}
export interface CriarAreaRequest {
nome: string;
descricao: string;
}
export interface CriarAreaSuccessResponse {
success: true;
id: string;
nome: string;
descricao: string;
}
export interface CriarAreaErrorResponse {
success: false;
message: string;
}
export type CriarAreaResponse = CriarAreaSuccessResponse | CriarAreaErrorResponse;
export interface EditarAreaBody {
nome: string;
descricao: string;
}
export interface EditarAreaSuccessResponse {
success: true;
id: string;
nome: string;
descricao: string;
}
export interface EditarAreaErrorResponse {
success: false;
message: string;
}
export interface DeletarAreaSuccessResponse {
success: true;
message: string;
}
export interface DeletarAreaErrorResponse {
success: false;
message: string;
}
export interface ListarAreasParams {
nome?: string;
page: number;
per_page: number;
}
export interface ListarAreasResponseBody {
success: true;
total_registros: number;
total_paginas: number;
per_page: number;
pagina_atual: number;
data: AreaItem[];
}
class AreasService {
async criar(nome: string, descricao: string): Promise<CriarAreaSuccessResponse> {
const body: CriarAreaRequest = {
nome: nome.trim(),
descricao: (descricao ?? "").trim(),
};
const response = await apiService.post<CriarAreaResponse>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/criar-area",
body
);
if (response.data.success === false) {
throw new Error(response.data.message ?? "Erro ao criar área");
}
return response.data as CriarAreaSuccessResponse;
}
async listar(params: ListarAreasParams): Promise<ListarAreasResponseBody> {
const { page, per_page, nome } = params;
const response = await apiService.get<ListarAreasResponseBody[]>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/listar-areas",
{
params: {
...(nome != null && nome.trim() !== "" ? { nome: nome.trim() } : {}),
page,
per_page,
},
}
);
const first = Array.isArray(response.data) ? response.data[0] : response.data;
if (!first || first.success !== true) {
throw new Error("Resposta inválida ao listar áreas");
}
return first;
}
async listarTotal(): Promise<AreaItem[]> {
interface ListarAreasTotalResponseItem {
success: true;
data: AreaItem[];
}
const response = await apiService.get<ListarAreasTotalResponseItem[]>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/listar-areas-total"
);
const first = Array.isArray(response.data) ? response.data[0] : response.data;
if (!first || first.success !== true || !Array.isArray(first.data)) {
throw new Error("Resposta inválida ao listar áreas");
}
return first.data;
}
async editar(id: string, nome: string, descricao: string): Promise<EditarAreaSuccessResponse> {
if (!id?.trim()) {
throw new Error("ID da área é obrigatório");
}
const body: EditarAreaBody = {
nome: nome.trim(),
descricao: (descricao ?? "").trim(),
};
const response = await apiService.put<EditarAreaSuccessResponse | EditarAreaErrorResponse>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/area/${id}`,
body
);
if (response.data.success === false) {
throw new Error((response.data as EditarAreaErrorResponse).message ?? "Erro ao editar área");
}
return response.data as EditarAreaSuccessResponse;
}
async deletar(id: string): Promise<DeletarAreaSuccessResponse> {
if (!id?.trim()) {
throw new Error("ID da área é obrigatório");
}
const idEnc = encodeURIComponent(id.trim());
/** Mesmo prefixo de webhook que `parecerApi.excluir` (fluxo n8n com UUID). */
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/4e6c2374-9c22-4c81-b558-45f0cfefa5c3/codex/parecer/areas/deletar/${idEnc}`;
const response = await apiService.delete<
DeletarAreaSuccessResponse | DeletarAreaErrorResponse | (DeletarAreaSuccessResponse | DeletarAreaErrorResponse)[]
>(url);
const raw = response.data;
const data = Array.isArray(raw) ? raw[0] : raw;
if (!data || typeof data !== "object" || !("success" in data)) {
throw new Error("Resposta inválida ao excluir área");
}
if (data.success === false) {
throw new Error((data as DeletarAreaErrorResponse).message ?? "Erro ao excluir área");
}
return data as DeletarAreaSuccessResponse;
}
}
export const areasService = new AreasService();
+66 -223
View File
@@ -1,3 +1,6 @@
import axios from 'axios';
import { GlobalFunctions } from '@/GlobalFunctions';
export interface AsanaWorkspaceResponse {
gid: string;
resource_type: string;
@@ -27,6 +30,7 @@ export interface AsanaUser {
id: string;
name: string;
}
export interface AsanaIntegrationResponse {
success: boolean;
integracao_id?: string;
@@ -47,298 +51,137 @@ export interface AsanaIntegrationRequest {
usuario_asana_nome: string;
}
type AxiosLikeError = { response?: { data?: { message?: string }; status?: number } };
class AsanaService {
private readonly ASANA_BASE_URL = 'https://app.asana.com/api/1.0';
private asanaHeaders(token: string): Record<string, string> {
return {
accept: 'application/json',
authorization: `Bearer ${token.trim()}`,
};
}
private async n8nHeaders(): Promise<Record<string, string>> {
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
accept: 'application/json',
};
if (apiKey) headers['apikey'] = apiKey;
if (token) headers['Authorization'] = `Bearer ${token}`;
return headers;
}
private handleAsanaError(error: unknown, fallback: string): never {
const e = error as AxiosLikeError;
if (e?.response?.status === 401) {
throw { success: false, message: 'Token inválido ou expirado. Verifique sua chave de API.' };
}
if (e?.response?.data) {
throw { success: false, message: e.response.data.message || fallback };
}
throw { success: false, message: error instanceof Error ? error.message : fallback };
}
async getWorkspaces(token: string): Promise<AsanaWorkspace[]> {
if (!token || token.trim().length === 0) {
throw {
success: false,
message: 'Token do Asana é obrigatório',
};
if (!token?.trim()) {
throw { success: false, message: 'Token do Asana é obrigatório' };
}
try {
const axios = (await import('axios')).default;
const headers: Record<string, string> = {
'accept': 'application/json',
'authorization': `Bearer ${token.trim()}`,
};
const response = await axios.get<AsanaWorkspacesResponse>(
`${this.ASANA_BASE_URL}/workspaces`,
{ headers }
{ headers: this.asanaHeaders(token) }
);
return response.data.data.map((workspace) => ({
id: workspace.gid,
name: workspace.name,
}));
return response.data.data.map((w) => ({ id: w.gid, name: w.name }));
} catch (error: unknown) {
console.error('Erro ao buscar workspaces do Asana:', error);
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: any; status?: number } };
if (axiosError.response?.status === 401) {
throw {
success: false,
message: 'Token inválido ou expirado. Verifique sua chave de API.',
};
}
if (axiosError.response?.data) {
throw {
success: false,
message: axiosError.response.data.message || 'Erro ao buscar workspaces do Asana',
};
}
}
throw {
success: false,
message: error instanceof Error ? error.message : 'Erro ao buscar workspaces do Asana',
};
this.handleAsanaError(error, 'Erro ao buscar workspaces do Asana');
}
}
async getUsers(token: string, workspaceId: string): Promise<AsanaUser[]> {
if (!token || token.trim().length === 0) {
throw {
success: false,
message: 'Token do Asana é obrigatório',
};
if (!token?.trim()) {
throw { success: false, message: 'Token do Asana é obrigatório' };
}
if (!workspaceId || workspaceId.trim().length === 0) {
throw {
success: false,
message: 'ID do workspace é obrigatório',
};
if (!workspaceId?.trim()) {
throw { success: false, message: 'ID do workspace é obrigatório' };
}
try {
const axios = (await import('axios')).default;
const headers: Record<string, string> = {
'accept': 'application/json',
'authorization': `Bearer ${token.trim()}`,
};
const response = await axios.get<AsanaUsersResponse>(
`${this.ASANA_BASE_URL}/users?workspace=${workspaceId}`,
{ headers }
{ headers: this.asanaHeaders(token) }
);
// Converte a resposta da API para o formato esperado pelo componente
// A API retorna gid, mas o componente espera id
return response.data.data.map((user) => ({
id: user.gid,
name: user.name,
}));
return response.data.data.map((u) => ({ id: u.gid, name: u.name }));
} catch (error: unknown) {
console.error('Erro ao buscar usuários do Asana:', error);
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: any; status?: number } };
if (axiosError.response?.status === 401) {
throw {
success: false,
message: 'Token inválido ou expirado. Verifique sua chave de API.',
};
}
if (axiosError.response?.data) {
throw {
success: false,
message: axiosError.response.data.message || 'Erro ao buscar usuários do Asana',
};
}
}
throw {
success: false,
message: error instanceof Error ? error.message : 'Erro ao buscar usuários do Asana',
};
this.handleAsanaError(error, 'Erro ao buscar usuários do Asana');
}
}
async getIntegration(userId: string): Promise<AsanaIntegrationResponse | null> {
if (!userId || userId.trim().length === 0) {
throw {
success: false,
message: 'ID do usuário é obrigatório',
};
if (!userId?.trim()) {
throw { success: false, message: 'ID do usuário é obrigatório' };
}
try {
const axios = (await import('axios')).default;
const { GlobalFunctions } = await import('@/GlobalFunctions');
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'accept': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const headers = await this.n8nHeaders();
const response = await axios.get<AsanaIntegrationResponse>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/c898beff-84cb-44df-a69c-6eff27ccd7aa/codex/agente-pessoal/integracoes/asana/${userId}`,
{ headers }
);
if (!response.data.success) {
return null;
}
return response.data;
return response.data.success ? response.data : null;
} catch (error: unknown) {
console.error('Erro ao buscar integração do Asana:', error);
// Se o erro for 404 ou similar, significa que não tem integração cadastrada
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
return null;
}
}
const e = error as AxiosLikeError;
if (e?.response?.status === 404) return null;
return null;
}
}
async createIntegration(request: AsanaIntegrationRequest): Promise<AsanaIntegrationResponse> {
if (!request.user_id || !request.api_key || !request.workspace_gid || !request.usuario_asana_gid) {
throw {
success: false,
message: 'Dados incompletos para criar integração',
};
throw { success: false, message: 'Dados incompletos para criar integração' };
}
try {
const axios = (await import('axios')).default;
const { GlobalFunctions } = await import('@/GlobalFunctions');
// Obtém o token JWT para autenticação
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'accept': 'application/json',
};
// Adiciona API key (obrigatória)
if (apiKey) {
headers['apikey'] = apiKey;
}
// Adiciona token JWT se disponível
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const headers = await this.n8nHeaders();
const response = await axios.post<AsanaIntegrationResponse>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/integracoes/asana`,
'https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/integracoes/asana',
request,
{ headers }
);
return response.data;
} catch (error: unknown) {
console.error('Erro ao criar integração do Asana:', error);
// Retorna o erro da API se existir
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: any; status?: number } };
if (axiosError.response?.data) {
const errorData = axiosError.response.data;
throw {
success: false,
message: errorData.message || 'Erro ao criar integração do Asana',
};
}
}
throw {
success: false,
message: error instanceof Error ? error.message : 'Erro ao criar integração do Asana',
};
this.handleAsanaError(error, 'Erro ao criar integração do Asana');
}
}
async updateIntegration(integracaoId: string, request: Omit<AsanaIntegrationRequest, 'user_id'>): Promise<AsanaIntegrationResponse> {
async updateIntegration(
integracaoId: string,
request: Omit<AsanaIntegrationRequest, 'user_id'>
): Promise<AsanaIntegrationResponse> {
if (!integracaoId || !request.api_key || !request.workspace_gid || !request.usuario_asana_gid) {
throw {
success: false,
message: 'Dados incompletos para atualizar integração',
};
throw { success: false, message: 'Dados incompletos para atualizar integração' };
}
try {
const axios = (await import('axios')).default;
const { GlobalFunctions } = await import('@/GlobalFunctions');
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'accept': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/c898beff-84cb-44df-a69c-6eff27ccd7aa/codex/agente-pessoal/integracoes/asana/${integracaoId}`;
const headers = await this.n8nHeaders();
const response = await axios.post<AsanaIntegrationResponse>(
url,
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/c898beff-84cb-44df-a69c-6eff27ccd7aa/codex/agente-pessoal/integracoes/asana/${integracaoId}`,
request,
{ headers }
);
return response.data;
} catch (error: unknown) {
console.error('Erro ao atualizar integração do Asana:', error);
// Retorna o erro da API se existir
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: any; status?: number } };
if (axiosError.response?.data) {
const errorData = axiosError.response.data;
throw {
success: false,
message: errorData.message || 'Erro ao atualizar integração do Asana',
};
}
}
throw {
success: false,
message: error instanceof Error ? error.message : 'Erro ao atualizar integração do Asana',
};
this.handleAsanaError(error, 'Erro ao atualizar integração do Asana');
}
}
}
// Exporta instância única (Singleton)
export const asanaService = new AsanaService();
+54
View File
@@ -0,0 +1,54 @@
import { apiService } from "./api";
const ASSISTANT_TIMEOUT_MS = 300_000;
export interface AssistantOutputResponse {
output?: string;
}
class AssistentePromptsService {
async gerar(instrucao: string, signal?: AbortSignal): Promise<string> {
if (!instrucao?.trim()) {
throw new Error("Instrução é obrigatória");
}
const response = await apiService.post<AssistantOutputResponse>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/assistente-criar-prompt",
{ instrucao: instrucao.trim() },
{ timeout: ASSISTANT_TIMEOUT_MS, signal }
);
if (response.data?.output != null) {
return String(response.data.output);
}
throw new Error("Resposta da API sem texto gerado (output)");
}
async refinar(promptAtual: string, instrucao: string, signal?: AbortSignal): Promise<string> {
if (!promptAtual?.trim()) {
throw new Error("Prompt atual é obrigatório");
}
if (!instrucao?.trim()) {
throw new Error("Instrução é obrigatória");
}
const response = await apiService.post<AssistantOutputResponse>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/assistente-melhorar-prompt",
{
instrucao: instrucao.trim(),
prompt_atual: promptAtual.trim(),
},
{ timeout: ASSISTANT_TIMEOUT_MS, signal }
);
if (response.data?.output != null) {
return String(response.data.output);
}
throw new Error("Resposta da API sem texto gerado (output)");
}
}
export const assistentePromptsService = new AssistentePromptsService();
+92 -267
View File
@@ -1,24 +1,15 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
/**
* Tipos de vozes disponíveis para geração de áudio
*/
export type VoiceType = 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer';
/**
* Interface para a resposta da API de geração de áudio
*/
export interface AudioGenerationResponse {
success: boolean;
audio_url: string; // URL do áudio gerado
audio_url: string;
audio_generation_id: string;
message: string; // Texto que foi convertido em áudio
message: string;
}
/**
* Interface para um áudio armazenado no banco de dados
*/
export interface AudioRecord {
id: string;
user_email: string;
@@ -33,9 +24,6 @@ export interface AudioRecord {
created_at: string;
}
/**
* Interface para os dados necessários para geração de áudio
*/
export interface AudioGenerationRequest {
message: string;
voice: VoiceType;
@@ -43,143 +31,80 @@ export interface AudioGenerationRequest {
estabelecimentoId?: number;
}
/**
* Informações sobre cada tipo de voz disponível
*/
export const VOICE_OPTIONS = {
alloy: {
label: "Alloy",
gender: "Masculina",
style: "Neutra, equilibrada, tom corporativo",
description: "Boa para tutoriais e comunicações institucionais."
label: 'Alloy',
gender: 'Masculina',
style: 'Neutra, equilibrada, tom corporativo',
description: 'Boa para tutoriais e comunicações institucionais.',
},
echo: {
label: "Echo",
gender: "Masculina",
style: "Forte e profissional, mais grave",
description: "Ideal para voz de autoridade ou locução firme."
label: 'Echo',
gender: 'Masculina',
style: 'Forte e profissional, mais grave',
description: 'Ideal para voz de autoridade ou locução firme.',
},
fable: {
label: "Fable",
gender: "Feminina",
style: "Narrativa, calorosa e envolvente",
description: "Ótima para storytelling e áudios empáticos."
label: 'Fable',
gender: 'Feminina',
style: 'Narrativa, calorosa e envolvente',
description: 'Ótima para storytelling e áudios empáticos.',
},
onyx: {
label: "Onyx",
gender: "Masculina",
style: "Grave, autoritária, impactante",
description: "Excelente para trailers, mensagens sérias ou institucionais."
label: 'Onyx',
gender: 'Masculina',
style: 'Grave, autoritária, impactante',
description: 'Excelente para trailers, mensagens sérias ou institucionais.',
},
nova: {
label: "Nova",
gender: "Feminina",
style: "Brilhante, animada, energética",
description: "Boa para vídeos curtos, marketing ou conteúdos leves."
label: 'Nova',
gender: 'Feminina',
style: 'Brilhante, animada, energética',
description: 'Boa para vídeos curtos, marketing ou conteúdos leves.',
},
shimmer: {
label: "Shimmer",
gender: "Feminina",
style: "Suave, otimista, clara",
description: "Boa para mensagens acolhedoras, explicações e IA conversacional."
}
label: 'Shimmer',
gender: 'Feminina',
style: 'Suave, otimista, clara',
description: 'Boa para mensagens acolhedoras, explicações e IA conversacional.',
},
} as const;
/**
* Serviço de geração de áudio (Text-to-Speech)
*/
type ApiErrorShape = { message?: string; status?: number };
class AudioGenerationService {
private readonly AUDIO_GENERATION_ENDPOINT = '/webhook/codex/gerar_audio';
private readonly GET_AUDIOS_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_gerar_audios';
private readonly DELETE_AUDIO_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_gerar_audio';
/**
* Gera um arquivo de áudio a partir de texto
*
* @param request - Dados da requisição (texto, voz, email, estabelecimento)
* @returns Promise com a resposta da API
*/
async generateAudio(request: AudioGenerationRequest): Promise<AudioGenerationResponse> {
const { message, voice, userEmail, estabelecimentoId } = request;
// Usa valores do .env se não forem fornecidos
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);//userEmail || import.meta.env.VITE_USER_EMAIL || '';
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);//estabelecimentoId || parseInt(import.meta.env.VITE_ESTABELECIMENTO_ID) || 1;
// Valida o texto
if (!message || message.trim().length === 0) {
throw {
success: false,
message: 'O texto não pode estar vazio',
};
}
// Valida a voz
if (!this.isValidVoice(voice)) {
throw {
success: false,
message: `Voz inválida. Opções disponíveis: ${Object.keys(VOICE_OPTIONS).join(', ')}`,
};
}
// Log para debug (remover em produção se necessário)
console.log('Gerando áudio:', {
messageLength: message.length,
voice,
userEmail: email,
estabelecimentoId: estabId,
});
try {
// Faz a requisição usando o serviço de API
const response = await apiService.post<AudioGenerationResponse>(
this.AUDIO_GENERATION_ENDPOINT,
{
estabelecimento_id: estabId,
user_email: email,
message: message,
voice: voice,
}
);
return response.data;
} catch (error: any) {
// Trata erros específicos
console.error('Erro na geração de áudio:', error);
throw {
success: false,
message: error.message || 'Erro ao gerar áudio',
status: error.status,
};
}
private resolveEmail(userEmail?: string): string {
return userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
}
private toApiError(error: unknown, fallback: string): never {
const e = error as ApiErrorShape;
throw { success: false, message: e?.message || fallback, status: e?.status };
}
private extractArray<T>(data: unknown, keys: string[]): T[] {
if (Array.isArray(data)) return data as T[];
if (data && typeof data === 'object') {
for (const key of keys) {
const candidate = (data as Record<string, unknown>)[key];
if (Array.isArray(candidate)) return candidate as T[];
}
}
return [];
}
/**
* Valida se a voz selecionada é suportada
*
* @param voice - Voz a ser validada
* @returns true se a voz é válida
*/
isValidVoice(voice: string): voice is VoiceType {
return Object.keys(VOICE_OPTIONS).includes(voice);
}
/**
* Obtém informações sobre uma voz específica
*
* @param voice - Tipo de voz
* @returns Informações da voz
*/
getVoiceInfo(voice: VoiceType) {
return VOICE_OPTIONS[voice];
}
/**
* Lista todas as vozes disponíveis
*
* @returns Array com todas as opções de voz
*/
getAllVoices() {
return Object.entries(VOICE_OPTIONS).map(([key, info]) => ({
value: key as VoiceType,
@@ -187,174 +112,74 @@ class AudioGenerationService {
}));
}
/**
* Valida o texto para geração de áudio
*
* @param text - Texto a ser validado
* @param maxLength - Comprimento máximo (padrão: 4096 caracteres)
* @returns Objeto com resultado da validação
*/
validateText(text: string, maxLength: number = 4096): { valid: boolean; error?: string } {
if (!text || text.trim().length === 0) {
return {
valid: false,
error: 'O texto não pode estar vazio',
};
}
if (text.length > maxLength) {
return {
valid: false,
error: `O texto é muito longo. Máximo: ${maxLength} caracteres`,
};
}
validateText(text: string, maxLength = 4096): { valid: boolean; error?: string } {
if (!text?.trim()) return { valid: false, error: 'O texto não pode estar vazio' };
if (text.length > maxLength) return { valid: false, error: `O texto é muito longo. Máximo: ${maxLength} caracteres` };
return { valid: true };
}
/**
* Lista áudios do banco de dados com paginação
*
* @param userEmail - Email do usuário
* @param page - Número da página (padrão: 1)
* @param perPage - Quantidade de itens por página (padrão: 10)
* @returns Promise com o array de áudios
*/
async getAudios(
userEmail?: string,
page: number = 1,
perPage: number = 10
): Promise<AudioRecord[]> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
async generateAudio(request: AudioGenerationRequest): Promise<AudioGenerationResponse> {
const { message, voice } = request;
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
if (!message?.trim()) {
throw { success: false, message: 'O texto não pode estar vazio' };
}
console.log('Buscando áudios:', {
userEmail: email,
page,
perPage,
});
if (!this.isValidVoice(voice)) {
throw { success: false, message: `Voz inválida. Opções disponíveis: ${Object.keys(VOICE_OPTIONS).join(', ')}` };
}
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
try {
// Faz a requisição GET com parâmetros na URL e query
const response = await apiService.get<AudioRecord[]>(
`${this.GET_AUDIOS_ENDPOINT}/${email}`,
{
params: {
page: page.toString(),
per_page: perPage.toString(),
},
}
const response = await apiService.post<AudioGenerationResponse>(
this.AUDIO_GENERATION_ENDPOINT,
{ estabelecimento_id: estabId, user_email: email, message, voice }
);
console.log('Resposta completa da API:', response);
console.log('response.data:', response.data);
console.log('É array?:', Array.isArray(response.data));
// A API retorna diretamente o array de áudios
// Garante que sempre retorna um array
if (Array.isArray(response.data)) {
return response.data;
} else if (response.data && typeof response.data === 'object') {
// Se a resposta for um objeto com uma propriedade 'audios' ou similar
console.warn('API retornou objeto em vez de array:', response.data);
// Tenta encontrar o array dentro do objeto
if (Array.isArray((response.data as any).audios)) {
return (response.data as any).audios;
} else if (Array.isArray((response.data as any).data)) {
return (response.data as any).data;
}
}
// Se não conseguir extrair array, retorna vazio
console.warn('Não foi possível extrair array de áudios da resposta');
return [];
} catch (error: any) {
console.error('Erro ao buscar áudios:', error);
throw {
success: false,
message: error.message || 'Erro ao buscar áudios',
status: error.status,
};
return response.data;
} catch (error: unknown) {
console.error('Erro na geração de áudio:', error);
this.toApiError(error, 'Erro ao gerar áudio');
}
}
/**
* Deleta um áudio do banco de dados
*
* @param audioId - ID do áudio a ser deletado
* @param userEmail - Email do usuário (opcional)
* @returns Promise com sucesso ou erro
*/
async deleteAudio(audioId: string, userEmail?: string): Promise<{ success: boolean; message?: string }> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
async getAudios(userEmail?: string, page = 1, perPage = 10): Promise<AudioRecord[]> {
const email = this.resolveEmail(userEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
throw { success: false, message: 'Email do usuário não fornecido' };
}
if (!audioId) {
throw {
success: false,
message: 'ID do áudio não fornecido',
};
}
console.log('Deletando áudio:', {
audioId,
userEmail: email,
});
try {
// Faz a requisição DELETE com parâmetros na URL
const response = await apiService.get<unknown>(
`${this.GET_AUDIOS_ENDPOINT}/${email}`,
{ params: { page: page.toString(), per_page: perPage.toString() } }
);
return this.extractArray<AudioRecord>(response.data, ['audios', 'data']);
} catch (error: unknown) {
console.error('Erro ao buscar áudios:', error);
this.toApiError(error, 'Erro ao buscar áudios');
}
}
async deleteAudio(audioId: string, userEmail?: string): Promise<{ success: boolean; message?: string }> {
const email = this.resolveEmail(userEmail);
if (!email) throw { success: false, message: 'Email do usuário não fornecido' };
if (!audioId) throw { success: false, message: 'ID do áudio não fornecido' };
try {
const response = await apiService.delete<{ success: boolean; message?: string } | Array<{ success: boolean; message?: string }>>(
`${this.DELETE_AUDIO_ENDPOINT}/${email}/${audioId}`
);
console.log('Resposta completa do DELETE:', response);
console.log('response.data:', response.data);
// A API retorna um array com um objeto: [{"success":true}]
let result: { success: boolean; message?: string };
if (Array.isArray(response.data)) {
// Se for array, pega o primeiro elemento
result = response.data[0];
console.log('API retornou array, usando primeiro elemento:', result);
} else {
// Se for objeto direto
result = response.data;
console.log('API retornou objeto direto:', result);
}
// Garante que tem a estrutura mínima
return {
success: result.success ?? true,
message: result.message || 'Áudio deletado com sucesso',
};
} catch (error: any) {
const result = Array.isArray(response.data) ? response.data[0] : response.data;
return { success: result.success ?? true, message: result.message || 'Áudio deletado com sucesso' };
} catch (error: unknown) {
console.error('Erro ao deletar áudio:', error);
throw {
success: false,
message: error.message || 'Erro ao deletar áudio',
status: error.status,
};
this.toApiError(error, 'Erro ao deletar áudio');
}
}
}
// Exporta instância única (Singleton)
export const audioGenerationService = new AudioGenerationService();
+236 -791
View File
File diff suppressed because it is too large Load Diff
+76 -295
View File
@@ -1,25 +1,16 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
/**
* Tamanhos de imagem disponíveis
*/
export type ImageSize = '1024x1024' | '1024x1792' | '1792x1024';
/**
* Interface para a resposta da API de geração de imagens
*/
export interface ImageGenerationResponse {
success: boolean;
image_url?: string; // URL da imagem gerada (opcional quando há erro)
image_generation_id?: string; // ID da geração (opcional quando há erro)
message: string; // Descrição original ou mensagem de erro
code?: string; // Código de erro (ex: "server_error", "invalid_request")
image_url?: string;
image_generation_id?: string;
message: string;
code?: string;
}
/**
* Interface para uma imagem armazenada no banco de dados
*/
export interface ImageRecord {
id: string;
user_email: string;
@@ -35,9 +26,6 @@ export interface ImageRecord {
created_at: string;
}
/**
* Interface para resposta paginada de imagens
*/
export interface GetImagesResponse {
images: ImageRecord[];
total: number;
@@ -46,9 +34,6 @@ export interface GetImagesResponse {
total_pages: number;
}
/**
* Interface para os dados necessários para geração de imagem
*/
export interface ImageGenerationRequest {
description: string;
size: ImageSize;
@@ -56,9 +41,6 @@ export interface ImageGenerationRequest {
estabelecimentoId?: number;
}
/**
* Informações sobre cada tamanho de imagem disponível
*/
export const IMAGE_SIZE_OPTIONS = {
'1024x1024': {
label: 'Quadrado',
@@ -80,127 +62,41 @@ export const IMAGE_SIZE_OPTIONS = {
},
} as const;
/**
* Serviço de geração de imagens com IA
*/
type ApiErrorShape = { message?: string; status?: number; success?: boolean; code?: string };
class ImageGenerationService {
private readonly IMAGE_GENERATION_ENDPOINT = '/webhook/codex/image_generator';
private readonly GET_IMAGES_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_images';
private readonly DELETE_IMAGE_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_images';
/**
* Gera uma imagem a partir de uma descrição em texto
*
* @param request - Dados da requisição (descrição, tamanho, email, estabelecimento)
* @returns Promise com a resposta da API
*/
async generateImage(request: ImageGenerationRequest): Promise<ImageGenerationResponse> {
const { description, size, userEmail, estabelecimentoId } = request;
// Usa valores do .env se não forem fornecidos
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);//userEmail || import.meta.env.VITE_USER_EMAIL || '';
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);//estabelecimentoId || parseInt(import.meta.env.VITE_ESTABELECIMENTO_ID) || 1;
// Valida a descrição
if (!description || description.trim().length === 0) {
throw {
success: false,
message: 'A descrição não pode estar vazia',
};
}
// Valida o tamanho
if (!this.isValidSize(size)) {
throw {
success: false,
message: `Tamanho inválido. Opções disponíveis: ${Object.keys(IMAGE_SIZE_OPTIONS).join(', ')}`,
};
}
// Log para debug (remover em produção se necessário)
console.log('Gerando imagem:', {
descriptionLength: description.length,
size,
userEmail: email,
estabelecimentoId: estabId,
});
try {
// Faz a requisição usando o serviço de API
// Nota: O campo no body é "estabelecito_id" (com typo na API)
const response = await apiService.post<ImageGenerationResponse>(
this.IMAGE_GENERATION_ENDPOINT,
{
estabelecimento_id: estabId, // Mantém o typo da API original
user_email: email,
description: description,
size: size,
}
);
// Verifica se a resposta indica erro
if (!response.data.success) {
throw {
success: false,
message: response.data.message || 'Erro ao gerar imagem',
code: response.data.code,
};
}
return response.data;
} catch (error: any) {
// Trata erros específicos da API
console.error('Erro na geração de imagem:', error);
// Se o erro já tem a estrutura esperada (veio da validação acima), repassa
if (error.success === false && error.message) {
throw error;
}
// Se o erro veio da requisição HTTP, tenta extrair a resposta da API
if (error.response?.data) {
const apiError = error.response.data;
throw {
success: false,
message: apiError.message || 'Erro ao gerar imagem',
code: apiError.code,
};
}
// Erro genérico
throw {
success: false,
message: error.message || 'Erro ao gerar imagem. Tente novamente.',
status: error.status,
};
}
private resolveEmail(userEmail?: string): string {
return userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
}
private toApiError(error: unknown, fallback: string): never {
const e = error as ApiErrorShape;
throw { success: false, message: e?.message || fallback, status: e?.status };
}
private extractArray<T>(data: unknown, keys: string[]): T[] {
if (Array.isArray(data)) return data as T[];
if (data && typeof data === 'object') {
for (const key of keys) {
const candidate = (data as Record<string, unknown>)[key];
if (Array.isArray(candidate)) return candidate as T[];
}
}
return [];
}
/**
* Valida se o tamanho selecionado é suportado
*
* @param size - Tamanho a ser validado
* @returns true se o tamanho é válido
*/
isValidSize(size: string): size is ImageSize {
return Object.keys(IMAGE_SIZE_OPTIONS).includes(size);
}
/**
* Obtém informações sobre um tamanho específico
*
* @param size - Tamanho da imagem
* @returns Informações do tamanho
*/
getSizeInfo(size: ImageSize) {
return IMAGE_SIZE_OPTIONS[size];
}
/**
* Lista todos os tamanhos disponíveis
*
* @returns Array com todas as opções de tamanho
*/
getAllSizes() {
return Object.entries(IMAGE_SIZE_OPTIONS).map(([key, info]) => ({
value: key as ImageSize,
@@ -208,213 +104,98 @@ class ImageGenerationService {
}));
}
/**
* Valida a descrição para geração de imagem
*
* @param description - Descrição a ser validada
* @param minLength - Comprimento mínimo (padrão: 3 caracteres)
* @param maxLength - Comprimento máximo (padrão: 1000 caracteres)
* @returns Objeto com resultado da validação
*/
validateDescription(
description: string,
minLength: number = 3,
maxLength: number = 1000
): { valid: boolean; error?: string } {
if (!description || description.trim().length === 0) {
return {
valid: false,
error: 'A descrição não pode estar vazia',
};
}
if (description.trim().length < minLength) {
return {
valid: false,
error: `A descrição deve ter pelo menos ${minLength} caracteres`,
};
}
if (description.length > maxLength) {
return {
valid: false,
error: `A descrição é muito longa. Máximo: ${maxLength} caracteres`,
};
}
validateDescription(description: string, minLength = 3, maxLength = 1000): { valid: boolean; error?: string } {
if (!description?.trim()) return { valid: false, error: 'A descrição não pode estar vazia' };
if (description.trim().length < minLength) return { valid: false, error: `A descrição deve ter pelo menos ${minLength} caracteres` };
if (description.length > maxLength) return { valid: false, error: `A descrição é muito longa. Máximo: ${maxLength} caracteres` };
return { valid: true };
}
/**
* Faz download de uma imagem gerada
*
* @param imageUrl - URL da imagem
* @param filename - Nome do arquivo (opcional)
*/
async generateImage(request: ImageGenerationRequest): Promise<ImageGenerationResponse> {
const { description, size } = request;
if (!description?.trim()) {
throw { success: false, message: 'A descrição não pode estar vazia' };
}
if (!this.isValidSize(size)) {
throw { success: false, message: `Tamanho inválido. Opções disponíveis: ${Object.keys(IMAGE_SIZE_OPTIONS).join(', ')}` };
}
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
try {
const response = await apiService.post<ImageGenerationResponse>(
this.IMAGE_GENERATION_ENDPOINT,
{ estabelecimento_id: estabId, user_email: email, description, size }
);
if (!response.data.success) {
throw { success: false, message: response.data.message || 'Erro ao gerar imagem', code: response.data.code };
}
return response.data;
} catch (error: unknown) {
const e = error as ApiErrorShape;
if (e?.success === false && e?.message) throw error;
console.error('Erro na geração de imagem:', error);
this.toApiError(error, 'Erro ao gerar imagem. Tente novamente.');
}
}
async downloadImage(imageUrl: string, filename?: string): Promise<void> {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename || `imagem_${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Erro ao baixar imagem:', error);
} catch {
throw new Error('Não foi possível baixar a imagem');
}
}
/**
* Lista imagens do banco de dados com paginação
*
* @param userEmail - Email do usuário
* @param page - Número da página (padrão: 1)
* @param perPage - Quantidade de itens por página (padrão: 10)
* @returns Promise com a resposta paginada
*/
async getImages(
userEmail?: string,
page: number = 1,
perPage: number = 10
): Promise<ImageRecord[]> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
async getImages(userEmail?: string, page = 1, perPage = 10): Promise<ImageRecord[]> {
const email = this.resolveEmail(userEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
throw { success: false, message: 'Email do usuário não fornecido' };
}
console.log('Buscando imagens:', {
userEmail: email,
page,
perPage,
});
try {
// Faz a requisição GET com parâmetros na URL e query
const response = await apiService.get<ImageRecord[]>(
const response = await apiService.get<unknown>(
`${this.GET_IMAGES_ENDPOINT}/${email}`,
{
params: {
page: page.toString(),
per_page: perPage.toString(),
},
}
{ params: { page: page.toString(), per_page: perPage.toString() } }
);
console.log('Resposta completa da API:', response);
console.log('response.data:', response.data);
console.log('É array?:', Array.isArray(response.data));
// A API retorna diretamente o array de imagens
// Garante que sempre retorna um array
if (Array.isArray(response.data)) {
return response.data;
} else if (response.data && typeof response.data === 'object') {
// Se a resposta for um objeto com uma propriedade 'images' ou similar
console.warn('API retornou objeto em vez de array:', response.data);
// Tenta encontrar o array dentro do objeto
if (Array.isArray((response.data as any).images)) {
return (response.data as any).images;
} else if (Array.isArray((response.data as any).data)) {
return (response.data as any).data;
}
}
// Se não conseguir extrair array, retorna vazio
console.warn('Não foi possível extrair array de imagens da resposta');
return [];
} catch (error: any) {
return this.extractArray<ImageRecord>(response.data, ['images', 'data']);
} catch (error: unknown) {
console.error('Erro ao buscar imagens:', error);
throw {
success: false,
message: error.message || 'Erro ao buscar imagens',
status: error.status,
};
this.toApiError(error, 'Erro ao buscar imagens');
}
}
/**
* Deleta uma imagem do banco de dados
*
* @param imageId - ID da imagem a ser deletada
* @param userEmail - Email do usuário (opcional)
* @returns Promise com sucesso ou erro
*/
async deleteImage(imageId: string, userEmail?: string): Promise<{ success: boolean; message?: string }> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
const email = this.resolveEmail(userEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
if (!imageId) {
throw {
success: false,
message: 'ID da imagem não fornecido',
};
}
console.log('Deletando imagem:', {
imageId,
userEmail: email,
});
if (!email) throw { success: false, message: 'Email do usuário não fornecido' };
if (!imageId) throw { success: false, message: 'ID da imagem não fornecido' };
try {
// Faz a requisição DELETE com parâmetros na URL
const response = await apiService.delete<{ success: boolean; message?: string } | Array<{ success: boolean; message?: string }>>(
`${this.DELETE_IMAGE_ENDPOINT}/${email}/${imageId}`
);
console.log('Resposta completa do DELETE:', response);
console.log('response.data:', response.data);
// A API pode retornar um array com um objeto: [{"success":true}]
// ou diretamente um objeto: {"success":true}
let result: { success: boolean; message?: string };
if (Array.isArray(response.data)) {
// Se for array, pega o primeiro elemento
result = response.data[0];
console.log('API retornou array, usando primeiro elemento:', result);
} else {
// Se for objeto direto
result = response.data;
console.log('API retornou objeto direto:', result);
}
// Garante que tem a estrutura mínima
return {
success: result.success ?? true,
message: result.message || 'Imagem deletada com sucesso',
};
} catch (error: any) {
const result = Array.isArray(response.data) ? response.data[0] : response.data;
return { success: result.success ?? true, message: result.message || 'Imagem deletada com sucesso' };
} catch (error: unknown) {
console.error('Erro ao deletar imagem:', error);
throw {
success: false,
message: error.message || 'Erro ao deletar imagem',
status: error.status,
};
this.toApiError(error, 'Erro ao deletar imagem');
}
}
}
// Exporta instância única (Singleton)
export const imageGenerationService = new ImageGenerationService();
+4 -4
View File
@@ -1,13 +1,13 @@
/**
* Exporta todos os serviços de API
*/
export { apiService } from './api';
export { transcriptionService } from './transcription';
export { audioGenerationService, VOICE_OPTIONS } from './audioGeneration';
export { imageGenerationService, IMAGE_SIZE_OPTIONS } from './imageGeneration';
export { personalAgent } from './personalAgent';
export { asanaService } from './asana';
export { areasService } from './areas';
export { parecerService } from './parecerApi';
export { promptsService } from './promptsApi';
export { assistentePromptsService } from './assistentePrompts';
export type { TranscriptionResponse, TranscriptionRequest } from './transcription';
export type { AudioGenerationResponse, AudioGenerationRequest, VoiceType } from './audioGeneration';
export type { ImageGenerationResponse, ImageGenerationRequest, ImageSize } from './imageGeneration';
+289
View File
@@ -0,0 +1,289 @@
import { apiService } from "./api";
import { GlobalFunctions, TransferAreaProperties } from "@/GlobalFunctions";
export interface GerarParecerParams {
titulo: string;
area_id: string;
prompt_id?: string;
prompt: string;
instrucao: string;
anexo?: File | null;
}
export interface GerarParecerSuccessResponse {
success: true;
id: string;
titulo: string;
conteudo_gerado: string;
status: string;
}
export interface GerarParecerErrorResponse {
success: false;
message?: string;
}
export type GerarParecerResponse = GerarParecerSuccessResponse | GerarParecerErrorResponse;
const PARECER_WEBHOOK_BASE = "https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer";
/** GET por id (detalhe / edição visualização) */
const PARECER_DETALHE_URL = (id: string) =>
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/parecer/${encodeURIComponent(id)}`;
const PARECER_CHAT_URL = (parecerId: string) =>
`${PARECER_DETALHE_URL(parecerId)}/chat`;
const PARECER_CHAT_ENVIAR_URL = (parecerId: string) =>
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/1ce1f771-fe6c-4905-9dbb-a69826d72632/codex/parecer/chat/${encodeURIComponent(parecerId)}`;
const PARECER_CHAT_MSG_HISTORICO_URL = (messageId: string) =>
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/parecer/chat/${encodeURIComponent(messageId)}/historico`;
export interface ParecerChatHistoricoItem {
id: string;
parecer_id: string;
role: string;
parecer_anterior: string;
parecer_atual: string;
created_at: string;
}
export interface ParecerChatMessage {
id: string;
parecer_id: string;
user_email: string;
role: "user" | "assistant";
content: string;
created_at: string;
}
export interface ParecerDetalhe {
id: string;
estabelecimento_id: number;
user_email: string;
titulo: string;
area_id: string;
prompt_id: string;
prompt_conteudo: string;
instrucao: string;
conteudo_gerado: string;
anexo_url: string | null;
anexo_nome: string | null;
status: string;
created_at: string;
updated_at: string;
criado_por: string | null;
atualizado_por: string | null;
}
export type ParecerStatus = "processando" | "concluido" | "erro";
export interface ParecerListItem {
id: string;
titulo: string;
prompt_id: string;
area_id: string;
status: string;
area_nome: string;
prompt_nome: string;
criado_por: string | null;
created_at: string;
updated_at: string;
}
export interface ListarParecerParams {
titulo?: string;
page?: number;
per_page?: number;
area_id?: string;
status?: ParecerStatus;
}
export interface ListarParecerResponse {
success: boolean;
total_registros: number;
total_paginas: number;
per_page: number;
pagina_atual: number;
data: ParecerListItem[];
}
class ParecerService {
async gerar(params: GerarParecerParams): Promise<GerarParecerSuccessResponse> {
if (!params.titulo?.trim()) {
throw new Error("Título é obrigatório");
}
if (!params.area_id?.trim()) {
throw new Error("Área é obrigatória");
}
if (!params.prompt?.trim()) {
throw new Error("Prompt é obrigatório");
}
if (!params.instrucao?.trim()) {
throw new Error("Instrução é obrigatória");
}
const userEmail = GlobalFunctions.getTransferProperty(
TransferAreaProperties.UsuarioEmail
);
const estabelecimentoId = GlobalFunctions.getTransferProperty(
TransferAreaProperties.EstabelecimentoCodigo
);
if (!userEmail) {
throw new Error("Usuário não identificado (user_email)");
}
if (!estabelecimentoId) {
throw new Error("Estabelecimento não identificado (estabelecimento_id)");
}
const form = new FormData();
form.append("user_email", String(userEmail));
form.append("estabelecimento_id", String(estabelecimentoId));
form.append("titulo", params.titulo.trim());
form.append("area_id", params.area_id.trim());
if (params.prompt_id?.trim()) {
form.append("prompt_id", params.prompt_id.trim());
}
form.append("prompt", params.prompt.trim());
form.append("instrucao", (params.instrucao ?? "").trim());
if (params.anexo) {
form.append("anexo", params.anexo);
}
// Sem limite de tempo: a geração pode levar vários minutos até o webhook responder.
const response = await apiService.postFormData<GerarParecerResponse>(
`${PARECER_WEBHOOK_BASE}/gerar`,
form,
{ timeout: 0 }
);
if (response.data.success === false) {
throw new Error(
(response.data as GerarParecerErrorResponse).message ?? "Erro ao gerar parecer"
);
}
const data = response.data as GerarParecerSuccessResponse;
if (data.status !== "concluido") {
throw new Error(
"A geração do parecer ainda não foi concluída. Tente novamente em instantes."
);
}
return data;
}
async listar(params: ListarParecerParams = {}): Promise<ListarParecerResponse> {
const query: Record<string, string | number> = {};
if (params.titulo?.trim()) query.titulo = params.titulo.trim();
if (params.page != null) query.page = params.page;
if (params.per_page != null) query.per_page = params.per_page;
if (params.area_id?.trim()) query.area_id = params.area_id.trim();
if (params.status?.trim()) query.status = params.status;
const response = await apiService.get<ListarParecerResponse[] | ListarParecerResponse>(
`${PARECER_WEBHOOK_BASE}/listar`,
{ params: query }
);
// API retorna um array cujo primeiro elemento é o objeto { success, total_registros, data, ... }
const raw = response.data;
const payload = Array.isArray(raw) ? raw[0] : raw;
if (!payload || !(payload as ListarParecerResponse).success) {
throw new Error("Erro ao listar pareceres");
}
return payload as ListarParecerResponse;
}
async buscarPorId(id: string): Promise<ParecerDetalhe> {
if (!id?.trim()) {
throw new Error("ID do parecer é obrigatório");
}
const response = await apiService.get<ParecerDetalhe[] | ParecerDetalhe>(PARECER_DETALHE_URL(id));
const raw = response.data;
const item = Array.isArray(raw) ? raw[0] : raw;
if (!item || typeof item !== "object" || !("id" in item)) {
throw new Error("Parecer não encontrado");
}
return item as ParecerDetalhe;
}
async enviarMensagemChat(parecerId: string, message: string): Promise<{ conteudo_gerado: string }> {
const userEmail = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!userEmail) {
throw new Error("Usuário não identificado (user_email)");
}
if (!parecerId?.trim()) {
throw new Error("ID do parecer é obrigatório");
}
if (!message?.trim()) {
throw new Error("Digite uma mensagem");
}
type ChatPostPayload = { success: boolean; conteudo_gerado?: string; message?: string };
const response = await apiService.post<ChatPostPayload[] | ChatPostPayload>(
PARECER_CHAT_ENVIAR_URL(parecerId),
{ user_email: String(userEmail), message: message.trim() },
{ timeout: 0, headers: { "Content-Type": "application/json" } }
);
const raw = response.data;
const payload = Array.isArray(raw) ? raw[0] : raw;
if (!payload?.success || typeof payload.conteudo_gerado !== "string") {
throw new Error(payload?.message ?? "Erro ao processar a mensagem do chat");
}
return { conteudo_gerado: payload.conteudo_gerado };
}
async listarChat(parecerId: string): Promise<ParecerChatMessage[]> {
if (!parecerId?.trim()) {
return [];
}
const response = await apiService.get<
Array<{ success: boolean; data?: ParecerChatMessage[] }> | { success: boolean; data?: ParecerChatMessage[] }
>(PARECER_CHAT_URL(parecerId));
const raw = response.data;
const payload = Array.isArray(raw) ? raw[0] : raw;
if (!payload?.success || !Array.isArray(payload.data)) {
return [];
}
return payload.data;
}
async buscarHistoricoChatMensagem(messageId: string): Promise<ParecerChatHistoricoItem> {
if (!messageId?.trim()) {
throw new Error("ID da mensagem é obrigatório");
}
const response = await apiService.get<
Array<{ success: boolean; data?: ParecerChatHistoricoItem[] }> | { success: boolean; data?: ParecerChatHistoricoItem[] }
>(PARECER_CHAT_MSG_HISTORICO_URL(messageId));
const raw = response.data;
const payload = Array.isArray(raw) ? raw[0] : raw;
const item = payload?.data?.[0];
if (!payload?.success || !item) {
throw new Error("Não foi possível carregar o histórico desta resposta.");
}
return item;
}
async excluir(id: string): Promise<{ message: string }> {
if (!id?.trim()) {
throw new Error("ID do parecer é obrigatório");
}
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/4e6c2374-9c22-4c81-b558-45f0cfefa5c3/codex/parecer/deletar/${encodeURIComponent(id)}`;
const response = await apiService.delete<{ success: boolean; message?: string }>(url);
const body = response.data;
if (!body?.success) {
throw new Error(body?.message ?? "Erro ao excluir parecer");
}
return { message: body.message ?? "Parecer excluído com sucesso." };
}
}
export const parecerService = new ParecerService();
+79 -285
View File
@@ -1,4 +1,6 @@
import axios from 'axios';
import { GlobalFunctions } from '@/GlobalFunctions';
export interface UserProfile {
success: boolean;
id?: string;
@@ -37,7 +39,7 @@ export interface ExpenseItem {
id: string;
data_hora: string;
descricao: string;
tipo: "pessoal" | "corporativo";
tipo: 'pessoal' | 'corporativo';
valor: string;
categoria_id: number;
categoria_nome: string;
@@ -71,273 +73,126 @@ export interface ExpenseCategory {
atualizado_em: string;
}
type AxiosLikeError = { response?: { data?: unknown; status?: number } };
class PersonalAgent {
private async buildHeaders(): Promise<Record<string, string>> {
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (apiKey) headers['apikey'] = apiKey;
if (token) headers['Authorization'] = `Bearer ${token}`;
return headers;
}
private resolveEmail(userEmail?: string): string {
return userEmail || GlobalFunctions.getUsuarioLogado().email;
}
private handleError(error: unknown, fallback: string): never {
const e = error as AxiosLikeError;
if (e?.response?.data) throw e.response.data;
throw {
success: false,
message: error instanceof Error ? error.message : fallback,
status: e?.response?.status,
};
}
async getUserProfile(userEmail?: string): Promise<UserProfile> {
const email = userEmail || GlobalFunctions.getUsuarioLogado().email;
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
const email = this.resolveEmail(userEmail);
if (!email) throw { success: false, message: 'Email do usuário não fornecido' };
try {
const axios = (await import('axios')).default;
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const headers = await this.buildHeaders();
const response = await axios.get<UserProfile>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/user/${email}`,
{ headers }
);
return response.data;
} catch (error: any) {
if (error.response?.data) {
return error.response.data;
}
throw {
success: false,
message: error.message || 'Erro ao buscar perfil do usuário',
status: error.response?.status,
};
} catch (error: unknown) {
const e = error as AxiosLikeError;
if (e?.response?.data) return e.response.data as UserProfile;
throw { success: false, message: error instanceof Error ? error.message : 'Erro ao buscar perfil do usuário', status: e?.response?.status };
}
}
async createUser(request: CreateUserRequest): Promise<UserProfile> {
if (!request.nome || request.nome.trim().length === 0) {
throw {
success: false,
message: 'Nome é obrigatório',
};
}
if (!request.email || request.email.trim().length === 0) {
throw {
success: false,
message: 'Email é obrigatório',
};
}
if (!request.whatsapp || request.whatsapp.trim().length === 0) {
throw {
success: false,
message: 'WhatsApp é obrigatório',
};
}
const whatsappNumbers = request.whatsapp.replace(/\D/g, '');
if (!request.nome?.trim()) throw { success: false, message: 'Nome é obrigatório' };
if (!request.email?.trim()) throw { success: false, message: 'Email é obrigatório' };
if (!request.whatsapp?.trim()) throw { success: false, message: 'WhatsApp é obrigatório' };
try {
const axios = (await import('axios')).default;
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
} else {
console.warn('API_KEY não configurada');
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const headers = await this.buildHeaders();
const response = await axios.post<UserProfile>(
'https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/user',
{
nome: request.nome.trim(),
email: request.email.trim(),
whatsapp: whatsappNumbers,
whatsapp: request.whatsapp.replace(/\D/g, ''),
followup: request.followup,
},
{ headers }
);
return response.data;
} catch (error: any) {
if (error.response?.data) {
throw error.response.data;
}
throw {
success: false,
message: error.message || 'Erro ao criar usuário',
status: error.response?.status,
};
} catch (error: unknown) {
this.handleError(error, 'Erro ao criar usuário');
}
}
async updateUser(userId: string, request: UpdateUserRequest): Promise<UserProfile> {
if (!userId || userId.trim().length === 0) {
throw {
success: false,
message: 'ID do usuário é obrigatório',
};
}
if (!request.nome || request.nome.trim().length === 0) {
throw {
success: false,
message: 'Nome é obrigatório',
};
}
if (!request.whatsapp || request.whatsapp.trim().length === 0) {
throw {
success: false,
message: 'WhatsApp é obrigatório',
};
}
const whatsappNumbers = request.whatsapp.replace(/\D/g, '');
if (!userId?.trim()) throw { success: false, message: 'ID do usuário é obrigatório' };
if (!request.nome?.trim()) throw { success: false, message: 'Nome é obrigatório' };
if (!request.whatsapp?.trim()) throw { success: false, message: 'WhatsApp é obrigatório' };
try {
const axios = (await import('axios')).default;
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const headers = await this.buildHeaders();
const response = await axios.post<UserProfile>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/user/${userId}`,
{
nome: request.nome.trim(),
whatsapp: whatsappNumbers,
whatsapp: request.whatsapp.replace(/\D/g, ''),
followup: request.followup,
},
{ headers }
);
return response.data;
} catch (error: any) {
if (error.response?.data) {
throw error.response.data;
}
throw {
success: false,
message: error.message || 'Erro ao atualizar usuário',
status: error.response?.status,
};
} catch (error: unknown) {
this.handleError(error, 'Erro ao atualizar usuário');
}
}
async getFinancialIndicators(userEmail?: string): Promise<FinancialIndicators> {
const email = userEmail || GlobalFunctions.getUsuarioLogado().email;
if (!email) {
throw {
success: false,
message: 'Email do usuário é obrigatório',
};
}
const email = this.resolveEmail(userEmail);
if (!email) throw { success: false, message: 'Email do usuário é obrigatório' };
try {
const axios = (await import('axios')).default;
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const headers = await this.buildHeaders();
const response = await axios.get<FinancialIndicators>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/financas/indicadores/${email}`,
{ headers }
);
return response.data;
} catch (error: unknown) {
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: FinancialIndicators } };
if (axiosError.response?.data) {
throw axiosError.response.data;
}
}
throw {
success: false,
message: error instanceof Error ? error.message : 'Erro ao buscar indicadores financeiros',
status: error && typeof error === 'object' && 'response' in error
? (error as { response?: { status?: number } }).response?.status
: undefined,
};
this.handleError(error, 'Erro ao buscar indicadores financeiros');
}
}
async getExpenses(userEmail?: string, filters?: ExpensesFilters): Promise<ExpensesResponse> {
const email = userEmail || GlobalFunctions.getUsuarioLogado().email;
const email = this.resolveEmail(userEmail);
if (!email) throw { success: false, message: 'Email do usuário é obrigatório' };
if (!email) {
throw {
success: false,
message: 'Email do usuário é obrigatório',
};
}
const empty: ExpensesResponse = {
success: true,
total_registros: 0,
total_paginas: 0,
per_page: filters?.per_page || 10,
pagina_atual: filters?.page || 1,
data: [],
};
try {
const axios = (await import('axios')).default;
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Constrói query params
const headers = await this.buildHeaders();
const params = new URLSearchParams();
if (filters?.page) params.append('page', filters.page.toString());
if (filters?.per_page) params.append('per_page', filters.per_page.toString());
@@ -346,96 +201,35 @@ class PersonalAgent {
if (filters?.data_inicial) params.append('data_inicial', filters.data_inicial);
if (filters?.data_final) params.append('data_final', filters.data_final);
const queryString = params.toString();
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/financas/${email}${queryString ? `?${queryString}` : ''}`;
const qs = params.toString();
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/financas/${email}${qs ? `?${qs}` : ''}`;
const response = await axios.get<ExpensesResponse[]>(url, { headers });
if (Array.isArray(response.data) && response.data.length > 0) {
const expensesResponse = response.data[0];
if (!Array.isArray(response.data) || response.data.length === 0) return empty;
// Valida e limpa o array de dados, removendo objetos vazios
if (expensesResponse.data && Array.isArray(expensesResponse.data)) {
// Filtra objetos vazios (sem propriedades ou apenas com propriedades vazias)
expensesResponse.data = expensesResponse.data.filter((item) => {
// Verifica se o objeto tem pelo menos uma propriedade válida
return item && typeof item === 'object' && Object.keys(item).length > 0 && item.id;
});
// Se após filtrar não há dados, garante que data seja um array vazio
if (expensesResponse.data.length === 0) {
expensesResponse.data = [];
expensesResponse.total_registros = 0;
expensesResponse.total_paginas = 0;
}
} else {
// Se data não é um array válido, inicializa como array vazio
expensesResponse.data = [];
expensesResponse.total_registros = 0;
expensesResponse.total_paginas = 0;
}
return expensesResponse;
const result = response.data[0];
result.data = Array.isArray(result.data) ? result.data.filter((item) => item && item.id) : [];
if (result.data.length === 0) {
result.total_registros = 0;
result.total_paginas = 0;
}
// Fallback caso a estrutura seja diferente - retorna resposta vazia
return {
success: true,
total_registros: 0,
total_paginas: 0,
per_page: filters?.per_page || 10,
pagina_atual: filters?.page || 1,
data: [],
};
return result;
} catch (error: unknown) {
// Retorna o erro da API se existir
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as { response?: { data?: ExpensesResponse } };
if (axiosError.response?.data) {
throw axiosError.response.data;
}
}
throw {
success: false,
message: error instanceof Error ? error.message : 'Erro ao buscar despesas',
status: error && typeof error === 'object' && 'response' in error
? (error as { response?: { status?: number } }).response?.status
: undefined,
};
this.handleError(error, 'Erro ao buscar despesas');
}
}
async getCategories(): Promise<ExpenseCategory[]> {
try {
const axios = (await import('axios')).default;
const token = await GlobalFunctions.getToken();
const apiKey = import.meta.env.VITE_API_KEY || '';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (apiKey) {
headers['apikey'] = apiKey;
}
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await axios.get<ExpenseCategory[]>('https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/categorias', { headers });
// A API retorna um array de categorias
if (Array.isArray(response.data)) {
return response.data;
}
return [];
const headers = await this.buildHeaders();
const response = await axios.get<ExpenseCategory[]>(
'https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/categorias',
{ headers }
);
return Array.isArray(response.data) ? response.data : [];
} catch (error: unknown) {
console.error('Erro ao buscar categorias:', error);
return [];
}
}
+176
View File
@@ -0,0 +1,176 @@
import { apiService } from "./api";
export interface PromptItem {
id: string;
titulo: string;
descricao: string;
conteudo: string;
area_id: string;
area_nome: string;
created_at: string;
updated_at: string;
}
export interface ListarPromptsParams {
titulo?: string;
area_id?: string;
page: number;
per_page: number;
}
export interface ListarPromptsResponseBody {
success: true;
total_registros: number;
total_paginas: number;
per_page: number;
pagina_atual: number;
data: PromptItem[];
}
export interface ListarPromptsPorAreaResponse {
success: boolean;
quantidade?: number;
data: PromptItem[];
}
export interface CriarPromptBody {
titulo: string;
descricao: string;
area_id: string;
conteudo: string;
}
export interface CriarPromptSuccessResponse {
success: true;
id: string;
titulo: string;
descricao: string;
conteudo: string;
}
export interface CriarPromptErrorResponse {
success: false;
missing_fields?: string[];
}
export interface EditarPromptBody {
titulo: string;
descricao: string;
area_id: string;
conteudo: string;
}
export interface EditarPromptSuccessResponse {
success: true;
id: string;
titulo: string;
descricao: string;
area_id: string;
conteudo: string;
}
export interface EditarPromptErrorResponse {
success: false;
missing_fields?: string[];
}
class PromptsService {
async listar(params: ListarPromptsParams): Promise<ListarPromptsResponseBody> {
const { page, per_page, titulo, area_id } = params;
const response = await apiService.get<ListarPromptsResponseBody[]>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/prompts",
{
params: {
...(titulo != null && titulo.trim() !== "" ? { titulo: titulo.trim() } : {}),
...(area_id != null && area_id.trim() !== "" ? { area_id: area_id.trim() } : {}),
page,
per_page,
},
}
);
const first = Array.isArray(response.data) ? response.data[0] : response.data;
if (!first || first.success !== true) {
throw new Error("Resposta inválida ao listar prompts");
}
return first;
}
async listarPorArea(areaId: string): Promise<ListarPromptsPorAreaResponse> {
if (!areaId?.trim()) {
return { success: true, quantidade: 0, data: [] };
}
const areaIdEnc = encodeURIComponent(areaId.trim());
const response = await apiService.get<ListarPromptsPorAreaResponse | ListarPromptsPorAreaResponse[]>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/prompts/listar-todos-area?area_id=${areaIdEnc}`
);
const first = Array.isArray(response.data) ? response.data[0] : response.data;
if (!first || first.success !== true) {
throw new Error("Resposta inválida ao listar prompts da área");
}
const list = first.data ?? [];
return { success: true, quantidade: first.quantidade ?? list.length, data: list };
}
async criar(body: CriarPromptBody): Promise<CriarPromptSuccessResponse> {
if (!body.titulo?.trim() || !body.area_id?.trim() || !body.conteudo?.trim()) {
throw new Error("Título, área e conteúdo são obrigatórios");
}
const response = await apiService.post<CriarPromptSuccessResponse | CriarPromptErrorResponse>(
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/prompt/criar",
{
titulo: body.titulo.trim(),
descricao: (body.descricao ?? "").trim(),
area_id: body.area_id.trim(),
conteudo: body.conteudo.trim(),
}
);
if (response.data.success === false) {
const err = response.data as CriarPromptErrorResponse;
const msg = err.missing_fields?.length
? `Preencha: ${err.missing_fields.join(", ")}`
: "Erro ao criar prompt.";
throw new Error(msg);
}
return response.data as CriarPromptSuccessResponse;
}
async editar(id: string, body: EditarPromptBody): Promise<EditarPromptSuccessResponse> {
if (!id?.trim()) {
throw new Error("ID do prompt é obrigatório");
}
const response = await apiService.put<EditarPromptSuccessResponse | EditarPromptErrorResponse>(
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/parecer/prompt/editar/${id}`,
{
titulo: (body.titulo ?? "").trim(),
descricao: (body.descricao ?? "").trim(),
area_id: body.area_id.trim(),
conteudo: (body.conteudo ?? "").trim(),
}
);
if (response.data.success === false) {
const err = response.data as EditarPromptErrorResponse;
const msg = err.missing_fields?.length
? `Preencha: ${err.missing_fields.join(", ")}`
: "Erro ao editar prompt.";
throw new Error(msg);
}
return response.data as EditarPromptSuccessResponse;
}
}
export const promptsService = new PromptsService();
+58 -202
View File
@@ -1,9 +1,6 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
/**
* Interface para a resposta da API de transcrição
*/
export interface TranscriptionResponse {
success: boolean;
transcription_id: string;
@@ -11,9 +8,6 @@ export interface TranscriptionResponse {
message: string;
}
/**
* Interface para uma transcrição armazenada no banco de dados
*/
export interface TranscriptionRecord {
id: string;
user_email: string;
@@ -27,246 +21,108 @@ export interface TranscriptionRecord {
created_at: string;
}
/**
* Interface para os dados necessários para transcrição
*/
export interface TranscriptionRequest {
audioFile: File;
userEmail?: string;
estabelecimentoId?: number;
}
/**
* Serviço de transcrição de áudio
*/
type ApiErrorShape = { message?: string; status?: number };
class TranscriptionService {
private readonly TRANSCRIPTION_ENDPOINT = '/webhook/codex/transcrever_audio';
private readonly GET_TRANSCRIPTIONS_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_transcrever_audio';
private readonly DELETE_TRANSCRIPTION_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_transcrever_audio';
/**
* Transcreve um arquivo de áudio
*
* @param request - Dados da requisição (arquivo, email, estabelecimento)
* @returns Promise com a resposta da API
*/
async transcribeAudio(request: TranscriptionRequest): Promise<TranscriptionResponse> {
const { audioFile, userEmail, estabelecimentoId } = request;
private readonly SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'];
private resolveEmail(userEmail?: string): string {
return userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
}
private toApiError(error: unknown, fallback: string): never {
const e = error as ApiErrorShape;
throw { success: false, message: e?.message || fallback, status: e?.status };
}
private extractArray<T>(data: unknown, keys: string[]): T[] {
if (Array.isArray(data)) return data as T[];
if (data && typeof data === 'object') {
for (const key of keys) {
const candidate = (data as Record<string, unknown>)[key];
if (Array.isArray(candidate)) return candidate as T[];
}
}
return [];
}
validateAudioFile(file: File, maxSizeMB = 25): { valid: boolean; error?: string } {
const maxBytes = maxSizeMB * 1024 * 1024;
if (file.size > maxBytes) {
return { valid: false, error: `Arquivo muito grande. Tamanho máximo: ${maxSizeMB}MB` };
}
const ext = file.name.split('.').pop()?.toLowerCase();
if (!ext || !this.SUPPORTED_FORMATS.includes(ext)) {
return { valid: false, error: `Formato não suportado. Formatos aceitos: ${this.SUPPORTED_FORMATS.join(', ')}` };
}
return { valid: true };
}
async transcribeAudio(request: TranscriptionRequest): Promise<TranscriptionResponse> {
const { audioFile } = request;
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
// Cria FormData para envio multipart
const formData = new FormData();
formData.append('data', audioFile);
// Usa valores do .env se não forem fornecidos
const email = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);//userEmail || import.meta.env.VITE_USER_EMAIL || '';
const estabId = GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);//estabelecimentoId || import.meta.env.VITE_ESTABELECIMENTO_ID || '';
formData.append('user_email', email);
formData.append('estabelecimento_id', estabId.toString());
// Log para debug (remover em produção se necessário)
console.log('Enviando transcrição:', {
fileName: audioFile.name,
fileSize: audioFile.size,
fileType: audioFile.type,
userEmail: email,
estabelecimentoId: estabId,
});
try {
// Faz a requisição usando o serviço de API
const response = await apiService.postFormData<TranscriptionResponse>(
this.TRANSCRIPTION_ENDPOINT,
formData
);
return response.data;
} catch (error: any) {
// Trata erros específicos
} catch (error: unknown) {
console.error('Erro na transcrição:', error);
throw {
success: false,
message: error.message || 'Erro ao transcrever áudio',
status: error.status,
};
this.toApiError(error, 'Erro ao transcrever áudio');
}
}
/**
* Valida se o arquivo de áudio é suportado
*
* @param file - Arquivo a ser validado
* @param maxSizeMB - Tamanho máximo em MB (padrão: 25MB)
* @returns Objeto com resultado da validação
*/
validateAudioFile(file: File, maxSizeMB: number = 25): { valid: boolean; error?: string } {
const SUPPORTED_FORMATS = ['flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'mpga', 'oga', 'ogg', 'wav', 'webm'];
const MAX_FILE_SIZE = maxSizeMB * 1024 * 1024;
// Valida tamanho
if (file.size > MAX_FILE_SIZE) {
return {
valid: false,
error: `Arquivo muito grande. Tamanho máximo: ${maxSizeMB}MB`,
};
}
// Valida formato
const fileExtension = file.name.split('.').pop()?.toLowerCase();
if (!fileExtension || !SUPPORTED_FORMATS.includes(fileExtension)) {
return {
valid: false,
error: `Formato não suportado. Formatos aceitos: ${SUPPORTED_FORMATS.join(', ')}`,
};
}
return { valid: true };
}
/**
* Lista transcrições do banco de dados com paginação
*
* @param userEmail - Email do usuário
* @param page - Número da página (padrão: 1)
* @param perPage - Quantidade de itens por página (padrão: 10)
* @returns Promise com o array de transcrições
*/
async getTranscriptions(
userEmail?: string,
page: number = 1,
perPage: number = 10
): Promise<TranscriptionRecord[]> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
console.log('Buscando transcrições:', {
userEmail: email,
page,
perPage,
});
async getTranscriptions(userEmail?: string, page = 1, perPage = 10): Promise<TranscriptionRecord[]> {
const email = this.resolveEmail(userEmail);
if (!email) throw { success: false, message: 'Email do usuário não fornecido' };
try {
// Faz a requisição GET com parâmetros na URL e query
const response = await apiService.get<TranscriptionRecord[]>(
const response = await apiService.get<unknown>(
`${this.GET_TRANSCRIPTIONS_ENDPOINT}/${email}`,
{
params: {
page: page.toString(),
per_page: perPage.toString(),
},
}
{ params: { page: page.toString(), per_page: perPage.toString() } }
);
console.log('Resposta completa da API:', response);
console.log('response.data:', response.data);
console.log('É array?:', Array.isArray(response.data));
// A API retorna diretamente o array de transcrições
// Garante que sempre retorna um array
if (Array.isArray(response.data)) {
return response.data;
} else if (response.data && typeof response.data === 'object') {
// Se a resposta for um objeto com uma propriedade 'transcriptions' ou similar
console.warn('API retornou objeto em vez de array:', response.data);
// Tenta encontrar o array dentro do objeto
if (Array.isArray((response.data as any).transcriptions)) {
return (response.data as any).transcriptions;
} else if (Array.isArray((response.data as any).data)) {
return (response.data as any).data;
}
}
// Se não conseguir extrair array, retorna vazio
console.warn('Não foi possível extrair array de transcrições da resposta');
return [];
} catch (error: any) {
return this.extractArray<TranscriptionRecord>(response.data, ['transcriptions', 'data']);
} catch (error: unknown) {
console.error('Erro ao buscar transcrições:', error);
throw {
success: false,
message: error.message || 'Erro ao buscar transcrições',
status: error.status,
};
this.toApiError(error, 'Erro ao buscar transcrições');
}
}
/**
* Deleta uma transcrição do banco de dados
*
* @param transcriptionId - ID da transcrição a ser deletada
* @param userEmail - Email do usuário (opcional)
* @returns Promise com sucesso ou erro
*/
async deleteTranscription(transcriptionId: string, userEmail?: string): Promise<{ success: boolean; message?: string }> {
// Usa valores do GlobalFunctions se não forem fornecidos
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!email) {
throw {
success: false,
message: 'Email do usuário não fornecido',
};
}
if (!transcriptionId) {
throw {
success: false,
message: 'ID da transcrição não fornecido',
};
}
console.log('Deletando transcrição:', {
transcriptionId,
userEmail: email,
});
const email = this.resolveEmail(userEmail);
if (!email) throw { success: false, message: 'Email do usuário não fornecido' };
if (!transcriptionId) throw { success: false, message: 'ID da transcrição não fornecido' };
try {
// Faz a requisição DELETE com parâmetros na URL
const response = await apiService.delete<{ success: boolean; message?: string } | Array<{ success: boolean; message?: string }>>(
`${this.DELETE_TRANSCRIPTION_ENDPOINT}/${email}/${transcriptionId}`
);
console.log('Resposta completa do DELETE:', response);
console.log('response.data:', response.data);
// A API retorna um array com um objeto: [{"success":true}]
let result: { success: boolean; message?: string };
if (Array.isArray(response.data)) {
// Se for array, pega o primeiro elemento
result = response.data[0];
console.log('API retornou array, usando primeiro elemento:', result);
} else {
// Se for objeto direto
result = response.data;
console.log('API retornou objeto direto:', result);
}
// Garante que tem a estrutura mínima
return {
success: result.success ?? true,
message: result.message || 'Transcrição deletada com sucesso',
};
} catch (error: any) {
const result = Array.isArray(response.data) ? response.data[0] : response.data;
return { success: result.success ?? true, message: result.message || 'Transcrição deletada com sucesso' };
} catch (error: unknown) {
console.error('Erro ao deletar transcrição:', error);
throw {
success: false,
message: error.message || 'Erro ao deletar transcrição',
status: error.status,
};
this.toApiError(error, 'Erro ao deletar transcrição');
}
}
}
// Exporta instância única (Singleton)
export const transcriptionService = new TranscriptionService();
-13
View File
@@ -1,27 +1,14 @@
/**
* Tipos compartilhados para as APIs
*/
/**
* Resposta padrão de sucesso/erro da API
*/
export interface ApiResponse<T = any> {
success: boolean;
message?: string;
data?: T;
}
/**
* Configuração de usuário para requisições
*/
export interface UserConfig {
userEmail?: string;
estabelecimentoId?: number;
}
/**
* Resposta de erro da API
*/
export interface ApiError {
success: false;
message: string;