Fix Contexto Hilo Conversación
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
// ChatbotApi/Controllers/ChatController.cs
|
// ChatbotApi/Controllers/ChatController.cs
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using ChatbotApi.Models;
|
|
||||||
using ChatbotApi.Data.Models;
|
using ChatbotApi.Data.Models;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -10,6 +9,7 @@ using Microsoft.AspNetCore.RateLimiting;
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
// Clases de Request/Response
|
// Clases de Request/Response
|
||||||
public class GenerationConfig
|
public class GenerationConfig
|
||||||
@@ -33,7 +33,7 @@ public class GeminiResponse { [JsonPropertyName("candidates")] public Candidate[
|
|||||||
public class Candidate { [JsonPropertyName("content")] public Content Content { 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 GeminiStreamingResponse { [JsonPropertyName("candidates")] public StreamingCandidate[] Candidates { get; set; } = default!; }
|
||||||
public class StreamingCandidate { [JsonPropertyName("content")] public Content Content { get; set; } = default!; }
|
public class StreamingCandidate { [JsonPropertyName("content")] public Content Content { get; set; } = default!; }
|
||||||
public enum IntentType { Article, Database, Homepage, ExternalSource }
|
public enum IntentType { Article, KnowledgeBase, Homepage }
|
||||||
|
|
||||||
namespace ChatbotApi.Controllers
|
namespace ChatbotApi.Controllers
|
||||||
{
|
{
|
||||||
@@ -47,6 +47,7 @@ namespace ChatbotApi.Controllers
|
|||||||
private readonly ILogger<ChatController> _logger;
|
private readonly ILogger<ChatController> _logger;
|
||||||
private static readonly HttpClient _httpClient = new HttpClient();
|
private static readonly HttpClient _httpClient = new HttpClient();
|
||||||
private static readonly string _knowledgeCacheKey = "KnowledgeBase";
|
private static readonly string _knowledgeCacheKey = "KnowledgeBase";
|
||||||
|
private static readonly string _fuentesCacheKey = "FuentesDeContexto";
|
||||||
|
|
||||||
private static readonly string _siteUrl = "https://www.eldia.com/";
|
private static readonly string _siteUrl = "https://www.eldia.com/";
|
||||||
private static readonly string[] PrefijosAQuitar = { "VIDEO.- ", "VIDEO. ", "FOTOS.- ", "FOTOS. " };
|
private static readonly string[] PrefijosAQuitar = { "VIDEO.- ", "VIDEO. ", "FOTOS.- ", "FOTOS. " };
|
||||||
@@ -57,9 +58,7 @@ namespace ChatbotApi.Controllers
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cache = memoryCache;
|
_cache = memoryCache;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
var apiKey = configuration["Gemini:GeminiApiKey"]
|
var apiKey = configuration["Gemini:GeminiApiKey"] ?? throw new InvalidOperationException("La API Key de Gemini no está configurada en .env");
|
||||||
?? throw new InvalidOperationException("La API Key de Gemini no está configurada en .env");
|
|
||||||
|
|
||||||
var baseUrl = configuration["Gemini:GeminiApiUrl"];
|
var baseUrl = configuration["Gemini:GeminiApiUrl"];
|
||||||
_apiUrl = $"{baseUrl}{apiKey}";
|
_apiUrl = $"{baseUrl}{apiKey}";
|
||||||
}
|
}
|
||||||
@@ -102,34 +101,14 @@ namespace ChatbotApi.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(IntentType intent, string? data)> GetIntentAsync(string userMessage, string? activeArticleContent, string? conversationSummary, Dictionary<string, ContextoItem> knowledgeBase)
|
private async Task<IntentType> GetIntentAsync(string userMessage, string? activeArticleContent, string? conversationSummary)
|
||||||
{
|
{
|
||||||
var promptBuilder = new StringBuilder();
|
var promptBuilder = new StringBuilder();
|
||||||
promptBuilder.AppendLine("Tu tarea es actuar como un router de intenciones...");
|
promptBuilder.AppendLine("Tu tarea es actuar como un router de intenciones. Basado en la conversación y la pregunta del usuario, elige la categoría de información necesaria. Responde única y exclusivamente con una de las siguientes tres opciones: [ARTICULO_ACTUAL], [BASE_DE_CONOCIMIENTO], [NOTICIAS_PORTADA].");
|
||||||
promptBuilder.AppendLine("- [ARTICULO_ACTUAL]");
|
promptBuilder.AppendLine("\n--- DESCRIPCIÓN DE CATEGORÍAS ---");
|
||||||
promptBuilder.AppendLine("- [NOTICIAS_PORTADA]");
|
promptBuilder.AppendLine("[ARTICULO_ACTUAL]: Si la pregunta es una continuación directa sobre el artículo que se está discutiendo.");
|
||||||
promptBuilder.AppendLine("- [BASE_DE_DATOS:CLAVE_SELECCIONADA]");
|
promptBuilder.AppendLine("[BASE_DE_CONOCIMIENTO]: Si la pregunta es sobre información general del diario (contacto, registro, suscripciones, preguntas frecuentes, etc.).");
|
||||||
|
promptBuilder.AppendLine("[NOTICIAS_PORTADA]: Para preguntas sobre noticias de último momento o eventos actuales.");
|
||||||
// --- LÓGICA DINÁMICA ---
|
|
||||||
List<FuenteContexto> fuentesExternas;
|
|
||||||
using (var scope = _serviceProvider.CreateScope())
|
|
||||||
{
|
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
|
||||||
fuentesExternas = await dbContext.FuentesDeContexto.Where(f => f.Activo).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var fuente in fuentesExternas)
|
|
||||||
{
|
|
||||||
promptBuilder.AppendLine($"[FUENTE_EXTERNA:{fuente.Url}]: Úsala si la pregunta trata sobre: {fuente.DescripcionParaIA}");
|
|
||||||
}
|
|
||||||
|
|
||||||
promptBuilder.AppendLine("\n--- DESCRIPCIÓN DE HERRAMIENTAS ---");
|
|
||||||
promptBuilder.AppendLine("[ARTICULO_ACTUAL]: Úsala si la pregunta es una continuación directa sobre el artículo que se está discutiendo.");
|
|
||||||
promptBuilder.AppendLine("[NOTICIAS_PORTADA]: Úsala para preguntas generales sobre noticias actuales o eventos.");
|
|
||||||
promptBuilder.AppendLine("[BASE_DE_DATOS:CLAVE_SELECCIONADA]: Úsala para preguntas sobre información específica del diario. DEBES reemplazar 'CLAVE_SELECCIONADA' con la clave más relevante de la siguiente lista:");
|
|
||||||
|
|
||||||
var dbKeys = string.Join(", ", knowledgeBase.Keys);
|
|
||||||
promptBuilder.AppendLine($" - Claves disponibles: {dbKeys}");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(conversationSummary))
|
if (!string.IsNullOrWhiteSpace(conversationSummary))
|
||||||
{
|
{
|
||||||
@@ -145,7 +124,7 @@ namespace ChatbotApi.Controllers
|
|||||||
|
|
||||||
promptBuilder.AppendLine("\n--- PREGUNTA DEL USUARIO ---");
|
promptBuilder.AppendLine("\n--- PREGUNTA DEL USUARIO ---");
|
||||||
promptBuilder.AppendLine(userMessage);
|
promptBuilder.AppendLine(userMessage);
|
||||||
promptBuilder.AppendLine("\n--- HERRAMIENTA Y DATOS SELECCIONADOS ---");
|
promptBuilder.AppendLine("\n--- CATEGORÍA SELECCIONADA ---");
|
||||||
|
|
||||||
var finalPrompt = promptBuilder.ToString();
|
var finalPrompt = promptBuilder.ToString();
|
||||||
var requestData = new GeminiRequest { Contents = new[] { new Content { Parts = new[] { new Part { Text = finalPrompt } } } } };
|
var requestData = new GeminiRequest { Contents = new[] { new Content { Parts = new[] { new Part { Text = finalPrompt } } } } };
|
||||||
@@ -154,36 +133,24 @@ namespace ChatbotApi.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData);
|
var response = await _httpClient.PostAsJsonAsync(nonStreamingApiUrl, requestData);
|
||||||
if (!response.IsSuccessStatusCode) return (IntentType.Homepage, null);
|
if (!response.IsSuccessStatusCode) return IntentType.Homepage;
|
||||||
|
|
||||||
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>();
|
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>();
|
||||||
var responseText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim() ?? "";
|
var responseText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text?.Trim() ?? "";
|
||||||
|
|
||||||
_logger.LogInformation("Intención y datos detectados: {Response}", responseText);
|
_logger.LogInformation("Intención detectada: {Intent}", responseText);
|
||||||
|
|
||||||
if (responseText.Contains("ARTICULO_ACTUAL")) return (IntentType.Article, null);
|
if (responseText.Contains("ARTICULO_ACTUAL")) return IntentType.Article;
|
||||||
if (responseText.Contains("NOTICIAS_PORTADA")) return (IntentType.Homepage, null);
|
if (responseText.Contains("BASE_DE_CONOCIMIENTO")) return IntentType.KnowledgeBase;
|
||||||
if (responseText.Contains("BASE_DE_DATOS:"))
|
return IntentType.Homepage;
|
||||||
{
|
|
||||||
var key = responseText.Split(new[] { ':' }, 2)[1].TrimEnd(']');
|
|
||||||
return (IntentType.Database, key);
|
|
||||||
}
|
|
||||||
if (responseText.Contains("FUENTE_EXTERNA:"))
|
|
||||||
{
|
|
||||||
var url = responseText.Split(new[] { ':' }, 2, StringSplitOptions.None)[1].TrimEnd(']');
|
|
||||||
return (IntentType.ExternalSource, url); // Necesitaremos un nuevo IntentType
|
|
||||||
}
|
|
||||||
|
|
||||||
return (IntentType.Homepage, null);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Excepción en GetIntentAsync. Usando fallback a Homepage.");
|
_logger.LogError(ex, "Excepción en GetIntentAsync. Usando fallback a Homepage.");
|
||||||
return (IntentType.Homepage, null);
|
return IntentType.Homepage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("stream-message")]
|
[HttpPost("stream-message")]
|
||||||
[EnableRateLimiting("fixed")]
|
[EnableRateLimiting("fixed")]
|
||||||
public async IAsyncEnumerable<string> StreamMessage(
|
public async IAsyncEnumerable<string> StreamMessage(
|
||||||
@@ -210,11 +177,7 @@ namespace ChatbotApi.Controllers
|
|||||||
articleContext = await GetArticleContentAsync(request.ContextUrl);
|
articleContext = await GetArticleContentAsync(request.ContextUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
var knowledgeBase = await GetKnowledgeAsync();
|
intent = await GetIntentAsync(userMessage, articleContext, request.ConversationSummary);
|
||||||
|
|
||||||
// --- CORRECCIÓN 2: El código que llama a GetIntentAsync debe esperar una tupla ---
|
|
||||||
var (detectedIntent, intentData) = await GetIntentAsync(userMessage, articleContext, request.ConversationSummary, knowledgeBase);
|
|
||||||
intent = detectedIntent;
|
|
||||||
|
|
||||||
switch (intent)
|
switch (intent)
|
||||||
{
|
{
|
||||||
@@ -224,24 +187,27 @@ namespace ChatbotApi.Controllers
|
|||||||
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene el texto completo de una noticia.";
|
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene el texto completo de una noticia.";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IntentType.Database:
|
case IntentType.KnowledgeBase:
|
||||||
// --- CORRECCIÓN 3: La lógica aquí debe manejar la clave recibida ---
|
_logger.LogInformation("Ejecutando intención: Base de Conocimiento Unificada.");
|
||||||
_logger.LogInformation("Ejecutando intención: Base de Datos con clave '{Key}'.", intentData);
|
var contextBuilder = new StringBuilder();
|
||||||
if (intentData != null && knowledgeBase.TryGetValue(intentData, out var dbItem))
|
contextBuilder.AppendLine("Usa la siguiente base de conocimiento para responder la pregunta del usuario:");
|
||||||
|
|
||||||
|
var knowledgeBaseItems = await GetKnowledgeItemsAsync();
|
||||||
|
foreach (var item in knowledgeBaseItems.Values)
|
||||||
{
|
{
|
||||||
// Ahora dbItem es un objeto ContextoItem, no un string.
|
contextBuilder.AppendLine($"- TEMA: {item.Descripcion}\n INFORMACIÓN: {item.Valor}");
|
||||||
var dbContextBuilder = new StringBuilder();
|
|
||||||
dbContextBuilder.AppendLine("Aquí tienes la información solicitada:");
|
|
||||||
dbContextBuilder.AppendLine($"- PREGUNTA: {dbItem.Descripcion}");
|
|
||||||
dbContextBuilder.AppendLine($" RESPUESTA: {dbItem.Valor}");
|
|
||||||
context = dbContextBuilder.ToString();
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
var fuentesExternas = await GetFuentesDeContextoAsync();
|
||||||
|
foreach (var fuente in fuentesExternas)
|
||||||
{
|
{
|
||||||
context = "No se encontró información relevante para la clave solicitada.";
|
contextBuilder.AppendLine($"\n--- Información de la página '{fuente.Nombre}' ---");
|
||||||
_logger.LogWarning("La clave '{Key}' devuelta por la IA no es válida.", intentData);
|
string scrapedContent = await ScrapeUrlContentAsync(fuente.Url);
|
||||||
|
contextBuilder.AppendLine(scrapedContent);
|
||||||
}
|
}
|
||||||
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene la pregunta y respuesta encontrada.";
|
|
||||||
|
context = contextBuilder.ToString();
|
||||||
|
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en la 'BASE DE CONOCIMIENTO' proporcionada.";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case IntentType.Homepage:
|
case IntentType.Homepage:
|
||||||
@@ -250,14 +216,6 @@ namespace ChatbotApi.Controllers
|
|||||||
context = await GetWebsiteNewsAsync(_siteUrl, 25);
|
context = await GetWebsiteNewsAsync(_siteUrl, 25);
|
||||||
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene una lista de noticias de portada.";
|
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene una lista de noticias de portada.";
|
||||||
break;
|
break;
|
||||||
case IntentType.ExternalSource:
|
|
||||||
_logger.LogInformation("Ejecutando intención: Fuente Externa con URL '{Url}'.", intentData);
|
|
||||||
if (!string.IsNullOrEmpty(intentData))
|
|
||||||
{
|
|
||||||
context = await ScrapeUrlContentAsync(intentData);
|
|
||||||
}
|
|
||||||
promptInstructions = "Tu tarea es responder la 'PREGUNTA DEL USUARIO' basándote ESTRICTA Y ÚNICAMENTE en el 'CONTEXTO' que contiene el texto de una página web.";
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -394,8 +352,6 @@ namespace ChatbotApi.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<string> GetWebsiteNewsAsync(string url, int cantidad)
|
private async Task<string> GetWebsiteNewsAsync(string url, int cantidad)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -470,25 +426,34 @@ namespace ChatbotApi.Controllers
|
|||||||
return textoDecodificado;
|
return textoDecodificado;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, ContextoItem>> GetKnowledgeAsync()
|
private async Task<Dictionary<string, ContextoItem>> GetKnowledgeItemsAsync()
|
||||||
{
|
{
|
||||||
return await _cache.GetOrCreateAsync(_knowledgeCacheKey, async entry =>
|
return await _cache.GetOrCreateAsync(_knowledgeCacheKey, async entry =>
|
||||||
{
|
{
|
||||||
_logger.LogInformation("La caché de conocimiento no existe o ha expirado. Recargando desde la base de datos...");
|
_logger.LogInformation("Cargando ContextoItems desde la base de datos a la caché...");
|
||||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||||
using (var scope = _serviceProvider.CreateScope())
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
{
|
{
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
||||||
// Usamos ToDictionaryAsync para obtener el objeto ContextoItem completo.
|
return await dbContext.ContextoItems.AsNoTracking().ToDictionaryAsync(item => item.Clave, item => item);
|
||||||
var knowledge = await dbContext.ContextoItems
|
|
||||||
.AsNoTracking()
|
|
||||||
.ToDictionaryAsync(item => item.Clave, item => item);
|
|
||||||
_logger.LogInformation($"Caché actualizada con {knowledge.Count} items.");
|
|
||||||
return knowledge;
|
|
||||||
}
|
}
|
||||||
}) ?? new Dictionary<string, ContextoItem>();
|
}) ?? new Dictionary<string, ContextoItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<FuenteContexto>> GetFuentesDeContextoAsync()
|
||||||
|
{
|
||||||
|
return await _cache.GetOrCreateAsync(_fuentesCacheKey, async entry =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Cargando FuentesDeContexto desde la base de datos a la caché...");
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
||||||
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppContexto>();
|
||||||
|
return await dbContext.FuentesDeContexto.Where(f => f.Activo).AsNoTracking().ToListAsync();
|
||||||
|
}
|
||||||
|
}) ?? new List<FuenteContexto>();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string?> GetArticleContentAsync(string url)
|
private async Task<string?> GetArticleContentAsync(string url)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -526,24 +491,27 @@ namespace ChatbotApi.Controllers
|
|||||||
|
|
||||||
private async Task<string> ScrapeUrlContentAsync(string url)
|
private async Task<string> ScrapeUrlContentAsync(string url)
|
||||||
{
|
{
|
||||||
// Usamos la URL como clave de caché para no scrapear la misma página una y otra vez.
|
return await _cache.GetOrCreateAsync($"scrape_{url}", async entry =>
|
||||||
return await _cache.GetOrCreateAsync(url, async entry =>
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Contenido de {Url} no encontrado en caché. Scrapeando...", url);
|
_logger.LogInformation("Contenido de {Url} no encontrado en caché. Scrapeando...", url);
|
||||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); // Cachear por 1 hora
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
var web = new HtmlWeb();
|
var web = new HtmlWeb();
|
||||||
var doc = await web.LoadFromWebAsync(url);
|
var doc = await web.LoadFromWebAsync(url);
|
||||||
|
|
||||||
// Selector genérico que intenta obtener el contenido principal
|
|
||||||
// Esto puede necesitar ajustes dependiendo de la estructura de las páginas
|
|
||||||
var mainContentNode = doc.DocumentNode.SelectSingleNode("//main") ?? doc.DocumentNode.SelectSingleNode("//body");
|
var mainContentNode = doc.DocumentNode.SelectSingleNode("//main") ?? doc.DocumentNode.SelectSingleNode("//body");
|
||||||
|
|
||||||
if (mainContentNode == null) return string.Empty;
|
if (mainContentNode == null) return string.Empty;
|
||||||
|
|
||||||
// Podríamos hacer esto mucho más inteligente, buscando <p>, <h2>, <li>, etc.
|
// Extraer texto de etiquetas comunes de contenido
|
||||||
// pero para empezar, InnerText es un buen punto de partida.
|
var textNodes = mainContentNode.SelectNodes(".//p | .//h1 | .//h2 | .//h3 | .//li");
|
||||||
return WebUtility.HtmlDecode(mainContentNode.InnerText);
|
if (textNodes == null) return WebUtility.HtmlDecode(mainContentNode.InnerText);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var node in textNodes)
|
||||||
|
{
|
||||||
|
sb.AppendLine(WebUtility.HtmlDecode(node.InnerText).Trim());
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
}) ?? string.Empty;
|
}) ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const SourceManager: React.FC<SourceManagerProps> = ({ onAuthError }) => {
|
|||||||
rows={3}
|
rows={3}
|
||||||
value={currentRow.descripcionParaIA || ''}
|
value={currentRow.descripcionParaIA || ''}
|
||||||
onChange={(e) => setCurrentRow({ ...currentRow, descripcionParaIA: e.target.value })}
|
onChange={(e) => setCurrentRow({ ...currentRow, descripcionParaIA: e.target.value })}
|
||||||
helperText="¡Crucial! Describe en una frase para qué sirve esta fuente. Ej: 'Usar para responder preguntas sobre cómo registrarse, iniciar sesión o por qué es obligatorio el registro'."
|
helperText="¡Crucial! Describe en una frase para qué sirve esta fuente. Ej: 'Usar para responder preguntas sobre cómo registrarse, iniciar sesión o sobre el registro'."
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
|
|||||||
Reference in New Issue
Block a user