Proyecto ChatBot Con Gemini
This commit is contained in:
85
.gitignore
vendored
Normal file
85
.gitignore
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# ## General / Sistema Operativo y Editores ##
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Archivos de caché del sistema operativo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Archivos de configuración y caché de IDEs y editores
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Archivos de log
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Archivos de variables de entorno locales (¡MUY IMPORTANTE!)
|
||||||
|
# Contienen secretos como API Keys y contraseñas. NUNCA deben subirse.
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# ## Backend: .NET / C# (Carpeta ChatbotApi/) ##
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Carpetas de compilación y binarios. Se generan al compilar.
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
|
||||||
|
# Configuración de desarrollo que puede contener secretos.
|
||||||
|
# Es mejor usar "User Secrets" en desarrollo para las claves.
|
||||||
|
appsettings.Development.json
|
||||||
|
|
||||||
|
# Archivos de publicación de Visual Studio
|
||||||
|
[Pp]roperties/[Pp]ublish[Pp]rofiles/
|
||||||
|
*.pubxml
|
||||||
|
*.publish.xml
|
||||||
|
|
||||||
|
# Directorio de paquetes de NuGet (formato antiguo)
|
||||||
|
[Pp]ackages/
|
||||||
|
|
||||||
|
# Archivos de caché de Visual Studio
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# Herramientas de .NET
|
||||||
|
.dotnet/
|
||||||
|
tools/
|
||||||
|
|
||||||
|
# Archivos generados por Entity Framework
|
||||||
|
# Nota: La carpeta 'Migrations' SÍ se suele incluir en el repositorio,
|
||||||
|
# pero si tienes algún archivo temporal o generado dentro, puedes añadirlo aquí.
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# ## Frontend: React / Vite / Node.js ##
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Dependencias (se instalan con 'npm install' o 'yarn')
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Carpetas de build (se generan con 'npm run build')
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# Archivos de caché de herramientas de frontend
|
||||||
|
.npm/
|
||||||
|
.vite/
|
||||||
|
.cache/
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Reportes de cobertura de tests
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Archivos de configuración de proxy de Create React App
|
||||||
|
.proxyrc.js
|
||||||
24
ChatBot.sln
Normal file
24
ChatBot.sln
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.2.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatbotApi", "ChatbotApi\ChatbotApi.csproj", "{8AC7EB60-FCD7-C582-D2A8-BC8E14A32B4F}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{8AC7EB60-FCD7-C582-D2A8-BC8E14A32B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8AC7EB60-FCD7-C582-D2A8-BC8E14A32B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8AC7EB60-FCD7-C582-D2A8-BC8E14A32B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8AC7EB60-FCD7-C582-D2A8-BC8E14A32B4F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {1143CF62-1A9D-4807-9B98-D09479769AF9}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
30
ChatbotApi/ChatbotApi.csproj
Normal file
30
ChatbotApi/ChatbotApi.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DotNetEnv" Version="3.1.1" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||||
|
<PackageReference Include="LLamaSharp" Version="0.25.0" />
|
||||||
|
<PackageReference Include="LLamaSharp.Backend.Cpu" Version="0.25.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||||
|
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.2.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.EntityFrameworkCore" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
ChatbotApi/ChatbotApi.http
Normal file
6
ChatbotApi/ChatbotApi.http
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@ChatbotApi_HostAddress = http://localhost:5126
|
||||||
|
|
||||||
|
GET {{ChatbotApi_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
90
ChatbotApi/Constrollers/AdminController.cs
Normal file
90
ChatbotApi/Constrollers/AdminController.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Controllers/AdminController.cs
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace ChatbotApi.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class AdminController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly AppContexto _context;
|
||||||
|
|
||||||
|
public AdminController(AppContexto context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/admin/contexto
|
||||||
|
[HttpGet("contexto")]
|
||||||
|
public async Task<IActionResult> GetAllContextoItems()
|
||||||
|
{
|
||||||
|
var items = await _context.ContextoItems.OrderBy(i => i.Clave).ToListAsync();
|
||||||
|
return Ok(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/admin/contexto
|
||||||
|
[HttpPost("contexto")]
|
||||||
|
public async Task<IActionResult> CreateContextoItem([FromBody] ContextoItem item)
|
||||||
|
{
|
||||||
|
if (await _context.ContextoItems.AnyAsync(i => i.Clave == item.Clave))
|
||||||
|
{
|
||||||
|
return BadRequest("La clave ya existe.");
|
||||||
|
}
|
||||||
|
item.FechaActualizacion = DateTime.UtcNow;
|
||||||
|
_context.ContextoItems.Add(item);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return CreatedAtAction(nameof(GetAllContextoItems), new { id = item.Id }, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT: api/admin/contexto/5
|
||||||
|
[HttpPut("contexto/{id}")]
|
||||||
|
public async Task<IActionResult> UpdateContextoItem(int id, [FromBody] ContextoItem item)
|
||||||
|
{
|
||||||
|
if (id != item.Id)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
var existingItem = await _context.ContextoItems.FindAsync(id);
|
||||||
|
if (existingItem == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
existingItem.Valor = item.Valor;
|
||||||
|
existingItem.Descripcion = item.Descripcion;
|
||||||
|
existingItem.FechaActualizacion = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: api/admin/contexto/5
|
||||||
|
[HttpDelete("contexto/{id}")]
|
||||||
|
public async Task<IActionResult> DeleteContextoItem(int id)
|
||||||
|
{
|
||||||
|
var item = await _context.ContextoItems.FindAsync(id);
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
_context.ContextoItems.Remove(item);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: api/admin/logs
|
||||||
|
[HttpGet("logs")]
|
||||||
|
public async Task<IActionResult> GetConversationLogs()
|
||||||
|
{
|
||||||
|
// Obtenemos los últimos 200 logs, ordenados del más reciente al más antiguo.
|
||||||
|
var logs = await _context.ConversacionLogs
|
||||||
|
.OrderByDescending(log => log.Fecha)
|
||||||
|
.Take(200)
|
||||||
|
.ToListAsync();
|
||||||
|
return Ok(logs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
ChatbotApi/Constrollers/AuthController.cs
Normal file
96
ChatbotApi/Constrollers/AuthController.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// /Controllers/AuthController.cs
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public required string Username { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public required string Password { get; set; }
|
||||||
|
}
|
||||||
|
public class LoginResponse { public required string Token { get; set; } }
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly UserManager<IdentityUser> _userManager;
|
||||||
|
|
||||||
|
// Inyectamos el UserManager que gestiona los usuarios
|
||||||
|
public AuthController(IConfiguration configuration, UserManager<IdentityUser> userManager)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
|
||||||
|
{
|
||||||
|
// Buscamos al usuario por su nombre
|
||||||
|
var user = await _userManager.FindByNameAsync(loginRequest.Username);
|
||||||
|
|
||||||
|
// Verificamos si el usuario existe y si la contraseña es correcta
|
||||||
|
if (user != null && await _userManager.CheckPasswordAsync(user, loginRequest.Password))
|
||||||
|
{
|
||||||
|
var token = GenerateJwtToken(user);
|
||||||
|
return Ok(new LoginResponse { Token = token });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unauthorized("Credenciales inválidas.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para crear el primer usuario administrador (solo para configuración inicial)
|
||||||
|
[HttpPost("setup-admin")]
|
||||||
|
public async Task<IActionResult> SetupAdminUser()
|
||||||
|
{
|
||||||
|
var adminUser = await _userManager.FindByNameAsync("admin");
|
||||||
|
if (adminUser == null)
|
||||||
|
{
|
||||||
|
adminUser = new IdentityUser
|
||||||
|
{
|
||||||
|
UserName = "admin",
|
||||||
|
Email = "tecnica@eldia.com",
|
||||||
|
};
|
||||||
|
var result = await _userManager.CreateAsync(adminUser, "Diagonal423");
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
return Ok("Usuario administrador creado exitosamente.");
|
||||||
|
}
|
||||||
|
return BadRequest(result.Errors);
|
||||||
|
}
|
||||||
|
return Ok("El usuario administrador ya existe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateJwtToken(IdentityUser user)
|
||||||
|
{
|
||||||
|
var jwtKey = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("La clave JWT no está configurada.");
|
||||||
|
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||||
|
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, user.UserName!),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: _configuration["Jwt:Issuer"],
|
||||||
|
audience: _configuration["Jwt:Audience"],
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.AddHours(8),
|
||||||
|
signingCredentials: credentials);
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
436
ChatbotApi/Constrollers/ChatController.cs
Normal file
436
ChatbotApi/Constrollers/ChatController.cs
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
// ChatbotApi/Controllers/ChatController.cs
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ChatbotApi.Models;
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using HtmlAgilityPack;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
// Clases de Request/Response
|
||||||
|
public class GeminiRequest { [JsonPropertyName("contents")] public Content[] Contents { get; set; } = default!; }
|
||||||
|
public class Content { [JsonPropertyName("parts")] public Part[] Parts { get; set; } = default!; }
|
||||||
|
public class Part { [JsonPropertyName("text")] public string Text { get; set; } = default!; }
|
||||||
|
public class GeminiResponse { [JsonPropertyName("candidates")] public Candidate[] Candidates { get; set; } = default!; }
|
||||||
|
public class Candidate { [JsonPropertyName("content")] public Content Content { get; set; } = default!; }
|
||||||
|
public class GeminiStreamingResponse { [JsonPropertyName("candidates")] public StreamingCandidate[] Candidates { get; set; } = default!; }
|
||||||
|
public class StreamingCandidate { [JsonPropertyName("content")] public Content Content { get; set; } = default!; }
|
||||||
|
|
||||||
|
namespace ChatbotApi.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class ChatController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly string _apiUrl;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly IServiceProvider _serviceProvider; // Para crear un scope de DB
|
||||||
|
private readonly ILogger<ChatController> _logger;
|
||||||
|
private static readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
private static readonly string _knowledgeCacheKey = "KnowledgeBase"; // Clave única para nuestra caché
|
||||||
|
|
||||||
|
private static readonly string _siteUrl = "https://www.eldia.com/";
|
||||||
|
private static readonly string[] PrefijosAQuitar = { "VIDEO.- ", "VIDEO. ", "FOTOS.- ", "FOTOS. " };
|
||||||
|
|
||||||
|
public ChatController(IConfiguration configuration, IMemoryCache memoryCache, IServiceProvider serviceProvider, ILogger<ChatController> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_cache = memoryCache;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
var apiKey = configuration["Gemini:GeminiApiKey"]
|
||||||
|
?? throw new InvalidOperationException("La API Key de Gemini no está configurada en .env");
|
||||||
|
|
||||||
|
var baseUrl = configuration["Gemini:GeminiApiUrl"];
|
||||||
|
_apiUrl = $"{baseUrl}{apiKey}";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("stream-message")]
|
||||||
|
[EnableRateLimiting("fixed")]
|
||||||
|
public async IAsyncEnumerable<string> StreamMessage(
|
||||||
|
[FromBody] ChatRequest request,
|
||||||
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// --- FASE 1: Validación y Preparación ---
|
||||||
|
if (string.IsNullOrWhiteSpace(request?.Message))
|
||||||
|
{
|
||||||
|
yield return "Error: No he recibido ningún mensaje.";
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
string userMessage = request.Message;
|
||||||
|
string lowerUserMessage = userMessage.ToLowerInvariant();
|
||||||
|
string context;
|
||||||
|
string? errorMessage = null; // Variable para almacenar el mensaje de error
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var knowledgeBase = await GetKnowledgeAsync();
|
||||||
|
string? dbContext = GetContextFromDb(lowerUserMessage, knowledgeBase);
|
||||||
|
context = dbContext ?? await GetWebsiteNewsAsync(_siteUrl, 15);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al obtener el contexto para el streaming.");
|
||||||
|
errorMessage = "Error: No se pudo obtener la información de contexto.";
|
||||||
|
context = string.Empty; // Aseguramos que el contexto no sea nulo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si hubo un error en la fase anterior, lo devolvemos
|
||||||
|
if (!string.IsNullOrEmpty(errorMessage))
|
||||||
|
{
|
||||||
|
yield return errorMessage;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(context))
|
||||||
|
{
|
||||||
|
yield return "Error: No pude obtener información para responder a tu pregunta.";
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FASE 2: Configuración de la Conexión ---
|
||||||
|
Stream? responseStream = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var promptBuilder = new StringBuilder();
|
||||||
|
promptBuilder.AppendLine("INSTRUCCIONES:");
|
||||||
|
promptBuilder.AppendLine("Eres DiaBot, el asistente virtual del periódico El Día. Tu personalidad es profesional, servicial y concisa.");
|
||||||
|
promptBuilder.AppendLine(GetContextFromDb(lowerUserMessage, await GetKnowledgeAsync()) != null ? "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene información específica (contacto, suscripciones, etc.). No hables de noticias." : "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO DEL SITIO WEB' que contiene una lista de noticias. Si el usuario pide la URL de una noticia, DEBES proporcionarla en formato Markdown: '[texto del enlace](URL)'.");
|
||||||
|
promptBuilder.AppendLine("NUNCA INVENTES información. Si la respuesta no está en el contexto, indica amablemente que no encontraste la información.");
|
||||||
|
promptBuilder.AppendLine("\nCONTEXTO:\n---");
|
||||||
|
promptBuilder.AppendLine(context);
|
||||||
|
promptBuilder.AppendLine("---\n\nPREGUNTA DEL USUARIO:\n---");
|
||||||
|
promptBuilder.AppendLine(userMessage);
|
||||||
|
promptBuilder.AppendLine("---\n\nRESPUESTA:");
|
||||||
|
string finalPrompt = promptBuilder.ToString();
|
||||||
|
|
||||||
|
var streamingApiUrl = _apiUrl;
|
||||||
|
var requestData = new GeminiRequest { Contents = new[] { new Content { Parts = new[] { new Part { Text = finalPrompt } } } } };
|
||||||
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, streamingApiUrl);
|
||||||
|
httpRequestMessage.Content = JsonContent.Create(requestData);
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("La API de Gemini (Streaming) devolvió un error. Status: {StatusCode}, Content: {ErrorContent}", response.StatusCode, errorContent);
|
||||||
|
throw new HttpRequestException("La API de Gemini devolvió un error.");
|
||||||
|
}
|
||||||
|
|
||||||
|
responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("La operación fue cancelada por el cliente durante la configuración del stream.");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error inesperado durante la configuración del stream.");
|
||||||
|
errorMessage = "Error: Lo siento, estoy teniendo un problema técnico.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devolvemos el error de la fase de conexión si ocurrió
|
||||||
|
if (!string.IsNullOrEmpty(errorMessage))
|
||||||
|
{
|
||||||
|
yield return errorMessage;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FASE 3: Lectura y Devolución del Stream ---
|
||||||
|
var fullBotReply = new StringBuilder();
|
||||||
|
if (responseStream != null)
|
||||||
|
{
|
||||||
|
await using (responseStream)
|
||||||
|
using (var reader = new StreamReader(responseStream))
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: ")) continue;
|
||||||
|
|
||||||
|
var jsonString = line.Substring(6);
|
||||||
|
string? chunk = null; // 1. Declaramos la variable 'chunk' fuera del try.
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. El bloque try solo se encarga de la deserialización.
|
||||||
|
var geminiResponse = JsonSerializer.Deserialize<GeminiStreamingResponse>(jsonString);
|
||||||
|
chunk = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text;
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
// 3. Si falla, lo registramos y pasamos al siguiente fragmento.
|
||||||
|
_logger.LogWarning(ex, "No se pudo deserializar un chunk del stream: {JsonChunk}", jsonString);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. El yield return ahora está fuera del bloque try-catch.
|
||||||
|
if (chunk != null)
|
||||||
|
{
|
||||||
|
fullBotReply.Append(chunk);
|
||||||
|
yield return chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FASE 4: Guardado final ---
|
||||||
|
if (fullBotReply.Length > 0)
|
||||||
|
{
|
||||||
|
await SaveConversationLogAsync(userMessage, fullBotReply.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveConversationLogAsync(string userMessage, string botReply)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
||||||
|
var logEntry = new ConversacionLog
|
||||||
|
{
|
||||||
|
UsuarioMensaje = userMessage,
|
||||||
|
BotRespuesta = botReply,
|
||||||
|
Fecha = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
dbContext.ConversacionLogs.Add(logEntry);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception logEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(logEx, "Error al guardar el log de la conversación después del streaming.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("message")]
|
||||||
|
[EnableRateLimiting("fixed")]
|
||||||
|
public async Task<IActionResult> PostMessage([FromBody] ChatRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request?.Message))
|
||||||
|
{
|
||||||
|
return BadRequest(new ChatResponse { Reply = "No he recibido ningún mensaje." });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string userMessage = request.Message;
|
||||||
|
string lowerUserMessage = userMessage.ToLowerInvariant();
|
||||||
|
string context;
|
||||||
|
string promptInstructions;
|
||||||
|
|
||||||
|
// 1. Obtenemos el conocimiento desde nuestro nuevo método de caché
|
||||||
|
var knowledgeBase = await GetKnowledgeAsync();
|
||||||
|
string? dbContext = GetContextFromDb(lowerUserMessage, knowledgeBase);
|
||||||
|
|
||||||
|
if (dbContext != null)
|
||||||
|
{
|
||||||
|
context = dbContext;
|
||||||
|
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene información específica (contacto, suscripciones, etc.). No hables de noticias.";
|
||||||
|
}
|
||||||
|
// 2. Si no encontramos nada en la base de conocimiento, buscamos en las noticias.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context = await GetWebsiteNewsAsync(_siteUrl, 15);
|
||||||
|
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO DEL SITIO WEB' que contiene una lista de noticias. Si el usuario pide la URL de una noticia, DEBES proporcionarla en formato Markdown: '[texto del enlace](URL)'.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(context))
|
||||||
|
{
|
||||||
|
return StatusCode(500, new ChatResponse { Reply = "No pude obtener información para responder a tu pregunta." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var promptBuilder = new StringBuilder();
|
||||||
|
promptBuilder.AppendLine("INSTRUCCIONES:");
|
||||||
|
promptBuilder.AppendLine("Eres DiaBot, el asistente virtual del periódico El Día. Tu personalidad es profesional, servicial y concisa. Debes hablar en español 'Rioplatense'.");
|
||||||
|
promptBuilder.AppendLine(promptInstructions); // Instrucciones dinámicas
|
||||||
|
promptBuilder.AppendLine("NUNCA INVENTES información. Si la respuesta no está en el contexto, indica amablemente que no encontraste la información.");
|
||||||
|
promptBuilder.AppendLine("\nCONTEXTO:\n---");
|
||||||
|
promptBuilder.AppendLine(context);
|
||||||
|
promptBuilder.AppendLine("---\n\nPREGUNTA DEL USUARIO:\n---");
|
||||||
|
promptBuilder.AppendLine(userMessage);
|
||||||
|
promptBuilder.AppendLine("---\n\nRESPUESTA:");
|
||||||
|
string finalPrompt = promptBuilder.ToString();
|
||||||
|
|
||||||
|
var requestData = new GeminiRequest { Contents = new[] { new Content { Parts = new[] { new Part { Text = finalPrompt } } } } };
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(_apiUrl, requestData);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("La API de Gemini devolvió un error. Status: {StatusCode}, Content: {ErrorContent}", response.StatusCode, errorContent);
|
||||||
|
return StatusCode(500, new ChatResponse { Reply = "Hubo un error al comunicarse con el asistente de IA." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>();
|
||||||
|
string botReply = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim() ?? "Lo siento, no pude procesar una respuesta.";
|
||||||
|
|
||||||
|
_logger.LogInformation($"[DEBUG] Respuesta de Gemini: '{botReply}'");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Usamos el IServiceProvider para crear un scope de DbContext temporal y seguro.
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
// Renombramos la variable para evitar conflictos de ámbito.
|
||||||
|
var scopedDbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
||||||
|
|
||||||
|
var logEntry = new ConversacionLog
|
||||||
|
{
|
||||||
|
UsuarioMensaje = userMessage,
|
||||||
|
BotRespuesta = botReply,
|
||||||
|
Fecha = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
scopedDbContext.ConversacionLogs.Add(logEntry);
|
||||||
|
await scopedDbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception logEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(logEx, "Error al intentar guardar el log de la conversación en la base de datos.");
|
||||||
|
}
|
||||||
|
return Ok(new ChatResponse { Reply = botReply });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error inesperado al procesar el mensaje del usuario.");
|
||||||
|
return StatusCode(500, new ChatResponse { Reply = "Lo siento, estoy teniendo un problema técnico." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para buscar contexto en la caché de la DB
|
||||||
|
|
||||||
|
private string? GetContextFromDb(string lowerUserMessage, Dictionary<string, string> knowledgeBase)
|
||||||
|
{
|
||||||
|
// 1. Definimos una lista de palabras comunes a ignorar para hacer la búsqueda más precisa.
|
||||||
|
var stopWords = new HashSet<string>
|
||||||
|
{
|
||||||
|
"el", "la", "los", "las", "un", "una", "unos", "unas", "de", "del", "a", "ante",
|
||||||
|
"con", "contra", "desde", "en", "entre", "hacia", "hasta", "para", "por", "segun",
|
||||||
|
"sin", "sobre", "tras", "y", "o", "que", "cual", "cuales", "como", "cuando", "donde",
|
||||||
|
"quien", "es", "soy", "estoy", "mi", "mis", "quiero", "necesito", "saber", "dime", "dame",
|
||||||
|
"informacion", "acerca", "mas"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Separamos el mensaje del usuario en palabras individuales, eliminando las stop words.
|
||||||
|
var userWords = lowerUserMessage
|
||||||
|
.Split(new[] { ' ', ',', '.', '?', '!', '¿', '¡' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Where(word => !stopWords.Contains(word))
|
||||||
|
.ToHashSet(); // Usamos un HashSet para búsquedas de palabras muy rápidas.
|
||||||
|
|
||||||
|
if (!userWords.Any())
|
||||||
|
{
|
||||||
|
return null; // Si solo había stop words, no buscamos nada.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Iteramos sobre la base de conocimiento para encontrar la mejor coincidencia.
|
||||||
|
foreach (var kvp in knowledgeBase)
|
||||||
|
{
|
||||||
|
// Separamos las claves compuestas ("contacto_telefono" -> ["contacto", "telefono"])
|
||||||
|
var keywords = kvp.Key.Split('_');
|
||||||
|
|
||||||
|
// 4. Comprobamos si ALGUNA de las palabras clave de la BD coincide con ALGUNA de las palabras del usuario.
|
||||||
|
if (keywords.Any(k => userWords.Contains(k)))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Contexto encontrado por coincidencia de palabra clave. Clave de BD: '{DbKey}', Palabra de usuario encontrada: '{MatchedWord}'",
|
||||||
|
kvp.Key,
|
||||||
|
string.Join(", ", keywords.Where(k => userWords.Contains(k))));
|
||||||
|
|
||||||
|
return kvp.Value; // Devolvemos el valor correspondiente a la primera clave que coincida.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // No se encontró ninguna coincidencia.
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetWebsiteNewsAsync(string url, int cantidad)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var web = new HtmlWeb();
|
||||||
|
var doc = await web.LoadFromWebAsync(url);
|
||||||
|
var nodosDeEnlace = doc.DocumentNode.SelectNodes("//article[contains(@class, 'item')]/a[@href]");
|
||||||
|
|
||||||
|
if (nodosDeEnlace == null || !nodosDeEnlace.Any()) return string.Empty;
|
||||||
|
|
||||||
|
var contextBuilder = new StringBuilder();
|
||||||
|
contextBuilder.AppendLine("Lista de noticias principales extraídas de la página:");
|
||||||
|
var urlsProcesadas = new HashSet<string>();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
foreach (var nodoEnlace in nodosDeEnlace)
|
||||||
|
{
|
||||||
|
var urlRelativa = nodoEnlace.GetAttributeValue("href", string.Empty);
|
||||||
|
if (string.IsNullOrEmpty(urlRelativa) || urlRelativa == "#" || urlsProcesadas.Contains(urlRelativa)) continue;
|
||||||
|
|
||||||
|
var nodoTitulo = nodoEnlace.SelectSingleNode(".//h1 | .//h2");
|
||||||
|
if (nodoTitulo != null)
|
||||||
|
{
|
||||||
|
var textoLimpio = CleanTitleText(nodoTitulo.InnerText);
|
||||||
|
var urlCompleta = urlRelativa.StartsWith("/") ? new Uri(new Uri(url), urlRelativa).ToString() : urlRelativa;
|
||||||
|
contextBuilder.AppendLine($"- Título: \"{textoLimpio}\", URL: {urlCompleta}");
|
||||||
|
urlsProcesadas.Add(urlRelativa);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (count >= cantidad) break;
|
||||||
|
}
|
||||||
|
return contextBuilder.ToString();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "No se pudo descargar o procesar la URL {Url}", url);
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CleanTitleText(string texto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(texto)) return string.Empty;
|
||||||
|
var textoDecodificado = WebUtility.HtmlDecode(texto).Trim();
|
||||||
|
foreach (var prefijo in PrefijosAQuitar)
|
||||||
|
{
|
||||||
|
if (textoDecodificado.StartsWith(prefijo, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
textoDecodificado = textoDecodificado.Substring(prefijo.Length).Trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return textoDecodificado;
|
||||||
|
}
|
||||||
|
private async Task<Dictionary<string, string>> GetKnowledgeAsync()
|
||||||
|
{
|
||||||
|
// Intenta obtener el diccionario de la caché.
|
||||||
|
// Si no existe, el segundo argumento (la función factory) se ejecutará.
|
||||||
|
return await _cache.GetOrCreateAsync(_knowledgeCacheKey, async entry =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("La caché de conocimiento no existe o ha expirado. Recargando desde la base de datos...");
|
||||||
|
|
||||||
|
// Establecemos un tiempo de expiración para la caché.
|
||||||
|
// Después de 5 minutos, la caché se considerará inválida y se recargará en la siguiente petición.
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
// Usamos IServiceProvider para crear un 'scope' de servicios temporal.
|
||||||
|
// Esto es necesario porque el DbContext tiene un tiempo de vida por petición (scoped),
|
||||||
|
// y esta función factory podría ejecutarse fuera de ese contexto.
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
||||||
|
var knowledge = await dbContext.ContextoItems
|
||||||
|
.AsNoTracking() // Mejora el rendimiento para consultas de solo lectura
|
||||||
|
.ToDictionaryAsync(item => item.Clave, item => item.Valor);
|
||||||
|
|
||||||
|
_logger.LogInformation($"Caché actualizada con {knowledge.Count} items.");
|
||||||
|
return knowledge;
|
||||||
|
}
|
||||||
|
}) ?? new Dictionary<string, string>(); // Si todo falla, devuelve un diccionario vacío.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
ChatbotApi/Data/AppContexto.cs
Normal file
13
ChatbotApi/Data/AppContexto.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// ChatbotApi/Data/Models/AppContexto.cs
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ChatbotApi.Data.Models
|
||||||
|
{
|
||||||
|
public class AppContexto : IdentityDbContext
|
||||||
|
{
|
||||||
|
public AppContexto(DbContextOptions<AppContexto> options) : base(options) { }
|
||||||
|
|
||||||
|
public DbSet<ContextoItem> ContextoItems { get; set; } = null!;
|
||||||
|
public DbSet<ConversacionLog> ConversacionLogs { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
ChatbotApi/Data/Models/ContextoItem.cs
Normal file
25
ChatbotApi/Data/Models/ContextoItem.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Data/Models/ContextoItem.cs
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ChatbotApi.Data.Models
|
||||||
|
{
|
||||||
|
public class ContextoItem
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string Clave { get; set; } = null!;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(2000)]
|
||||||
|
public string Valor { get; set; } = null!;
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Descripcion { get; set; }
|
||||||
|
|
||||||
|
public DateTime FechaActualizacion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
18
ChatbotApi/Data/Models/ConversacionLog.cs
Normal file
18
ChatbotApi/Data/Models/ConversacionLog.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ChatbotApi.Data.Models
|
||||||
|
{
|
||||||
|
public class ConversacionLog
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string UsuarioMensaje { get; set; } = null!;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string BotRespuesta { get; set; } = null!;
|
||||||
|
|
||||||
|
public DateTime Fecha { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
58
ChatbotApi/Migrations/20251117132337_InitialCreate.Designer.cs
generated
Normal file
58
ChatbotApi/Migrations/20251117132337_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppContexto))]
|
||||||
|
[Migration("20251117132337_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ContextoItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Clave")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Descripcion")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FechaActualizacion")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Valor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ContextoItems");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
ChatbotApi/Migrations/20251117132337_InitialCreate.cs
Normal file
38
ChatbotApi/Migrations/20251117132337_InitialCreate.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ContextoItems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
Clave = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Valor = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Descripcion = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
FechaActualizacion = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ContextoItems", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ContextoItems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
ChatbotApi/Migrations/20251117161250_AddValidationLimitsToContextoItem.Designer.cs
generated
Normal file
60
ChatbotApi/Migrations/20251117161250_AddValidationLimitsToContextoItem.Designer.cs
generated
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppContexto))]
|
||||||
|
[Migration("20251117161250_AddValidationLimitsToContextoItem")]
|
||||||
|
partial class AddValidationLimitsToContextoItem
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ContextoItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Clave")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Descripcion")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FechaActualizacion")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Valor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ContextoItems");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddValidationLimitsToContextoItem : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Valor",
|
||||||
|
table: "ContextoItems",
|
||||||
|
type: "nvarchar(2000)",
|
||||||
|
maxLength: 2000,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(max)");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Descripcion",
|
||||||
|
table: "ContextoItems",
|
||||||
|
type: "nvarchar(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(max)",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Valor",
|
||||||
|
table: "ContextoItems",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(2000)",
|
||||||
|
oldMaxLength: 2000);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Descripcion",
|
||||||
|
table: "ContextoItems",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "nvarchar(500)",
|
||||||
|
oldMaxLength: 500,
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
309
ChatbotApi/Migrations/20251117173123_AddIdentityTables.Designer.cs
generated
Normal file
309
ChatbotApi/Migrations/20251117173123_AddIdentityTables.Designer.cs
generated
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppContexto))]
|
||||||
|
[Migration("20251117173123_AddIdentityTables")]
|
||||||
|
partial class AddIdentityTables
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ContextoItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Clave")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Descripcion")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FechaActualizacion")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Valor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ContextoItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
224
ChatbotApi/Migrations/20251117173123_AddIdentityTables.cs
Normal file
224
ChatbotApi/Migrations/20251117173123_AddIdentityTables.cs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddIdentityTables : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
|
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUsers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
|
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
|
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
|
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||||
|
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||||
|
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetRoleClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserLogins",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserRoles",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||||
|
column: x => x.RoleId,
|
||||||
|
principalTable: "AspNetRoles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AspNetUserTokens",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
|
||||||
|
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "AspNetUsers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetRoleClaims_RoleId",
|
||||||
|
table: "AspNetRoleClaims",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "RoleNameIndex",
|
||||||
|
table: "AspNetRoles",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true,
|
||||||
|
filter: "[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserClaims_UserId",
|
||||||
|
table: "AspNetUserClaims",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserLogins_UserId",
|
||||||
|
table: "AspNetUserLogins",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AspNetUserRoles_RoleId",
|
||||||
|
table: "AspNetUserRoles",
|
||||||
|
column: "RoleId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "EmailIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedEmail");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "UserNameIndex",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
column: "NormalizedUserName",
|
||||||
|
unique: true,
|
||||||
|
filter: "[NormalizedUserName] IS NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoleClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserLogins");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUserTokens");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetRoles");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
333
ChatbotApi/Migrations/20251118140720_AddConversacionLogTable.Designer.cs
generated
Normal file
333
ChatbotApi/Migrations/20251118140720_AddConversacionLogTable.Designer.cs
generated
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppContexto))]
|
||||||
|
[Migration("20251118140720_AddConversacionLogTable")]
|
||||||
|
partial class AddConversacionLogTable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ContextoItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Clave")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Descripcion")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FechaActualizacion")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Valor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ContextoItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ConversacionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BotRespuesta")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Fecha")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UsuarioMensaje")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ConversacionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddConversacionLogTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ConversacionLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
UsuarioMensaje = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
BotRespuesta = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Fecha = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ConversacionLogs", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ConversacionLogs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
330
ChatbotApi/Migrations/AppContextoModelSnapshot.cs
Normal file
330
ChatbotApi/Migrations/AppContextoModelSnapshot.cs
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ChatbotApi.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppContexto))]
|
||||||
|
partial class AppContextoModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ContextoItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Clave")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Descripcion")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FechaActualizacion")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Valor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ContextoItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ChatbotApi.Data.Models.ConversacionLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BotRespuesta")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Fecha")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("UsuarioMensaje")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ConversacionLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex")
|
||||||
|
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("datetimeoffset");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("nvarchar(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex")
|
||||||
|
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
ChatbotApi/Models/ChatRequest.cs
Normal file
8
ChatbotApi/Models/ChatRequest.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
public class ChatRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public required string Message { get; set; }
|
||||||
|
}
|
||||||
7
ChatbotApi/Models/ChatResponse.cs
Normal file
7
ChatbotApi/Models/ChatResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ChatbotApi.Models
|
||||||
|
{
|
||||||
|
public class ChatResponse
|
||||||
|
{
|
||||||
|
public required string Reply { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
144
ChatbotApi/Program.cs
Normal file
144
ChatbotApi/Program.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using ChatbotApi.Data.Models;
|
||||||
|
using DotNetEnv;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.RateLimiting;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
// Cargar variables de entorno desde el archivo .env
|
||||||
|
Env.Load();
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Definimos una política de CORS para permitir solicitudes desde nuestro frontend de Vite
|
||||||
|
var myAllowSpecificOrigins = "_myAllowSpecificOrigins";
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy(name: myAllowSpecificOrigins,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.WithOrigins("http://localhost:5173", "http://localhost:5174") // La URL de tu frontend
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 1. Añadimos el DbContext para Entity Framework
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
||||||
|
builder.Services.AddDbContext<AppContexto>(options =>
|
||||||
|
options.UseSqlServer(connectionString));
|
||||||
|
|
||||||
|
// 2. Añadimos ASP.NET Core Identity
|
||||||
|
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
||||||
|
{
|
||||||
|
options.Password.RequireDigit = true;
|
||||||
|
options.Password.RequiredLength = 8;
|
||||||
|
options.Password.RequireNonAlphanumeric = false;
|
||||||
|
options.Password.RequireUppercase = true;
|
||||||
|
options.Password.RequireLowercase = false;
|
||||||
|
})
|
||||||
|
.AddEntityFrameworkStores<AppContexto>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
|
// =========== INICIO DE CONFIGURACIÓN JWT ===========
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||||
|
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
|
builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("La clave JWT no está configurada.")
|
||||||
|
))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// RATE LIMITING
|
||||||
|
builder.Services.AddRateLimiter(options =>
|
||||||
|
{
|
||||||
|
options.AddFixedWindowLimiter(policyName: "fixed", limiterOptions =>
|
||||||
|
{
|
||||||
|
limiterOptions.PermitLimit = 10; // Permitir 10 peticiones...
|
||||||
|
limiterOptions.Window = TimeSpan.FromMinutes(1); // ...por cada minuto.
|
||||||
|
limiterOptions.QueueLimit = 2; // Poner en cola hasta 2 peticiones si se excede el límite brevemente.
|
||||||
|
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Esta función se ejecuta cuando una petición es rechazada
|
||||||
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
|
||||||
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Chatbot API", Version = "v1" });
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Por favor, introduce 'Bearer' seguido de un espacio y el token JWT",
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
Scheme = "Bearer"
|
||||||
|
});
|
||||||
|
|
||||||
|
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new string[] {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
// =========== CONFIGURACIÓN Y USO DEL MIDDLEWARE DE ENCABEZADOS DE SEGURIDAD ===========
|
||||||
|
app.UseSecurityHeaders(policy =>
|
||||||
|
{
|
||||||
|
policy.AddDefaultSecurityHeaders(); // Añade los encabezados por defecto
|
||||||
|
policy.AddContentSecurityPolicy(builder =>
|
||||||
|
{
|
||||||
|
builder.AddDefaultSrc().Self();
|
||||||
|
|
||||||
|
// Permisos necesarios para Swagger UI
|
||||||
|
builder.AddScriptSrc().Self().UnsafeInline();
|
||||||
|
builder.AddStyleSrc().Self().UnsafeInline();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.UseCors(myAllowSpecificOrigins);
|
||||||
|
app.UseRateLimiter();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
23
ChatbotApi/Properties/launchSettings.json
Normal file
23
ChatbotApi/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5126",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "https://localhost:7140;http://localhost:5126",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ChatbotApi/appsettings.json
Normal file
20
ChatbotApi/appsettings.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Gemini": {
|
||||||
|
"GeminiApiUrl": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent?alt=sse&key=",
|
||||||
|
"GeminiApiKey": ""
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"Key": "",
|
||||||
|
"Issuer": "",
|
||||||
|
"Audience": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
24
chatbot-admin/.gitignore
vendored
Normal file
24
chatbot-admin/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
73
chatbot-admin/README.md
Normal file
73
chatbot-admin/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
23
chatbot-admin/eslint.config.js
Normal file
23
chatbot-admin/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
12
chatbot-admin/index.html
Normal file
12
chatbot-admin/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>chatbot-admin</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4530
chatbot-admin/package-lock.json
generated
Normal file
4530
chatbot-admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
chatbot-admin/package.json
Normal file
36
chatbot-admin/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "chatbot-admin",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.3.5",
|
||||||
|
"@mui/material": "^7.3.5",
|
||||||
|
"@mui/x-data-grid": "^8.18.0",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.3",
|
||||||
|
"vite": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
chatbot-admin/src/App.css
Normal file
6
chatbot-admin/src/App.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
40
chatbot-admin/src/App.tsx
Normal file
40
chatbot-admin/src/App.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// src/App.tsx
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import AdminPanel from './components/AdminPanel';
|
||||||
|
import Login from './components/Login';
|
||||||
|
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
|
||||||
|
|
||||||
|
const darkTheme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem('jwt_token', token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('jwt_token');
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setToken(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={darkTheme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{token ? (
|
||||||
|
<AdminPanel onLogout={handleLogout} />
|
||||||
|
) : (
|
||||||
|
<Login onLoginSuccess={setToken} />
|
||||||
|
)}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
27
chatbot-admin/src/api/apiClient.ts
Normal file
27
chatbot-admin/src/api/apiClient.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// src/api/apiClient.ts
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Creamos la instancia de Axios
|
||||||
|
const apiClient = axios.create({
|
||||||
|
// Es una buena práctica establecer la URL base aquí
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
// Añadimos el interceptor para inyectar el token JWT en cada petición
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('jwt_token');
|
||||||
|
if (token) {
|
||||||
|
// Aseguramos que la cabecera de autorización se establezca correctamente
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Manejamos errores en la configuración de la petición
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exportamos la instancia configurada para que otros archivos puedan usarla
|
||||||
|
export default apiClient;
|
||||||
46
chatbot-admin/src/components/AdminPanel.tsx
Normal file
46
chatbot-admin/src/components/AdminPanel.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// src/components/AdminPanel.tsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Box, Typography, AppBar, Toolbar, IconButton, Tabs, Tab } from '@mui/material';
|
||||||
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
|
|
||||||
|
// Importamos los dos componentes que mostraremos en las pestañas
|
||||||
|
import ContextManager from './ContextManager'; // Renombraremos el AdminPanel original
|
||||||
|
import LogsViewer from './LogsViewer';
|
||||||
|
|
||||||
|
interface AdminPanelProps {
|
||||||
|
onLogout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// El componente se convierte en un contenedor con pestañas
|
||||||
|
const AdminPanel: React.FC<AdminPanelProps> = ({ onLogout }) => {
|
||||||
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
|
|
||||||
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||||
|
setCurrentTab(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
Panel de Administración del Chatbot
|
||||||
|
</Typography>
|
||||||
|
<IconButton color="inherit" onClick={onLogout} aria-label="Cerrar sesión">
|
||||||
|
<LogoutIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Toolbar>
|
||||||
|
<Tabs value={currentTab} onChange={handleTabChange} textColor="inherit" indicatorColor="secondary">
|
||||||
|
<Tab label="Gestor de Contexto" />
|
||||||
|
<Tab label="Historial de Conversaciones" />
|
||||||
|
</Tabs>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
{/* Mostramos el componente correspondiente a la pestaña activa */}
|
||||||
|
{currentTab === 0 && <ContextManager onAuthError={onLogout} />}
|
||||||
|
{currentTab === 1 && <LogsViewer onAuthError={onLogout} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPanel;
|
||||||
213
chatbot-admin/src/components/ContextManager.tsx
Normal file
213
chatbot-admin/src/components/ContextManager.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
// src/components/AdminPanel.tsx
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid';
|
||||||
|
import type { GridColDef } from '@mui/x-data-grid';
|
||||||
|
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Alert, DialogContentText, TextField } from '@mui/material';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
|
import apiClient from '../api/apiClient';
|
||||||
|
|
||||||
|
interface ContextoItem {
|
||||||
|
id: number;
|
||||||
|
clave: string;
|
||||||
|
valor: string;
|
||||||
|
descripcion: string;
|
||||||
|
fechaActualizacion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContextManagerProps {
|
||||||
|
onAuthError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextManager: React.FC<ContextManagerProps> = ({ onAuthError }) => {
|
||||||
|
const [rows, setRows] = useState<ContextoItem[]>([]);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
|
const [currentRow, setCurrentRow] = useState<Partial<ContextoItem>>({});
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Estado para el diálogo de confirmación de borrado
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [itemToDelete, setItemToDelete] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/api/admin/contexto');
|
||||||
|
setRows(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('No se pudieron cargar los datos.');
|
||||||
|
if (axios.isAxiosError(err) && err.response?.status === 401) {
|
||||||
|
onAuthError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [onAuthError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const handleOpen = (item?: ContextoItem) => {
|
||||||
|
if (item) {
|
||||||
|
setIsEdit(true);
|
||||||
|
setCurrentRow(item);
|
||||||
|
} else {
|
||||||
|
setIsEdit(false);
|
||||||
|
setCurrentRow({ clave: '', valor: '', descripcion: '' });
|
||||||
|
}
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
if (isEdit) {
|
||||||
|
await apiClient.put(`/api/admin/contexto/${currentRow.id}`, currentRow);
|
||||||
|
} else {
|
||||||
|
await apiClient.post('/api/admin/contexto', currentRow);
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
handleClose();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Error al guardar el item.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Abre el diálogo de confirmación
|
||||||
|
const handleDeleteClick = (id: number) => {
|
||||||
|
setItemToDelete(id);
|
||||||
|
setConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cierra el diálogo de confirmación
|
||||||
|
const handleConfirmClose = () => {
|
||||||
|
setConfirmOpen(false);
|
||||||
|
setItemToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ejecuta la eliminación si se confirma
|
||||||
|
const handleConfirmDelete = async () => {
|
||||||
|
if (itemToDelete !== null) {
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/api/admin/contexto/${itemToDelete}`);
|
||||||
|
fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
setError('Error al eliminar el item.');
|
||||||
|
} finally {
|
||||||
|
handleConfirmClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{ field: 'clave', headerName: 'Clave', width: 200 },
|
||||||
|
{ field: 'valor', headerName: 'Valor', width: 350 },
|
||||||
|
{ field: 'descripcion', headerName: 'Descripción', flex: 1 },
|
||||||
|
{
|
||||||
|
field: 'fechaActualizacion',
|
||||||
|
headerName: 'Última Actualización',
|
||||||
|
width: 200,
|
||||||
|
valueGetter: (value) => new Date(value).toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
type: 'actions',
|
||||||
|
width: 100,
|
||||||
|
getActions: (params) => [
|
||||||
|
<GridActionsCellItem
|
||||||
|
icon={<EditIcon />}
|
||||||
|
label="Editar"
|
||||||
|
onClick={() => handleOpen(params.row as ContextoItem)}
|
||||||
|
/>,
|
||||||
|
<GridActionsCellItem
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
label="Eliminar"
|
||||||
|
// Llama a la función que abre el diálogo
|
||||||
|
onClick={() => handleDeleteClick(params.id as number)}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 4 }}>
|
||||||
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||||
|
<Button startIcon={<AddIcon />} variant="contained" onClick={() => handleOpen()} sx={{ mb: 1 }}>
|
||||||
|
Añadir Nuevo Item
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ height: 600, width: '100%' }}>
|
||||||
|
<DataGrid rows={rows} columns={columns} pageSizeOptions={[10, 25, 50, 100]} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 4 }}>
|
||||||
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||||
|
<Button startIcon={<AddIcon />} variant="contained" onClick={() => handleOpen()} sx={{ mb: 1 }}>
|
||||||
|
Añadir Nuevo Item
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ height: 600, width: '100%' }}>
|
||||||
|
<DataGrid rows={rows} columns={columns} pageSizeOptions={[10, 25, 50, 100]} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* --- DIÁLOGO DE EDICIÓN/CREACIÓN --- */}
|
||||||
|
<Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
|
||||||
|
<DialogTitle>{isEdit ? 'Editar Item' : 'Añadir Nuevo Item'}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Clave"
|
||||||
|
fullWidth
|
||||||
|
value={currentRow.clave || ''}
|
||||||
|
disabled={isEdit} // La clave no se puede editar una vez creada
|
||||||
|
onChange={(e) => setCurrentRow({ ...currentRow, clave: e.target.value })}
|
||||||
|
helperText={!isEdit ? "Esta clave no se podrá modificar en el futuro." : ""}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Valor"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
value={currentRow.valor || ''}
|
||||||
|
onChange={(e) => setCurrentRow({ ...currentRow, valor: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Descripción"
|
||||||
|
fullWidth
|
||||||
|
value={currentRow.descripcion || ''}
|
||||||
|
onChange={(e) => setCurrentRow({ ...currentRow, descripcion: e.target.value })}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>Cancelar</Button>
|
||||||
|
<Button onClick={handleSave} variant="contained">Guardar</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* --- DIÁLOGO DE CONFIRMACIÓN DE BORRADO --- */}
|
||||||
|
<Dialog
|
||||||
|
open={confirmOpen}
|
||||||
|
onClose={handleConfirmClose}
|
||||||
|
>
|
||||||
|
<DialogTitle>Confirmar Eliminación</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
¿Estás seguro de que quieres eliminar este item? Esta acción no se puede deshacer.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleConfirmClose}>Cancelar</Button>
|
||||||
|
<Button onClick={handleConfirmDelete} color="error" variant="contained">
|
||||||
|
Eliminar
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextManager;
|
||||||
81
chatbot-admin/src/components/Login.tsx
Normal file
81
chatbot-admin/src/components/Login.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// src/components/Login.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Button, TextField, Typography, Paper, Alert } from '@mui/material';
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
onLoginSuccess: (token: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
|
||||||
|
const [username, setUsername] = React.useState('');
|
||||||
|
const [password, setPassword] = React.useState('');
|
||||||
|
const [error, setError] = React.useState('');
|
||||||
|
|
||||||
|
const handleLogin = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Usamos la variable de entorno para la URL de la API
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
onLoginSuccess(data.token);
|
||||||
|
} else {
|
||||||
|
setError('Usuario o contraseña incorrectos.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('No se pudo conectar con el servidor. Inténtalo de nuevo más tarde.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||||
|
<Paper elevation={6} sx={{ padding: 4, width: '100%', maxWidth: 400 }}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom textAlign="center">
|
||||||
|
Iniciar Sesión
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" textAlign="center" sx={{ mb: 3 }}>
|
||||||
|
Gestor de Contexto del Chatbot
|
||||||
|
</Typography>
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<TextField
|
||||||
|
label="Usuario"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Contraseña"
|
||||||
|
type="password"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
sx={{ mt: 3 }}
|
||||||
|
>
|
||||||
|
Entrar
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
72
chatbot-admin/src/components/LogsViewer.tsx
Normal file
72
chatbot-admin/src/components/LogsViewer.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// src/components/LogsViewer.tsx
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||||
|
import { Box, Typography, Alert } from '@mui/material';
|
||||||
|
import apiClient from '../api/apiClient';
|
||||||
|
|
||||||
|
interface ConversacionLog {
|
||||||
|
id: number;
|
||||||
|
usuarioMensaje: string;
|
||||||
|
botRespuesta: string;
|
||||||
|
fecha: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogsViewerProps {
|
||||||
|
onAuthError: () => void; // Función para desloguear si el token es inválido
|
||||||
|
}
|
||||||
|
|
||||||
|
const LogsViewer: React.FC<LogsViewerProps> = ({ onAuthError }) => {
|
||||||
|
const [logs, setLogs] = useState<ConversacionLog[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchLogs = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/api/admin/logs');
|
||||||
|
setLogs(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
setError('No se pudieron cargar los logs.');
|
||||||
|
if (axios.isAxiosError(err) && err.response?.status === 401) {
|
||||||
|
onAuthError(); // Llamamos a la función de logout si hay un error de autenticación
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [onAuthError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs();
|
||||||
|
}, [fetchLogs]);
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{
|
||||||
|
field: 'fecha',
|
||||||
|
headerName: 'Fecha',
|
||||||
|
width: 200,
|
||||||
|
valueGetter: (value) => new Date(value).toLocaleString(),
|
||||||
|
},
|
||||||
|
{ field: 'usuarioMensaje', headerName: 'Mensaje del Usuario', flex: 1 },
|
||||||
|
{ field: 'botRespuesta', headerName: 'Respuesta del Bot', flex: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Historial de Conversaciones
|
||||||
|
</Typography>
|
||||||
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||||
|
<Box sx={{ height: 700, width: '100%' }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={logs}
|
||||||
|
columns={columns}
|
||||||
|
pageSizeOptions={[25, 50, 100]}
|
||||||
|
initialState={{
|
||||||
|
sorting: {
|
||||||
|
sortModel: [{ field: 'fecha', sort: 'desc' }],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsViewer;
|
||||||
66
chatbot-admin/src/index.css
Normal file
66
chatbot-admin/src/index.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
chatbot-admin/src/main.tsx
Normal file
10
chatbot-admin/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
28
chatbot-admin/tsconfig.app.json
Normal file
28
chatbot-admin/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
chatbot-admin/tsconfig.json
Normal file
7
chatbot-admin/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
chatbot-admin/tsconfig.node.json
Normal file
26
chatbot-admin/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
chatbot-admin/vite.config.ts
Normal file
7
chatbot-admin/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
24
chatbot-widget/.gitignore
vendored
Normal file
24
chatbot-widget/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
73
chatbot-widget/README.md
Normal file
73
chatbot-widget/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
23
chatbot-widget/eslint.config.js
Normal file
23
chatbot-widget/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
13
chatbot-widget/index.html
Normal file
13
chatbot-widget/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>chatbot-widget</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4672
chatbot-widget/package-lock.json
generated
Normal file
4672
chatbot-widget/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
chatbot-widget/package.json
Normal file
32
chatbot-widget/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "chatbot-widget",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"rehype-sanitize": "^6.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.3",
|
||||||
|
"vite": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
chatbot-widget/src/App.css
Normal file
0
chatbot-widget/src/App.css
Normal file
17
chatbot-widget/src/App.tsx
Normal file
17
chatbot-widget/src/App.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Chatbot from './components/Chatbot';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
{/* El contenido de la página web iría aquí */}
|
||||||
|
<h1>Página de ejemplo de El Día</h1>
|
||||||
|
<p>Este es un contenido de demostración para el sitio.</p>
|
||||||
|
|
||||||
|
{/* Aquí integramos nuestro widget de chatbot */}
|
||||||
|
<Chatbot />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
140
chatbot-widget/src/components/Chatbot.css
Normal file
140
chatbot-widget/src/components/Chatbot.css
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/* Estilos para la burbuja flotante del chat */
|
||||||
|
.chat-bubble {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 25px;
|
||||||
|
right: 25px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para la ventana del chat */
|
||||||
|
.chat-window {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 100px;
|
||||||
|
right: 25px;
|
||||||
|
width: 350px;
|
||||||
|
height: 500px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity 0.3s, transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para el encabezado del chat */
|
||||||
|
.chat-header {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.chat-header .close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.chat-header .close-button:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contenedor de mensajes */
|
||||||
|
.messages-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos individuales de los mensajes */
|
||||||
|
.message {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
max-width: 80%;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.bot {
|
||||||
|
background-color: #e9e9eb;
|
||||||
|
color: #333;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Formulario de entrada de texto */
|
||||||
|
.input-form {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-form input {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-form button {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container input {
|
||||||
|
padding-right: 8px; /* Espacio para que no se solape con el botón */
|
||||||
|
width: calc(100% - 28px); /* Ajuste del ancho del input */
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-counter {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #888;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
203
chatbot-widget/src/components/Chatbot.tsx
Normal file
203
chatbot-widget/src/components/Chatbot.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// src/components/Chatbot.tsx
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
import './Chatbot.css';
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
text: string;
|
||||||
|
sender: 'user' | 'bot';
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_CHARS = 200;
|
||||||
|
// Constante para la clave del localStorage
|
||||||
|
const CHAT_HISTORY_KEY = 'chatbot-history';
|
||||||
|
|
||||||
|
const Chatbot: React.FC = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [messages, setMessages] = useState<Message[]>(() => {
|
||||||
|
try {
|
||||||
|
// 1. Intentamos obtener el historial guardado.
|
||||||
|
const savedHistory = localStorage.getItem(CHAT_HISTORY_KEY);
|
||||||
|
if (savedHistory) {
|
||||||
|
// 2. Si existe, lo parseamos y lo devolvemos para usarlo como estado inicial.
|
||||||
|
return JSON.parse(savedHistory);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("No se pudo cargar el historial del chat desde localStorage:", error);
|
||||||
|
}
|
||||||
|
// 3. Si no hay nada guardado o hay un error, devolvemos el estado por defecto.
|
||||||
|
return [{ text: '¡Hola! Soy tu asistente virtual. ¿En qué puedo ayudarte hoy?', sender: 'bot' }];
|
||||||
|
});
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Añadimos un useEffect para guardar los mensajes.
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
// Cada vez que el array de 'messages' cambie, lo guardamos en localStorage.
|
||||||
|
localStorage.setItem(CHAT_HISTORY_KEY, JSON.stringify(messages));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("No se pudo guardar el historial del chat en localStorage:", error);
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Solo intentamos hacer scroll si la ventana del chat está abierta.
|
||||||
|
if (isOpen) {
|
||||||
|
// Usamos un pequeño retardo para asegurar que el navegador haya renderizado
|
||||||
|
// completamente la ventana antes de intentar hacer el scroll.
|
||||||
|
// Esto previene problemas si hay animaciones CSS.
|
||||||
|
setTimeout(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}, [messages, isOpen]);
|
||||||
|
|
||||||
|
const toggleChat = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessage = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (inputValue.trim() === '' || isLoading) return;
|
||||||
|
|
||||||
|
const userMessage: Message = { text: inputValue, sender: 'user' };
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
const messageToSend = inputValue;
|
||||||
|
setInputValue('');
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const botMessagePlaceholder: Message = { text: '', sender: 'bot' };
|
||||||
|
setMessages(prev => [...prev, botMessagePlaceholder]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/chat/stream-message`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ message: messageToSend }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error('Error en la respuesta del servidor.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let accumulatedResponse = ''; // Variable para acumular el texto crudo
|
||||||
|
|
||||||
|
const readStream = async () => {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
// El stream ha terminado, no hacemos nada más aquí.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acumulamos la respuesta cruda que viene del backend
|
||||||
|
accumulatedResponse += decoder.decode(value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Intentamos limpiar la respuesta acumulada
|
||||||
|
// 1. Parseamos como si fuera un array JSON
|
||||||
|
const parsedArray = JSON.parse(accumulatedResponse
|
||||||
|
// Añadimos un corchete de cierre por si el stream se corta a la mitad
|
||||||
|
.replace(/,$/, '') + ']');
|
||||||
|
|
||||||
|
// 2. Unimos los fragmentos del array en un solo texto
|
||||||
|
const cleanText = Array.isArray(parsedArray) ? parsedArray.join('') : accumulatedResponse;
|
||||||
|
|
||||||
|
// 3. Actualizamos el estado con el texto limpio
|
||||||
|
setMessages(prev => {
|
||||||
|
const lastMessage = prev[prev.length - 1];
|
||||||
|
const updatedLastMessage = { ...lastMessage, text: cleanText };
|
||||||
|
return [...prev.slice(0, -1), updatedLastMessage];
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
// Si hay un error de parseo (porque el JSON aún no está completo),
|
||||||
|
// mostramos el texto sin los caracteres iniciales/finales.
|
||||||
|
const partiallyCleanedText = accumulatedResponse
|
||||||
|
.replace(/^\[?"|"?,"?|"?\]$/g, '')
|
||||||
|
.replace(/","/g, '');
|
||||||
|
|
||||||
|
setMessages(prev => {
|
||||||
|
const lastMessage = prev[prev.length - 1];
|
||||||
|
const updatedLastMessage = { ...lastMessage, text: partiallyCleanedText };
|
||||||
|
return [...prev.slice(0, -1), updatedLastMessage];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await readStream();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al conectar con la API de streaming:", error);
|
||||||
|
const errorText = error instanceof Error ? error.message : 'Lo siento, no pude conectarme.';
|
||||||
|
|
||||||
|
setMessages(prev => {
|
||||||
|
const lastMessage = prev[prev.length - 1];
|
||||||
|
const updatedLastMessage = { ...lastMessage, text: errorText };
|
||||||
|
return [...prev.slice(0, -1), updatedLastMessage];
|
||||||
|
});
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="chat-bubble" onClick={toggleChat}>
|
||||||
|
<span>{isOpen ? 'X' : '💬'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="chat-window">
|
||||||
|
<div className="chat-header">
|
||||||
|
<span>Asistente Virtual - El Día</span>
|
||||||
|
<button className="close-button" onClick={toggleChat}>×</button>
|
||||||
|
</div>
|
||||||
|
<div className="messages-container">
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div key={index} className={`message ${msg.sender}`}>
|
||||||
|
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>
|
||||||
|
{msg.text.replace(/\\n/g, "\n")}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
<form className="input-form" onSubmit={handleSendMessage}>
|
||||||
|
<div className="input-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={isLoading ? "Esperando respuesta..." : "Escribe tu consulta..."}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
disabled={isLoading}
|
||||||
|
maxLength={MAX_CHARS}
|
||||||
|
/>
|
||||||
|
<div className="char-counter">
|
||||||
|
{inputValue.length} / {MAX_CHARS}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={isLoading}>
|
||||||
|
{isLoading ? '...' : '→'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Chatbot;
|
||||||
0
chatbot-widget/src/index.css
Normal file
0
chatbot-widget/src/index.css
Normal file
10
chatbot-widget/src/main.tsx
Normal file
10
chatbot-widget/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
28
chatbot-widget/tsconfig.app.json
Normal file
28
chatbot-widget/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
chatbot-widget/tsconfig.json
Normal file
7
chatbot-widget/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
chatbot-widget/tsconfig.node.json
Normal file
26
chatbot-widget/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
chatbot-widget/vite.config.ts
Normal file
7
chatbot-widget/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "ChatBot",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user