Feat: Ajustes y Preparación Docker
This commit is contained in:
@@ -48,7 +48,7 @@ namespace ChatbotApi.Controllers
|
|||||||
var baseUrl = configuration["Gemini:GeminiApiUrl"];
|
var baseUrl = configuration["Gemini:GeminiApiUrl"];
|
||||||
_apiUrl = $"{baseUrl}{apiKey}";
|
_apiUrl = $"{baseUrl}{apiKey}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IntentType> GetIntentAsync(string userMessage, string? activeArticleContent)
|
private async Task<IntentType> GetIntentAsync(string userMessage, string? activeArticleContent)
|
||||||
{
|
{
|
||||||
var promptBuilder = new StringBuilder();
|
var promptBuilder = new StringBuilder();
|
||||||
@@ -92,12 +92,12 @@ namespace ChatbotApi.Controllers
|
|||||||
return IntentType.Homepage;
|
return IntentType.Homepage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("stream-message")]
|
[HttpPost("stream-message")]
|
||||||
[EnableRateLimiting("fixed")]
|
[EnableRateLimiting("fixed")]
|
||||||
public async IAsyncEnumerable<string> StreamMessage(
|
public async IAsyncEnumerable<string> StreamMessage(
|
||||||
[FromBody] ChatRequest request,
|
[FromBody] ChatRequest request,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(request?.Message))
|
if (string.IsNullOrWhiteSpace(request?.Message))
|
||||||
{
|
{
|
||||||
@@ -111,6 +111,9 @@ namespace ChatbotApi.Controllers
|
|||||||
string? articleContext = null;
|
string? articleContext = null;
|
||||||
string? errorMessage = null;
|
string? errorMessage = null;
|
||||||
|
|
||||||
|
// --- CORRECCIÓN: Declarar e inicializar 'intent' aquí ---
|
||||||
|
IntentType intent = IntentType.Homepage; // Default fallback
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(request.ContextUrl))
|
if (!string.IsNullOrEmpty(request.ContextUrl))
|
||||||
@@ -118,7 +121,8 @@ namespace ChatbotApi.Controllers
|
|||||||
articleContext = await GetArticleContentAsync(request.ContextUrl);
|
articleContext = await GetArticleContentAsync(request.ContextUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
IntentType intent = await GetIntentAsync(userMessage, articleContext);
|
// Ya no se declara con "IntentType intent", solo se le asigna el valor.
|
||||||
|
intent = await GetIntentAsync(userMessage, articleContext);
|
||||||
|
|
||||||
switch (intent)
|
switch (intent)
|
||||||
{
|
{
|
||||||
@@ -138,7 +142,7 @@ namespace ChatbotApi.Controllers
|
|||||||
case IntentType.Homepage:
|
case IntentType.Homepage:
|
||||||
default:
|
default:
|
||||||
_logger.LogInformation("Ejecutando intención: Noticias de Portada.");
|
_logger.LogInformation("Ejecutando intención: Noticias de Portada.");
|
||||||
context = await GetWebsiteNewsAsync(_siteUrl, 15);
|
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. Si encuentras una noticia relevante, proporciona su enlace en formato Markdown: '[título](URL)'.";
|
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. Si encuentras una noticia relevante, proporciona su enlace en formato Markdown: '[título](URL)'.";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -151,6 +155,9 @@ namespace ChatbotApi.Controllers
|
|||||||
promptInstructions = string.Empty;
|
promptInstructions = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ahora 'intent' es accesible aquí.
|
||||||
|
yield return $"INTENT::{intent}";
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(errorMessage))
|
if (!string.IsNullOrEmpty(errorMessage))
|
||||||
{
|
{
|
||||||
yield return errorMessage;
|
yield return errorMessage;
|
||||||
@@ -267,7 +274,7 @@ namespace ChatbotApi.Controllers
|
|||||||
_logger.LogError(logEx, "Error al guardar el log de la conversación después del streaming.");
|
_logger.LogError(logEx, "Error al guardar el log de la conversación después del streaming.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> FindBestDbItemAsync(string userMessage, Dictionary<string, string> knowledgeBase)
|
private async Task<string?> FindBestDbItemAsync(string userMessage, Dictionary<string, string> knowledgeBase)
|
||||||
{
|
{
|
||||||
if (knowledgeBase == null || !knowledgeBase.Any()) return null;
|
if (knowledgeBase == null || !knowledgeBase.Any()) return null;
|
||||||
|
|||||||
32
ChatbotApi/Dockerfile.api
Normal file
32
ChatbotApi/Dockerfile.api
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Dockerfile.api
|
||||||
|
|
||||||
|
# ---- Etapa de Compilación (Build) ----
|
||||||
|
# Usamos la imagen del SDK de .NET para compilar la aplicación
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copiamos los archivos .csproj y restauramos las dependencias primero
|
||||||
|
# Esto aprovecha el cache de Docker para acelerar futuras compilaciones
|
||||||
|
COPY ["ChatbotApi.csproj", "."]
|
||||||
|
RUN dotnet restore "./ChatbotApi.csproj"
|
||||||
|
|
||||||
|
# Copiamos el resto del código fuente y construimos la aplicación
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/."
|
||||||
|
RUN dotnet build "ChatbotApi.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
# Publicamos la aplicación en modo Release
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "ChatbotApi.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
# ---- Etapa Final (Runtime) ----
|
||||||
|
# Usamos la imagen de runtime de ASP.NET, que es mucho más ligera
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
|
||||||
|
# Exponemos el puerto 80, el puerto estándar para HTTP dentro del contenedor
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# El comando para iniciar la aplicación cuando el contenedor se ejecute
|
||||||
|
ENTRYPOINT ["dotnet", "ChatbotApi.dll"]
|
||||||
@@ -20,7 +20,8 @@ builder.Services.AddCors(options =>
|
|||||||
options.AddPolicy(name: myAllowSpecificOrigins,
|
options.AddPolicy(name: myAllowSpecificOrigins,
|
||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins("http://localhost:5173", "http://localhost:5174") // La URL de tu frontend
|
//policy.WithOrigins("http://localhost:5173", "http://localhost:5174")
|
||||||
|
policy.WithOrigins("http://192.168.5.129:8081", "http://192.168.5.129:8082")
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowAnyMethod();
|
.AllowAnyMethod();
|
||||||
});
|
});
|
||||||
|
|||||||
21
chatbot-admin/Dockerfile.admin
Normal file
21
chatbot-admin/Dockerfile.admin
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Dockerfile.admin
|
||||||
|
|
||||||
|
# ---- Etapa de Compilación (Build) ----
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
# Definimos la URL de la API para el panel de admin
|
||||||
|
RUN VITE_API_BASE_URL=http://192.168.5.129:8080 npm run build
|
||||||
|
|
||||||
|
# ---- Etapa Final (Runtime) ----
|
||||||
|
FROM nginx:alpine AS final
|
||||||
|
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
15
chatbot-admin/nginx.conf
Normal file
15
chatbot-admin/nginx.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# nginx.conf
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Si un archivo o directorio no existe, redirige al index.html
|
||||||
|
# Esto es esencial para que el enrutamiento del lado del cliente de React funcione.
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,25 @@ const LogsViewer: React.FC<LogsViewerProps> = ({ onAuthError }) => {
|
|||||||
field: 'fecha',
|
field: 'fecha',
|
||||||
headerName: 'Fecha',
|
headerName: 'Fecha',
|
||||||
width: 200,
|
width: 200,
|
||||||
valueGetter: (value) => new Date(value).toLocaleString(),
|
valueGetter: (value) => {
|
||||||
|
// 1. Le decimos a JavaScript que la fecha que viene de la DB es UTC añadiendo una 'Z'.
|
||||||
|
const date = new Date(value + 'Z');
|
||||||
|
|
||||||
|
// 2. Definimos las opciones de formato.
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
timeZone: 'America/Argentina/Buenos_Aires',
|
||||||
|
hour12: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Ahora la conversión será correcta.
|
||||||
|
return date.toLocaleString('es-AR', options);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ field: 'usuarioMensaje', headerName: 'Mensaje del Usuario', flex: 1 },
|
{ field: 'usuarioMensaje', headerName: 'Mensaje del Usuario', flex: 1 },
|
||||||
{ field: 'botRespuesta', headerName: 'Respuesta del Bot', flex: 1 },
|
{ field: 'botRespuesta', headerName: 'Respuesta del Bot', flex: 1 },
|
||||||
|
|||||||
25
chatbot-widget/Dockerfile.widget
Normal file
25
chatbot-widget/Dockerfile.widget
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Dockerfile.widget
|
||||||
|
|
||||||
|
# ---- Etapa de Compilación (Build) ----
|
||||||
|
FROM node:24-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
# IMPORTANTE: Aquí definimos la URL de la API para producción
|
||||||
|
# El docker-compose se encargará de exponer la API en el puerto 8080 del host
|
||||||
|
RUN VITE_API_BASE_URL=http://192.168.5.129:8080 npm run build
|
||||||
|
|
||||||
|
# ---- Etapa Final (Runtime) ----
|
||||||
|
FROM nginx:alpine AS final
|
||||||
|
|
||||||
|
# Copiamos los archivos estáticos construidos en la etapa anterior
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copiamos nuestra configuración personalizada de Nginx
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
15
chatbot-widget/nginx.conf
Normal file
15
chatbot-widget/nginx.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# nginx.conf
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Si un archivo o directorio no existe, redirige al index.html
|
||||||
|
# Esto es esencial para que el enrutamiento del lado del cliente de React funcione.
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -150,4 +150,40 @@ opacity: 0.8;
|
|||||||
|
|
||||||
.context-indicator span {
|
.context-indicator span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- INICIO DE LA ANIMACIÓN DE "ESCRIBIENDO" --- */
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 0; /* Espacio vertical para que no se pegue a los bordes */
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
background-color: #999;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
animation: typing-bounce 1.4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aplicamos un pequeño retardo a cada punto para crear el efecto de onda */
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-bounce {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ interface Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAX_CHARS = 200;
|
const MAX_CHARS = 200;
|
||||||
// Constante para la clave del localStorage
|
// Constantes para la clave del localStorage
|
||||||
const CHAT_HISTORY_KEY = 'chatbot-history';
|
const CHAT_HISTORY_KEY = 'chatbot-history';
|
||||||
|
const CHAT_CONTEXT_KEY = 'chatbot-active-article';
|
||||||
|
|
||||||
const Chatbot: React.FC = () => {
|
const Chatbot: React.FC = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -33,8 +34,22 @@ const Chatbot: React.FC = () => {
|
|||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
||||||
const [activeArticleUrl, setActiveArticleUrl] = useState<string | null>(null);
|
const [activeArticle, setActiveArticle] = useState<{ url: string; title: string; } | null>(() => {
|
||||||
|
try {
|
||||||
|
// 1. Intentamos obtener el contexto del artículo guardado.
|
||||||
|
const savedContext = localStorage.getItem(CHAT_CONTEXT_KEY);
|
||||||
|
if (savedContext) {
|
||||||
|
// 2. Si existe, lo parseamos y lo usamos como estado inicial.
|
||||||
|
return JSON.parse(savedContext);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("No se pudo cargar el contexto del artículo desde localStorage:", error);
|
||||||
|
}
|
||||||
|
// 3. Si no hay nada guardado o hay un error, el estado inicial es null.
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
// Añadimos un useEffect para guardar los mensajes.
|
// Añadimos un useEffect para guardar los mensajes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -46,6 +61,19 @@ const Chatbot: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
if (activeArticle) {
|
||||||
|
// Si hay un artículo activo, lo guardamos en localStorage.
|
||||||
|
localStorage.setItem(CHAT_CONTEXT_KEY, JSON.stringify(activeArticle));
|
||||||
|
} else {
|
||||||
|
// Si el artículo activo es null, lo eliminamos de localStorage para mantenerlo limpio.
|
||||||
|
localStorage.removeItem(CHAT_CONTEXT_KEY);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("No se pudo guardar el contexto del artículo en localStorage:", error);
|
||||||
|
}
|
||||||
|
}, [activeArticle]); // Wste efecto se ejecuta cada vez que 'activeArticle' cambia.
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Solo intentamos hacer scroll si la ventana del chat está abierta.
|
// Solo intentamos hacer scroll si la ventana del chat está abierta.
|
||||||
@@ -73,15 +101,15 @@ const Chatbot: React.FC = () => {
|
|||||||
setMessages(prev => [...prev, userMessage]);
|
setMessages(prev => [...prev, userMessage]);
|
||||||
const messageToSend = inputValue;
|
const messageToSend = inputValue;
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const botMessagePlaceholder: Message = { text: '', sender: 'bot' };
|
// Inicia el estado de carga, pero aún no el de streaming
|
||||||
setMessages(prev => [...prev, botMessagePlaceholder]);
|
setIsLoading(true);
|
||||||
|
setIsStreaming(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
message: messageToSend,
|
message: messageToSend,
|
||||||
contextUrl: activeArticleUrl
|
contextUrl: activeArticle ? activeArticle.url : null
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/chat/stream-message`, {
|
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/chat/stream-message`, {
|
||||||
@@ -98,52 +126,67 @@ const Chatbot: React.FC = () => {
|
|||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
const readStream = async () => {
|
const readStream = async () => {
|
||||||
let fullReply = '';
|
let fullReplyRaw = '';
|
||||||
|
let isFirstChunk = true;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
let finalCleanText = '';
|
// ... (La lógica de `if (done)` no cambia)
|
||||||
try {
|
try {
|
||||||
const parsedArray = JSON.parse(fullReply.replace(/,$/, '') + ']');
|
const responseArray = JSON.parse(fullReplyRaw);
|
||||||
finalCleanText = Array.isArray(parsedArray) ? parsedArray.join('') : fullReply;
|
let intent = 'Homepage';
|
||||||
|
let messageChunks = [];
|
||||||
|
if (Array.isArray(responseArray) && responseArray.length > 0) {
|
||||||
|
if (typeof responseArray[0] === 'string' && responseArray[0].startsWith('INTENT::')) {
|
||||||
|
intent = responseArray[0].split('::')[1];
|
||||||
|
messageChunks = responseArray.slice(1);
|
||||||
|
} else {
|
||||||
|
messageChunks = responseArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const finalCleanText = messageChunks.join('');
|
||||||
|
const linkRegex = /\[(.*?)\]\((https?:\/\/[^\s]+)\)/;
|
||||||
|
const match = finalCleanText.match(linkRegex);
|
||||||
|
if (match && match[1] && match[2]) {
|
||||||
|
setActiveArticle({ title: match[1], url: match[2] });
|
||||||
|
} else if (intent === 'Database' || intent === 'Homepage') {
|
||||||
|
setActiveArticle(null);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
finalCleanText = fullReply.replace(/^\["|"]$|","/g, '');
|
console.error("Error al procesar la respuesta final del stream:", e, "Contenido crudo:", fullReplyRaw);
|
||||||
|
setActiveArticle(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkRegex = /\[.*?\]\((https?:\/\/[^\s]+)\)/;
|
|
||||||
const match = finalCleanText.match(linkRegex);
|
|
||||||
|
|
||||||
// --- INICIO DE LA CORRECCIÓN ---
|
|
||||||
// Si encontramos un nuevo enlace, actualizamos el contexto.
|
|
||||||
// Si NO encontramos un enlace, ya no hacemos nada, permitiendo que el contexto anterior persista.
|
|
||||||
if (match && match[1]) {
|
|
||||||
console.log("Noticia activa establecida:", match[1]);
|
|
||||||
setActiveArticleUrl(match[1]);
|
|
||||||
}
|
|
||||||
// HEMOS ELIMINADO EL BLOQUE "ELSE" QUE RESETEABA EL CONTEXTO.
|
|
||||||
// --- FIN DE LA CORRECCIÓN ---
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (el resto del bucle while sigue exactamente igual)
|
|
||||||
const chunk = decoder.decode(value);
|
const chunk = decoder.decode(value);
|
||||||
fullReply += chunk;
|
fullReplyRaw += chunk;
|
||||||
|
|
||||||
let cleanText = '';
|
let cleanTextForDisplay = '';
|
||||||
try {
|
try {
|
||||||
const parsedArray = JSON.parse(fullReply.replace(/,$/, '') + ']');
|
const parsedArray = JSON.parse(fullReplyRaw.replace(/,$/, '') + ']');
|
||||||
cleanText = Array.isArray(parsedArray) ? parsedArray.join('') : fullReply;
|
const displayChunks = parsedArray[0] && parsedArray[0].startsWith('INTENT::')
|
||||||
|
? parsedArray.slice(1)
|
||||||
|
: parsedArray;
|
||||||
|
cleanTextForDisplay = displayChunks.join('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cleanText = fullReply.replace(/^\["|"]$|","/g, '');
|
cleanTextForDisplay = fullReplyRaw.replace(/^\["INTENT::.*?","|\["|"]$|","/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev => {
|
if (isFirstChunk) {
|
||||||
const lastMessage = prev[prev.length - 1];
|
// En el primer chunk, activamos el flag de streaming
|
||||||
const updatedLastMessage = { ...lastMessage, text: cleanText };
|
setIsStreaming(true);
|
||||||
return [...prev.slice(0, -1), updatedLastMessage];
|
setMessages(prev => [...prev, { text: cleanTextForDisplay, sender: 'bot' }]);
|
||||||
});
|
isFirstChunk = false;
|
||||||
|
} else {
|
||||||
|
setMessages(prev => {
|
||||||
|
const updatedMessages = [...prev];
|
||||||
|
updatedMessages[updatedMessages.length - 1].text = cleanTextForDisplay;
|
||||||
|
return updatedMessages;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,15 +195,11 @@ const Chatbot: React.FC = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error al conectar con la API de streaming:", error);
|
console.error("Error al conectar con la API de streaming:", error);
|
||||||
const errorText = error instanceof Error ? error.message : 'Lo siento, no pude conectarme.';
|
const errorText = error instanceof Error ? error.message : 'Lo siento, no pude conectarme.';
|
||||||
|
setMessages(prev => [...prev, { text: errorText, sender: 'bot' }]);
|
||||||
setMessages(prev => {
|
|
||||||
const lastMessage = prev[prev.length - 1];
|
|
||||||
const updatedLastMessage = { ...lastMessage, text: errorText };
|
|
||||||
return [...prev.slice(0, -1), updatedLastMessage];
|
|
||||||
});
|
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
// Al final, reseteamos ambos estados
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
setIsStreaming(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +215,7 @@ const Chatbot: React.FC = () => {
|
|||||||
<span>Asistente Virtual - El Día</span>
|
<span>Asistente Virtual - El Día</span>
|
||||||
<button className="close-button" onClick={toggleChat}>×</button>
|
<button className="close-button" onClick={toggleChat}>×</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="messages-container">
|
<div className={`messages-container ${isLoading ? 'is-loading' : ''}`}>
|
||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<div key={index} className={`message ${msg.sender}`}>
|
<div key={index} className={`message ${msg.sender}`}>
|
||||||
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>
|
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>
|
||||||
@@ -184,11 +223,23 @@ const Chatbot: React.FC = () => {
|
|||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{isLoading && !isStreaming && (
|
||||||
|
<div className="message bot">
|
||||||
|
<div className="typing-indicator">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
{activeArticleUrl && (
|
{activeArticle && (
|
||||||
<div className="context-indicator">
|
<div className="context-indicator">
|
||||||
Hablando sobre: <span>Noticia actual</span>
|
Hablando sobre:
|
||||||
|
<a href={activeArticle.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{activeArticle.title}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form className="input-form" onSubmit={handleSendMessage}>
|
<form className="input-form" onSubmit={handleSendMessage}>
|
||||||
|
|||||||
40
docker-compose.yml
Normal file
40
docker-compose.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
services:
|
||||||
|
chatbot-api:
|
||||||
|
build:
|
||||||
|
context: ./ChatbotApi
|
||||||
|
dockerfile: Dockerfile.api
|
||||||
|
container_name: chatbot-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:80" # Mapea el puerto 80 del contenedor al 8080 de tu servidor Docker
|
||||||
|
environment:
|
||||||
|
# --- Variables de Entorno para la API ---
|
||||||
|
# Docker Compose las leerá automáticamente desde el archivo .env
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- ConnectionStrings__DefaultConnection=${DB_CONNECTION_STRING}
|
||||||
|
- Gemini__GeminiApiKey=${GEMINI_API_KEY}
|
||||||
|
- Jwt__Key=${JWT_KEY}
|
||||||
|
- Jwt__Issuer=${JWT_ISSUER}
|
||||||
|
- Jwt__Audience=${JWT_AUDIENCE}
|
||||||
|
|
||||||
|
chatbot-widget:
|
||||||
|
build:
|
||||||
|
context: ./chatbot-widget
|
||||||
|
dockerfile: Dockerfile.widget
|
||||||
|
container_name: chatbot-widget
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8081:80" # Mapea el puerto 80 del contenedor Nginx al 8081 del host
|
||||||
|
|
||||||
|
chatbot-admin:
|
||||||
|
build:
|
||||||
|
context: ./chatbot-admin
|
||||||
|
dockerfile: Dockerfile.admin
|
||||||
|
container_name: chatbot-admin
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8082:80" # Mapea el puerto 80 del contenedor Nginx al 8082 del host
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
Reference in New Issue
Block a user