Files
OPEN_CODEX_API/src/services/chat.ts
T
2025-10-22 12:52:23 -03:00

482 lines
13 KiB
TypeScript

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<ChatResponse> {
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<ChatResponse>(
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();