Feat: Principio Memoria de Hilo Conversacional

This commit is contained in:
2025-11-21 10:21:34 -03:00
parent 37b97eeb97
commit 1a46f15ec1
3 changed files with 135 additions and 26 deletions

View File

@@ -14,6 +14,7 @@ const MAX_CHARS = 200;
// Constantes para la clave del localStorage
const CHAT_HISTORY_KEY = 'chatbot-history';
const CHAT_CONTEXT_KEY = 'chatbot-active-article';
const CHAT_SUMMARY_KEY = 'chatbot-summary';
const Chatbot: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
@@ -51,6 +52,23 @@ const Chatbot: React.FC = () => {
return null;
});
const [conversationSummary, setConversationSummary] = useState<string>(() => {
try {
return localStorage.getItem(CHAT_SUMMARY_KEY) || "";
} catch (error) {
console.error("No se pudo cargar el resumen de la conversación:", error);
return "";
}
});
useEffect(() => {
try {
localStorage.setItem(CHAT_SUMMARY_KEY, conversationSummary);
} catch (error) {
console.error("No se pudo guardar el resumen de la conversación:", error);
}
}, [conversationSummary]);
// Añadimos un useEffect para guardar los mensajes.
useEffect(() => {
try {
@@ -102,14 +120,14 @@ const Chatbot: React.FC = () => {
const messageToSend = inputValue;
setInputValue('');
// Inicia el estado de carga, pero aún no el de streaming
setIsLoading(true);
setIsStreaming(false);
try {
const requestBody = {
message: messageToSend,
contextUrl: activeArticle ? activeArticle.url : null
contextUrl: activeArticle ? activeArticle.url : null,
conversationSummary: conversationSummary
};
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/chat/stream-message`, {
@@ -133,22 +151,41 @@ const Chatbot: React.FC = () => {
const { done, value } = await reader.read();
if (done) {
// ... (La lógica de `if (done)` no cambia)
try {
const responseArray = JSON.parse(fullReplyRaw);
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 messageChunks: string[] = [];
let finalSummary = conversationSummary;
if (Array.isArray(responseArray)) {
responseArray.forEach((item: string) => {
if (typeof item === 'string') {
if (item.startsWith('INTENT::')) {
intent = item.split('::')[1];
} else if (item.startsWith('SUMMARY::')) {
finalSummary = item.split('::')[1];
} else {
messageChunks.push(item);
}
}
});
}
setConversationSummary(finalSummary);
const finalCleanText = messageChunks.join('');
// 1. Aseguramos que el último mensaje tenga el texto final y 100% limpio.
setMessages(prev => {
const updatedMessages = [...prev];
if (updatedMessages.length > 0) {
updatedMessages[updatedMessages.length - 1].text = finalCleanText;
}
return updatedMessages;
});
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') {
@@ -164,26 +201,37 @@ const Chatbot: React.FC = () => {
const chunk = decoder.decode(value);
fullReplyRaw += chunk;
// --- LÓGICA DE VISUALIZACIÓN EN TIEMPO REAL ---
let cleanTextForDisplay = '';
try {
const parsedArray = JSON.parse(fullReplyRaw.replace(/,$/, '') + ']');
const displayChunks = parsedArray[0] && parsedArray[0].startsWith('INTENT::')
? parsedArray.slice(1)
: parsedArray;
// Filtramos CUALQUIER item que sea una pista interna.
const displayChunks = Array.isArray(parsedArray)
? parsedArray.filter((item: string) =>
typeof item === 'string' && !item.startsWith('INTENT::') && !item.startsWith('SUMMARY::')
)
: [];
cleanTextForDisplay = displayChunks.join('');
} catch (e) {
cleanTextForDisplay = fullReplyRaw.replace(/^\["INTENT::.*?","|\["|"]$|","/g, '');
// El fallback también debe filtrar ambas pistas.
cleanTextForDisplay = fullReplyRaw
.replace(/\"INTENT::.*?\",?/g, '')
.replace(/\"SUMMARY::.*?\",?/g, '')
.replace(/^\["|"]$|","/g, '');
}
if (isFirstChunk) {
// En el primer chunk, activamos el flag de streaming
setIsStreaming(true);
setMessages(prev => [...prev, { text: cleanTextForDisplay, sender: 'bot' }]);
isFirstChunk = false;
} else {
setMessages(prev => {
const updatedMessages = [...prev];
updatedMessages[updatedMessages.length - 1].text = cleanTextForDisplay;
if (updatedMessages.length > 0) {
updatedMessages[updatedMessages.length - 1].text = cleanTextForDisplay;
}
return updatedMessages;
});
}
@@ -197,7 +245,6 @@ const Chatbot: React.FC = () => {
const errorText = error instanceof Error ? error.message : 'Lo siento, no pude conectarme.';
setMessages(prev => [...prev, { text: errorText, sender: 'bot' }]);
} finally {
// Al final, reseteamos ambos estados
setIsLoading(false);
setIsStreaming(false);
}