diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc61f26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Variáveis de ambiente +.env +.env.* # Ignora todas as variações (.env.local, .env.dev, etc.) + +# Dependências +node_modules/ + +# Logs e temporários +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ + +#Testes +test.js diff --git a/README.md b/README.md index 72aa510..27e6311 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,185 @@ -# projeto_cli_desenvolve -Projeto em equipe do Desenvolve do Grupo Boticário +# 🤖 VingaDevs CLI — Análise de Sentimentos com DeepSeek via OpenRouter + +[![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/) +[![API](https://img.shields.io/badge/API-OpenRouter_DeepSeek-blue.svg)](https://openrouter.ai/) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Team](https://img.shields.io/badge/Team-VingaDevs-purple.svg)](https://github.com/LilianMS/projeto_cli_desenvolve) + +> **Projeto da equipe VingaDevs - Desenvolve Grupo Boticário** 🚀 + +Este é um **CLI (Command Line Interface)** desenvolvido pela equipe **VingaDevs** que analisa sentimentos em textos em português utilizando o modelo **DeepSeek** via API **OpenRouter**, que oferece acesso gratuito a modelos avançados de IA. + +--- + +## ✨ Funcionalidades Principais + +- 🎯 Classificação simples e rápida: Positivo, Neutro ou Negativo +- 💬 Resultado com emojis e cores para melhor visualização +- 🔄 Retry automático para lidar com falhas temporárias da API +- 🧹 Sanitização do texto para melhorar a qualidade da análise +- 🛡️ Validação e tratamento de erros na entrada e API +- ⚙️ Configuração flexível para trocar modelos IA conforme necessidade +- 📦 Código modular, organizado e fácil de manter + +--- + +## 🧰 Tecnologias Utilizadas + +- **Node.js** v18+ +- **OpenRouter API** (modelo DeepSeek Chat) +- **Commander** para interface de linha de comando +- **Chalk** para cores e emojis no terminal +- **Dotenv** para variáveis de ambiente +- **Node-fetch** para requisições HTTP +- Arquitetura modularizada para facilidade de manutenção + +--- + +## 🚀 Passo a Passo: Instalação e Uso + +### 1. Clone o Repositório e Entre na Branch + +```bash +git clone https://github.com/LilianMS/projeto_cli_desenvolve.git +cd projeto_cli_desenvolve +git checkout leticia +``` +### 2. Instale as Dependências + +```bash +npm install +``` +### 3. Crie sua conta no OpenRouter e obtenha a API Key + +* Acesse o site oficial do OpenRouter: https://openrouter.ai/ + +* Clique em Sign Up e crie sua conta gratuita + +* Após confirmar o email e fazer login, vá para o painel de usuário (Dashboard) + +* Na aba API Keys, clique em Create New API Key + +* Copie sua chave gerada (string alfanumérica) + +### 4. Configure a chave no projeto + +Na raiz do projeto, crie um arquivo .env com o seguinte conteúdo: + +```bash +OPENROUTER_API_KEY="sua_chave_aqui" +``` +### 5. Execute a análise de sentimento + +Você pode analisar o sentimento de um texto direto pelo terminal, assim: + +```bash +node src/index.js "Estou muito feliz com o resultado!" +``` +Saída esperada: + +```bash +😄 Positivo (Confiança: 92%) +``` + +--- + +### ⚙️ Como usar outro modelo IA + +Este projeto foi desenvolvido para ser flexível e permitir o uso de outros modelos IA via OpenRouter (ex: GPT-4, Claude) ou outros provedores. + +Para trocar o modelo: + +1. Abra o arquivo src/config/constants.js + +2. Modifique a linha do modelo, por exemplo: + +```js +export const CONFIG = { + MODEL: "openrouter/deepseek-chat", // modelo padrão + // MODEL: "openai/gpt-4", // outro modelo disponível + MAX_RETRIES: 3, +}; + +``` +3. Ajuste, se necessário, a estrutura das chamadas na camada de serviço (arquivo services/sentimentService.js), pois diferentes modelos podem ter requisições e respostas distintas. + +Dica: Consulte a documentação do OpenRouter e do modelo escolhido para ajustar o payload da API. + +--- + +### 🛠 Estrutura do Projeto + +```bash + src/ +├── commands/ # Comandos CLI e parsing de argumentos +├── config/ # Constantes e configurações (chave, modelo, retries) +├── services/ # Integração com API OpenRouter +├── utils/ # Sanitização, validação e saída formatada +├── index.js # Ponto de entrada do CLI + +``` + +--- + +### 💡 Exemplos de Uso + +| Comando | Saída Esperada | +|---------------------------------------------|--------------------------| +| `node src/index.js "Hoje foi um dia ótimo!"` | 😄 Positivo (92%) | +| `node src/index.js "Não gostei do atendimento"` | 😠 Negativo (88%) | +| `node src/index.js "Está tudo ok."` | 😐 Neutro (75%) | + +--- + +### 🔧 Tratamento de Erros e Retry + +* Validação do texto para garantir entrada válida + +* Retry automático em até 3 tentativas para chamadas falhas na API + +* Mensagens claras para erros de rede ou chave inválida + +--- + +## 🤝 Equipe VingaDevs + +- **Alicia** — UX e modo interativo +- **Brenda** — Integração API e testes +- **Davis** — Arquitetura e backend +- **Leticia** — Documentação, requisitos e código (versão DeepSeek/OpenRouter) +- **Lilian** — Organização e integração de APIs + +--- + +## 🚧 Melhorias Futuras + +- 📁 Análise de arquivos de texto (`.txt`) +- 🐛 Modo verboso para logs detalhados (`--verbose`) +- 🧠 Cache local para reduzir chamadas repetidas +- 🧪 Testes automatizados com Jest/Mocha +- 📤 Exportação dos resultados para CSV/JSON + +--- + +## 📄 Licença + +MIT License © 2024 - Equipe VingaDevs + +--- + +## 🙏 Agradecimentos + +- **Desenvolve Grupo Boticário** pelo programa de capacitação +- **OpenRouter** pela API gratuita e confiável +- **Comunidade Node.js** e mantenedores das bibliotecas utilizadas + +--- + +
+ +Feito com ❤️ pela equipe **VingaDevs** do Desenvolve Grupo Boticário + +[⬆️ Voltar ao topo](#) + +
+``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..664cb83 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,175 @@ +{ + "name": "sentimentos-cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sentimentos-cli", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "dotenv": "^17.2.1", + "node-fetch": "^3.3.2", + "prompt-sync": "^4.2.0" + }, + "devDependencies": {} + }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/prompt-sync": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/prompt-sync/-/prompt-sync-4.2.0.tgz", + "integrity": "sha512-BuEzzc5zptP5LsgV5MZETjDaKSWfchl5U9Luiu8SKp7iZWD5tZalOxvNcZRwv+d2phNFr8xlbxmFNcRKfJOzJw==", + "license": "MIT", + "dependencies": { + "strip-ansi": "^5.0.0" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b1102a --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "sentimentos-cli", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node src/index.js", + "test": "node src/index.js 'Texto de teste'" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "dotenv": "^17.2.1", + "node-fetch": "^3.3.2", + "prompt-sync": "^4.2.0" + }, + "devDependencies": {} +} diff --git a/src/commands/analyzeCmd.js b/src/commands/analyzeCmd.js new file mode 100644 index 0000000..ff92b40 --- /dev/null +++ b/src/commands/analyzeCmd.js @@ -0,0 +1,56 @@ +import { analyzeSentiment } from '../services/sentimentService.js'; +import { validarTexto, sanitizarTexto } from '../utils/sentimentValidation.js'; +import { exibirResultado, exibirErro, exibirMensagemAnalise } from '../utils/display.js'; +import { CONFIG, MESSAGES } from '../config/constants.js'; + +/** + * Executa a análise de sentimento. + * @param {string} texto + */ +export async function executarAnalise(textoBruto) { + try { + // 1. Validação da entrada + const { valido, erro } = validarTexto(textoBruto); + if (!valido) { + exibirErro(`${MESSAGES.INVALID_TEXT}: ${erro}`); + return; + } + + // 2. Sanitização do texto + const textoSanitizado = sanitizarTexto(textoBruto); + + // 3. Exibir mensagem de análise + exibirMensagemAnalise(); + + // 4. Chamar o serviço de análise de sentimento (com retries) + let resultadoAnalise = null; + let tentativas = 0; + while (tentativas < CONFIG.MAX_RETRIES) { + try { + resultadoAnalise = await analyzeSentiment(textoSanitizado); + break; // Sai do loop se a chamada for bem-sucedida + } catch (apiError) { + tentativas++; + if (tentativas < CONFIG.MAX_RETRIES) { + exibirErro(`Tentativa ${tentativas} falhou: ${apiError.message}. Retentando...`); + // Opcional: adicionar um delay antes de retentar + await new Promise(resolve => setTimeout(resolve, 1000 * tentativas)); // Espera 1s, depois 2s... + } else { + throw apiError; // Lança o erro após todas as tentativas falharem + } + } + } + + // 5. Exibir resultado + if (resultadoAnalise) { + exibirResultado(resultadoAnalise.sentimento, resultadoAnalise.confianca); + } else { + exibirErro("Não foi possível obter um resultado da análise."); + } + + } catch (error) { + // Tratamento de erros centralizado para qualquer falha não capturada anteriormente + exibirErro(error.message, error); + } +} + diff --git a/src/config/constants.js b/src/config/constants.js new file mode 100644 index 0000000..dbcb502 --- /dev/null +++ b/src/config/constants.js @@ -0,0 +1,31 @@ +//Mensagens em PT-BR (centralizadas para fácil manutenção) +export const MESSAGES = { + API_ERROR: "Erro na API", + INVALID_TEXT: "Texto inválido", + TIMEOUT: "Tempo de resposta excedido", + AUTH_ERROR: " Chave da API inválida", + ANALYZING: 'Analisando sentimento... Aguarde 🧠', + RESULT_PREFIX: 'Resultado da análise:', + +}; + +// Configurações Padrão +export const CONFIG={ + MODEL: "deepseek/deepseek-chat-v3-0324:free", // Corrigido para o ID correto do modelo + MAX_RETRIES: 2, + TIMEOUT_MS: 30000, // 30 segundos +}; + +export const SENTIMENT_EMOJIS = { + positive: '😄', + negative: '😠', + neutral: '😐', + unknown: '🤔' +}; + +export const SENTIMENT_LABELS = { + positive: 'Positivo', + negative: 'Negativo', + neutral: 'Neutro', + unknown: 'Indeterminado' +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..493c863 --- /dev/null +++ b/src/index.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +import { program } from 'commander'; +import { executarAnalise } from './commands/analyzeCmd.js'; +import { exibirErro, exibirAjuda } from './utils/display.js'; + +program + .description('Analisa sentimentos em PT-BR usando DeepSeek') + .argument('', 'Texto para análise') + .action(executarAnalise); + +// Exibe ajuda se nenhum argumento for fornecido +if (process.argv.length < 3) { + exibirAjuda(); + process.exit(0); +} + +program.parseAsync(process.argv).catch((error) => { + if (error.code === 'commander.missingArgument') { + exibirErro(`Erro: ${error.message}`); + exibirAjuda(); + } else { + exibirErro(error.message, error); + } + process.exit(1); +}); + + diff --git a/src/services/sentimentService.js b/src/services/sentimentService.js new file mode 100644 index 0000000..6ea084a --- /dev/null +++ b/src/services/sentimentService.js @@ -0,0 +1,48 @@ +import 'dotenv/config'; +import { CONFIG, MESSAGES } from '../config/constants.js'; +import fetch from 'node-fetch'; // Importar node-fetch explicitamente + +// Configurações específicas para OpenRouter (API) +const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions'; +const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; // Renomeie no .env quando for usar outra API +const HTTP_REFERER = 'https://github.com/LilianMS/projeto_cli_desenvolve/tree/leticia'; // Substitua pelo seu projeto + +export async function analyzeSentiment(texto) { + if (!OPENROUTER_API_KEY) throw new Error(MESSAGES.AUTH_ERROR); + + try { + const response = await fetch(OPENROUTER_API_URL, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, + 'HTTP-Referer': HTTP_REFERER, // Obrigatório para OpenRouter! + 'Content-Type': 'application/json', + 'X-Title': 'Análise de Sentimentos' // Opcional (identificação) + }, + body: JSON.stringify({ + model: CONFIG.MODEL, // Modelo via OpenRouter + messages: [{ + role: "user", + content: `Analise o sentimento do texto (responda APENAS com "POSITIVO", "NEGATIVO" ou "NEUTRO"): "${texto}"` + }], + temperature: 0.3 + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error?.message || 'Erro na API'); + } + + const data = await response.json(); + const result = data.choices[0].message.content.trim().toUpperCase(); + + // Mapeamento simples + if (result.includes("POSITIVO")) return { sentimento: "positive", confianca: 0.9 }; + if (result.includes("NEGATIVO")) return { sentimento: "negative", confianca: 0.9 }; + return { sentimento: "neutral", confianca: 0.7 }; + + } catch (error) { + throw new Error(`${MESSAGES.API_ERROR}: ${error.message}`); + } +} diff --git a/src/utils/display.js b/src/utils/display.js new file mode 100644 index 0000000..3811b9a --- /dev/null +++ b/src/utils/display.js @@ -0,0 +1,71 @@ +import chalk from 'chalk'; +import { MESSAGES, SENTIMENT_EMOJIS, SENTIMENT_LABELS } from '../config/constants.js'; + +/** + * Exibe mensagem de carregamento/análise. + */ +export function exibirMensagemAnalise() { + console.log(chalk.yellow(MESSAGES.ANALYZING)); +} + +/** + * Exibe o resultado da análise de sentimento. + * @param {string} sentimento - Sentimento analisado ('positive', 'negative', 'neutral', 'unknown') + * @param {number} confianca - Nível de confiança da análise (0-1) + */ +export function exibirResultado(sentimento, confianca = null) { + console.log(chalk.green(MESSAGES.RESULT_PREFIX)); + + const emoji = SENTIMENT_EMOJIS[sentimento] || SENTIMENT_EMOJIS.unknown; + const label = SENTIMENT_LABELS[sentimento] || SENTIMENT_LABELS.unknown; + + console.log(`${emoji} ${chalk.bold(label)}`); + + if (confianca !== null) { + const porcentagem = Math.round(confianca * 100); + const cor = porcentagem >= 80 ? 'green' : porcentagem >= 60 ? 'yellow' : 'red'; + console.log(chalk[cor](`Confiança: ${porcentagem}%`)); + } +} + +/** + * Exibe mensagem de erro formatada. + * @param {string} mensagem - Mensagem de erro. + * @param {Error} erro - Objeto de erro (opcional). + */ +export function exibirErro(mensagem, erro = null) { + console.error(chalk.red(`❌ ${mensagem}`)); + + // Exibe o stack trace completo em ambiente de desenvolvimento + if (erro && process.env.NODE_ENV === 'development') { + console.error(chalk.gray(erro.stack)); + } +} + +/** + * Exibe informações sobre o uso da aplicação. + */ +export function exibirAjuda() { + console.log(chalk.cyan('\n📖 Como usar:')); + console.log(' node src/index.js "sua frase aqui"'); + console.log('\n📝 Exemplos:'); + console.log(' node src/index.js "Estou muito feliz hoje!"'); + console.log(' node src/index.js "Que dia terrível..."'); + console.log(' node src/index.js "O tempo está normal."'); +} + +/** + * Exibe mensagem de sucesso. + * @param {string} mensagem - Mensagem de sucesso. + */ +export function exibirSucesso(mensagem) { + console.log(chalk.green(`✅ ${mensagem}`)); +} + +/** + * Exibe mensagem de aviso. + * @param {string} mensagem - Mensagem de aviso. + */ +export function exibirAviso(mensagem) { + console.log(chalk.yellow(`⚠️ ${mensagem}`)); +} \ No newline at end of file diff --git a/src/utils/sentimentValidation.js b/src/utils/sentimentValidation.js new file mode 100644 index 0000000..368869a --- /dev/null +++ b/src/utils/sentimentValidation.js @@ -0,0 +1,81 @@ +/** + * Utilitários para validação de entrada + */ + +/** + * Valida se o texto fornecido é válido para análise + * @param {string} texto - Texto a ser validado + * @returns {Object} - { valido: boolean, erro: string|null } + */ +export function validarTexto(texto) { + // Verifica se o texto existe + if (!texto) { + return { + valido: false, + erro: 'Texto não fornecido' + }; + } + + // Verifica se é uma string + if (typeof texto !== 'string') { + return { + valido: false, + erro: 'Texto deve ser uma string' + }; + } + + // Remove espaços em branco e verifica se não está vazio + const textoLimpo = texto.trim(); + if (textoLimpo.length === 0) { + return { + valido: false, + erro: 'Texto não pode estar vazio' + }; + } + + // Verifica comprimento mínimo + if (textoLimpo.length < 2) { + return { + valido: false, + erro: 'Texto muito curto (mínimo 2 caracteres)' + }; + } + + // Verifica comprimento máximo (para evitar textos excessivamente longos) + if (textoLimpo.length > 1000) { + return { + valido: false, + erro: 'Texto muito longo (máximo 1000 caracteres)' + }; + } + + // Verifica se contém apenas espaços ou caracteres especiais + const textoSemEspacos = textoLimpo.replace(/\s+/g, ''); + if (textoSemEspacos.length === 0) { + return { + valido: false, + erro: 'Texto deve conter pelo menos uma palavra' + }; + } + + return { + valido: true, + erro: null + }; +} + +/** + * Sanitiza o texto removendo caracteres desnecessários, espaços múltiplos / limpa o texto + * @param {string} texto - Texto a ser sanitizado + * @returns {string} - Texto sanitizado + */ +export function sanitizarTexto(texto) { + if (!texto || typeof texto !== 'string') { + return ''; + } + + return texto + .trim() + .replace(/\s+/g, ' ') // Remove espaços múltiplos + .replace(/[^\w\s\u00C0-\u017F.,!?;:\'"()-]/g, ''); // Remove caracteres especiais, mantendo acentos e pontuação básica +} \ No newline at end of file