Nova versao codex Parecer, atualizacao completa
This commit is contained in:
+84
-262
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+76
-295
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user