import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions'; import { apiService } from './api'; /** * Interface para a resposta da API de chat */ export interface ChatResponse { success: boolean; response: string; // Mensagem da IA chat_id: string; // ID do chat retornado pela API (importante para manter contexto) } /** * Interface para os dados necessários para enviar mensagem */ export interface ChatMessageRequest { chatId: string; message: string; modelId: string; personalidade?: string; anexos?: File[]; // Array de até 5 arquivos userEmail?: string; estabelecimentoId?: number; } /** * Interface para armazenamento local de conversas * Preparando para futura integração com banco de dados */ export interface StoredChat { id: string; title: string; createdAt: Date; updatedAt: Date; model: string; systemPrompt: string; messages: Array<{ id: string; role: 'user' | 'assistant'; content: string; model?: string; timestamp: Date; attachments?: Array<{ name: string; type: string; size: number; }>; }>; } /** * Interface para armazenamento de pastas * Preparando para futura integração com banco de dados */ export interface StoredFolder { id: string; name: string; createdAt: Date; chatIds: string[]; } /** * Interface para Folder no banco de dados */ export interface FolderRecord { id: string; name: string; created_at: string; updated_at: string; } /** * Interface para Chat no banco de dados */ export interface ChatRecord { id: string; title: string; model_id: number; folder_id: string | null; created_at: string; updated_at: string; personalidade: string; // Personalidade/prompt do sistema salvo no banco estabelecimento_id: number; } /** * Interface para resposta do GET de chats e folders */ export interface GetChatsAndFoldersResponse { chats: ChatRecord[]; folders: FolderRecord[]; } /** * Interface para Message no banco de dados */ export interface MessageRecord { id: string; chat_id: string; role: 'user' | 'assistant'; content: string; model_id: number; model_name?: string; // Nome do modelo retornado pela API has_attachments: number; input_tokens: number; output_tokens: number; total_tokens: number; cost_usd: string; created_at: string; } /** * Serviço de chat com IA */ class ChatService { private readonly CHAT_ENDPOINT = '/webhook/codex/message'; private readonly STORAGE_KEY_CHATS = 'hgtx_chats'; private readonly STORAGE_KEY_FOLDERS = 'hgtx_folders'; // Endpoints para folders e chats private readonly POST_FOLDER_ENDPOINT = '/webhook/codex/post_folders'; private readonly GET_CHATS_FOLDERS_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_chat_folders'; private readonly PUT_CHAT_IN_FOLDER_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/insert_chat_in_folder'; private readonly DELETE_FOLDER_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_chat_folder'; private readonly DELETE_CHAT_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_chat_messages'; private readonly GET_CHAT_MESSAGES_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_chat_messages'; // Formatos de arquivo permitidos (atualmente) private readonly ALLOWED_FILE_TYPES = { // Formatos ativos 'application/pdf': { ext: '.pdf', label: 'PDF' }, 'image/png': { ext: '.png', label: 'PNG' }, 'image/jpeg': { ext: '.jpg, .jpeg', label: 'JPEG' }, 'image/webp': { ext: '.webp', label: 'WebP' }, // Formatos futuros (desabilitados por enquanto) // 'text/csv': { ext: '.csv', label: 'CSV' }, // 'text/plain': { ext: '.txt', label: 'TXT' }, // 'application/json': { ext: '.json', label: 'JSON' }, // 'application/vnd.ms-excel': { ext: '.xls', label: 'XLS' }, // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { ext: '.xlsx', label: 'XLSX' }, }; // Número máximo de anexos permitidos private readonly MAX_ATTACHMENTS = 5; /** * Gera um chat_id único e seguro usando hash * Formato: timestamp + random + hash * * NOTA: Esta função está mantida para compatibilidade e uso no localStorage, * mas para comunicação com a API, o fluxo correto é: * 1. Enviar chat_id: "0" na primeira mensagem * 2. API retorna o chat_id real * 3. Usar o chat_id retornado nas próximas mensagens * * @returns string - ID único para o chat (uso local) */ generateChatId(): string { const timestamp = Date.now().toString(36); const randomPart = Math.random().toString(36).substring(2, 15); const randomPart2 = Math.random().toString(36).substring(2, 15); // Combina timestamp e partes aleatórias para criar ID único const chatId = `chat_${timestamp}_${randomPart}${randomPart2}`; return chatId; } /** * Envia mensagem para a API de chat * * @param request - Dados da requisição (mensagem, modelo, anexos, etc) * @returns Promise com a resposta da API */ async sendMessage(request: ChatMessageRequest): Promise { const { chatId, message, modelId, personalidade, anexos, 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 || import.meta.env.VITE_ESTABELECIMENTO_ID || ''; // Validações if (!chatId) { throw { success: false, message: 'ID do chat é obrigatório', }; } if (!message || message.trim().length === 0) { throw { success: false, message: 'A mensagem não pode estar vazia', }; } if (!modelId) { throw { success: false, message: 'Modelo da IA é obrigatório', }; } // Valida número de anexos if (anexos && anexos.length > 5) { throw { success: false, message: 'Máximo de 5 arquivos anexos permitidos', }; } // Cria FormData para envio multipart const formData = new FormData(); formData.append('estabelecimento_id', estabId.toString()); formData.append('chat_id', chatId); formData.append('user_email', email); formData.append('model_id', modelId); formData.append('message', message); // Adiciona personalidade se fornecida if (personalidade && personalidade.trim().length > 0) { formData.append('personalidade', personalidade); } // Adiciona anexos (máximo 5) if (anexos && anexos.length > 0) { anexos.forEach((file, index) => { if (index < 5) { formData.append(`anexo${index + 1}`, file); } }); } // Log para debug (remover em produção se necessário) console.log('Enviando mensagem:', { chatId, messageLength: message.length, modelId, hasPersonalidade: !!personalidade, attachmentsCount: anexos?.length || 0, userEmail: email, estabelecimentoId: estabId, }); try { // Faz a requisição usando o serviço de API const response = await apiService.postFormData( this.CHAT_ENDPOINT, formData ); return response.data; } catch (error: any) { // Trata erros específicos console.error('Erro ao enviar mensagem:', error); throw { success: false, message: error.message || 'Erro ao comunicar com a IA', status: error.status, }; } } /** * Salva ou atualiza um chat no localStorage * Preparado para futura migração para banco de dados * * @param chat - Chat a ser salvo */ saveChat(chat: StoredChat): void { try { const chats = this.getAllChats(); const existingIndex = chats.findIndex(c => c.id === chat.id); if (existingIndex >= 0) { chats[existingIndex] = { ...chat, updatedAt: new Date(), }; } else { chats.push(chat); } localStorage.setItem(this.STORAGE_KEY_CHATS, JSON.stringify(chats)); } catch (error) { console.error('Erro ao salvar chat:', error); throw new Error('Não foi possível salvar o chat'); } } /** * Busca um chat específico por ID * * @param chatId - ID do chat * @returns Chat encontrado ou undefined */ getChat(chatId: string): StoredChat | undefined { const chats = this.getAllChats(); return chats.find(c => c.id === chatId); } /** * Retorna todos os chats salvos * * @returns Array de chats */ getAllChats(): StoredChat[] { try { const chatsJson = localStorage.getItem(this.STORAGE_KEY_CHATS); if (!chatsJson) return []; const chats = JSON.parse(chatsJson); // Converte strings de data para objetos Date return chats.map((chat: any) => ({ ...chat, createdAt: new Date(chat.createdAt), updatedAt: new Date(chat.updatedAt), messages: chat.messages.map((msg: any) => ({ ...msg, timestamp: new Date(msg.timestamp), })), })); } catch (error) { console.error('Erro ao carregar chats:', error); return []; } } /** * Deleta um chat do localStorage * * @param chatId - ID do chat a ser deletado */ deleteChatLocal(chatId: string): void { try { const chats = this.getAllChats(); const filteredChats = chats.filter(c => c.id !== chatId); localStorage.setItem(this.STORAGE_KEY_CHATS, JSON.stringify(filteredChats)); } catch (error) { console.error('Erro ao deletar chat:', error); throw new Error('Não foi possível deletar o chat'); } } /** * Salva uma pasta no localStorage * * @param folder - Pasta a ser salva */ saveFolder(folder: StoredFolder): void { try { const folders = this.getAllFolders(); const existingIndex = folders.findIndex(f => f.id === folder.id); if (existingIndex >= 0) { folders[existingIndex] = folder; } else { folders.push(folder); } localStorage.setItem(this.STORAGE_KEY_FOLDERS, JSON.stringify(folders)); } catch (error) { console.error('Erro ao salvar pasta:', error); throw new Error('Não foi possível salvar a pasta'); } } /** * Retorna todas as pastas salvas * * @returns Array de pastas */ getAllFolders(): StoredFolder[] { try { const foldersJson = localStorage.getItem(this.STORAGE_KEY_FOLDERS); if (!foldersJson) return []; const folders = JSON.parse(foldersJson); // Converte strings de data para objetos Date return folders.map((folder: any) => ({ ...folder, createdAt: new Date(folder.createdAt), })); } catch (error) { console.error('Erro ao carregar pastas:', error); return []; } } /** * Deleta uma pasta do localStorage * * @param folderId - ID da pasta a ser deletada */ deleteFolderLocal(folderId: string): void { try { const folders = this.getAllFolders(); const filteredFolders = folders.filter(f => f.id !== folderId); localStorage.setItem(this.STORAGE_KEY_FOLDERS, JSON.stringify(filteredFolders)); } catch (error) { console.error('Erro ao deletar pasta:', error); throw new Error('Não foi possível deletar a pasta'); } } /** * Gera título automático para o chat baseado na primeira mensagem * * @param message - Primeira mensagem do usuário * @param maxLength - Comprimento máximo do título * @returns Título gerado */ generateChatTitle(message: string, maxLength: number = 50): string { if (!message || message.trim().length === 0) { return 'Nova Conversa'; } const trimmed = message.trim(); if (trimmed.length <= maxLength) { return trimmed; } return trimmed.substring(0, maxLength) + '...'; } /** * Limpa todas as conversas (usar com cuidado) */ clearAllChats(): void { localStorage.removeItem(this.STORAGE_KEY_CHATS); } /** * Limpa todas as pastas (usar com cuidado) */ clearAllFolders(): void { localStorage.removeItem(this.STORAGE_KEY_FOLDERS); } /** * Valida anexos antes de enviar para a API * * @param files - Array de arquivos a serem validados * @returns Objeto com resultado da validação */ validateAttachments(files: File[]): { valid: boolean; error?: string } { // Valida número de anexos if (files.length > this.MAX_ATTACHMENTS) { return { valid: false, error: `Máximo de ${this.MAX_ATTACHMENTS} anexos permitidos. Você selecionou ${files.length}.`, }; } // Valida cada arquivo for (let i = 0; i < files.length; i++) { const file = files[i]; const fileType = file.type; const fileName = file.name; // Verifica se o tipo MIME é permitido if (!this.ALLOWED_FILE_TYPES[fileType as keyof typeof this.ALLOWED_FILE_TYPES]) { // Tenta validar pela extensão também const extension = fileName.toLowerCase().substring(fileName.lastIndexOf('.')); const isExtensionValid = Object.values(this.ALLOWED_FILE_TYPES).some( type => type.ext.includes(extension) ); if (!isExtensionValid) { const allowedFormats = Object.values(this.ALLOWED_FILE_TYPES) .map(t => t.label) .join(', '); return { valid: false, error: `Arquivo "${fileName}" não é permitido. Formatos aceitos: ${allowedFormats}`, }; } } } return { valid: true }; } /** * Retorna lista de formatos de arquivo permitidos * * @returns Array com informações dos formatos */ getAllowedFileTypes() { return Object.entries(this.ALLOWED_FILE_TYPES).map(([mimeType, info]) => ({ mimeType, ...info, })); } /** * Retorna string formatada com formatos permitidos (para exibição) * * @returns String formatada (ex: "PDF, PNG, JPEG, WebP") */ getAllowedFileTypesLabel(): string { return Object.values(this.ALLOWED_FILE_TYPES) .map(t => t.label) .join(', '); } /** * Retorna string com extensões para usar no input file accept * * @returns String formatada (ex: ".pdf,.png,.jpg,.jpeg,.webp") */ getAllowedFileExtensions(): string { return Object.values(this.ALLOWED_FILE_TYPES) .map(t => t.ext) .join(','); } /** * Retorna número máximo de anexos permitidos */ getMaxAttachments(): number { return this.MAX_ATTACHMENTS; } // ===== MÉTODOS DE INTEGRAÇÃO COM BANCO DE DADOS ===== /** * Cria uma nova pasta no banco de dados * * @param name - Nome da pasta * @param userEmail - Email do usuário (opcional) * @returns Promise com sucesso ou erro */ async createFolder(name: string, userEmail?: string): Promise<{ success: boolean; folder?: FolderRecord }> { const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!email) { throw { success: false, message: 'Email do usuário não fornecido', }; } if (!name || name.trim().length === 0) { throw { success: false, message: 'Nome da pasta não pode estar vazio', }; } console.log('Criando pasta:', { name, userEmail: email, }); try { const response = await apiService.post>( this.POST_FOLDER_ENDPOINT, { user_email: email, name: name.trim(), } ); console.log('Resposta completa do POST:', response); console.log('response.data:', response.data); // A API retorna um array com um objeto: [{"success":true, ...}] let result: { success: boolean; user_email?: string; name?: string }; if (Array.isArray(response.data)) { result = response.data[0]; console.log('API retornou array, usando primeiro elemento:', result); } else { result = response.data as any; console.log('API retornou objeto direto:', result); } return { success: result.success ?? true, }; } catch (error: any) { console.error('Erro ao criar pasta:', error); throw { success: false, message: error.message || 'Erro ao criar pasta', status: error.status, }; } } /** * Renomeia uma pasta existente * * @param folderId - ID da pasta a ser renomeada * @param newName - Novo nome da pasta * @param userEmail - Email do usuário (opcional) * @returns Promise com o resultado da operação */ async renameFolder(folderId: string, newName: string, userEmail?: string): Promise<{ success: boolean; folder?: FolderRecord }> { const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!email) { throw { success: false, message: 'Email do usuário não fornecido', }; } if (!folderId || folderId.trim().length === 0) { throw { success: false, message: 'ID da pasta não pode estar vazio', }; } if (!newName || newName.trim().length === 0) { throw { success: false, message: 'Nome da pasta não pode estar vazio', }; } console.log('Renomeando pasta:', { folderId, newName, userEmail: email, }); try { const response = await apiService.put>( this.POST_FOLDER_ENDPOINT, { id: folderId, user_email: email, name: newName.trim(), } ); console.log('Resposta completa do PUT (renomear pasta):', response); console.log('response.data:', response.data); // A API retorna um array com um objeto: [{"success":true, "user_email": "...", "name": "...", "id": "..."}] let result: { success: boolean; user_email?: string; name?: string; id?: string }; if (Array.isArray(response.data)) { result = response.data[0]; console.log('API retornou array, usando primeiro elemento:', result); } else { result = response.data as any; console.log('API retornou objeto direto:', result); } return { success: result.success ?? true, }; } catch (error: any) { console.error('Erro ao renomear pasta:', error); throw { success: false, message: error.message || 'Erro ao renomear pasta', status: error.status, }; } } /** * Busca todos os chats e pastas do usuário * * @param userEmail - Email do usuário (opcional) * @returns Promise com chats e folders */ async getChatsAndFolders(userEmail?: string): Promise { const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!email) { throw { success: false, message: 'Email do usuário não fornecido', }; } console.log('Buscando chats e pastas:', { userEmail: email, }); try { const response = await apiService.get>( `${this.GET_CHATS_FOLDERS_ENDPOINT}/${email}` ); console.log('Resposta completa da API:', response); console.log('response.data:', response.data); // A API retorna um array com um objeto "result": [{"result": {"chats": [...], "folders": [...]}}] let result: GetChatsAndFoldersResponse; if (Array.isArray(response.data) && response.data.length > 0) { result = response.data[0].result; console.log('API retornou array com result:', result); } else if ((response.data as any).result) { result = (response.data as any).result; console.log('API retornou objeto com result:', result); } else { // Fallback: retorna vazio console.warn('Estrutura inesperada da resposta'); result = { chats: [], folders: [] }; } // Garante que chats e folders são arrays return { chats: Array.isArray(result.chats) ? result.chats : [], folders: Array.isArray(result.folders) ? result.folders : [], }; } catch (error: any) { console.error('Erro ao buscar chats e pastas:', error); throw { success: false, message: error.message || 'Erro ao buscar chats e pastas', status: error.status, }; } } /** * Move um chat para dentro de uma pasta * * @param chatId - ID do chat * @param folderId - ID da pasta * @returns Promise com sucesso ou erro */ async moveChatToFolder(chatId: string, folderId: string): Promise<{ success: boolean }> { if (!chatId || !folderId) { throw { success: false, message: 'Chat ID e Folder ID são obrigatórios', }; } console.log('Movendo chat para pasta:', { chatId, folderId, }); try { const response = await apiService.put>( `${this.PUT_CHAT_IN_FOLDER_ENDPOINT}/${folderId}/${chatId}` ); console.log('Resposta completa do PUT:', response); console.log('response.data:', response.data); // A API retorna um array com um objeto: [{"success":true}] let result: { success: boolean }; if (Array.isArray(response.data)) { result = response.data[0]; console.log('API retornou array, usando primeiro elemento:', result); } else { result = response.data; console.log('API retornou objeto direto:', result); } return { success: result.success ?? true, }; } catch (error: any) { console.error('Erro ao mover chat para pasta:', error); throw { success: false, message: error.message || 'Erro ao mover chat para pasta', status: error.status, }; } } /** * Deleta uma pasta do banco de dados * * @param folderId - ID da pasta * @param userEmail - Email do usuário (opcional) * @returns Promise com sucesso ou erro */ async deleteFolder(folderId: string, userEmail?: string): Promise<{ success: boolean }> { const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!email) { throw { success: false, message: 'Email do usuário não fornecido', }; } if (!folderId) { throw { success: false, message: 'ID da pasta não fornecido', }; } console.log('Deletando pasta:', { folderId, userEmail: email, }); try { const response = await apiService.delete>( `${this.DELETE_FOLDER_ENDPOINT}/${email}/${folderId}` ); 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 }; if (Array.isArray(response.data)) { result = response.data[0]; console.log('API retornou array, usando primeiro elemento:', result); } else { result = response.data; console.log('API retornou objeto direto:', result); } return { success: result.success ?? true, }; } catch (error: any) { console.error('Erro ao deletar pasta:', error); throw { success: false, message: error.message || 'Erro ao deletar pasta', status: error.status, }; } } /** * Deleta um chat do banco de dados * * @param chatId - ID do chat * @param userEmail - Email do usuário (opcional) * @returns Promise com sucesso ou erro */ async deleteChat(chatId: string, userEmail?: string): Promise<{ success: boolean }> { const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!email) { throw { success: false, message: 'Email do usuário não fornecido', }; } if (!chatId) { throw { success: false, message: 'ID do chat não fornecido', }; } console.log('Deletando chat:', { chatId, userEmail: email, }); try { const response = await apiService.delete>( `${this.DELETE_CHAT_ENDPOINT}/${email}/${chatId}` ); 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 }; if (Array.isArray(response.data)) { result = response.data[0]; console.log('API retornou array, usando primeiro elemento:', result); } else { result = response.data; console.log('API retornou objeto direto:', result); } return { success: result.success ?? true, }; } catch (error: any) { console.error('Erro ao deletar chat:', error); throw { success: false, message: error.message || 'Erro ao deletar chat', status: error.status, }; } } /** * Busca todas as mensagens de um chat específico * * @param chatId - ID do chat * @param userEmail - Email do usuário (opcional) * @returns Promise com array de mensagens */ async getChatMessages(chatId: string, userEmail?: string): Promise { const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!email) { throw { success: false, message: 'Email do usuário não fornecido', }; } if (!chatId) { throw { success: false, message: 'ID do chat não fornecido', }; } console.log('Buscando mensagens do chat:', { chatId, userEmail: email, }); try { const response = await apiService.get( `${this.GET_CHAT_MESSAGES_ENDPOINT}/${email}/${chatId}` ); 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 mensagens // 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 'messages' 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).messages)) { return (response.data as any).messages; } 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 mensagens da resposta'); return []; } catch (error: any) { console.error('Erro ao buscar mensagens:', error); throw { success: false, message: error.message || 'Erro ao buscar mensagens', status: error.status, }; } } } // Exporta instância única (Singleton) export const chatService = new ChatService();