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[]; } /** * 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'; // 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 = userEmail || import.meta.env.VITE_USER_EMAIL || ''; const estabId = 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 * * @param chatId - ID do chat a ser deletado */ deleteChat(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 * * @param folderId - ID da pasta a ser deletada */ deleteFolder(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; } } // Exporta instância única (Singleton) export const chatService = new ChatService();