frontend.html•34.1 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bedrock MCP Agent Conversacional + Glue</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.chat-container {
height: calc(100vh - 200px);
}
</style>
</head>
<body class="bg-gray-100">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// Configuración
const API_BASE_URL = 'http://localhost:5000';
const FIXED_MODEL = 'Claude 3 Sonnet Conversacional';
function BedrockMCPApp() {
const [messages, setMessages] = useState([]);
const [currentMessage, setCurrentMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [serverStatus, setServerStatus] = useState('checking');
const [settings, setSettings] = useState({
temperature: 0.7,
maxTokens: 1000
});
const [showSettings, setShowSettings] = useState(false);
const [showGluePanel, setShowGluePanel] = useState(false);
const [showConversationPanel, setShowConversationPanel] = useState(false);
const [glueStats, setGlueStats] = useState(null);
const [hasGlueIntegration, setHasGlueIntegration] = useState(false);
const [conversationSummary, setConversationSummary] = useState(null);
const [sessionId, setSessionId] = useState('');
const messagesEndRef = useRef(null);
// Auto scroll al final
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// Verificar estado del servidor al cargar
useEffect(() => {
checkServerHealth();
loadGlueStats();
loadConversationSummary();
}, []);
// Verificar salud del servidor
const checkServerHealth = async () => {
try {
const response = await fetch(`${API_BASE_URL}/health`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
setServerStatus(data.status === 'healthy' ? 'connected' : 'error');
setHasGlueIntegration(data.glue_mcp_status === 'initialized');
setSessionId(data.session_id || '');
} else {
setServerStatus('error');
}
} catch (error) {
setServerStatus('offline');
console.error('Error verificando servidor:', error);
}
};
// Cargar estadísticas de Glue
const loadGlueStats = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/glue/stats`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setGlueStats(data.data);
}
}
} catch (error) {
console.error('Error cargando estadísticas de Glue:', error);
}
};
// Cargar resumen de conversación
const loadConversationSummary = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/conversation/summary`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setConversationSummary(data.summary);
}
}
} catch (error) {
console.error('Error cargando resumen de conversación:', error);
}
};
// Enviar mensaje
const sendMessage = async () => {
if (!currentMessage.trim() || isLoading || serverStatus !== 'connected') {
return;
}
const userMessage = {
id: Date.now(),
type: 'user',
content: currentMessage.trim(),
timestamp: new Date().toLocaleTimeString()
};
setMessages(prev => [...prev, userMessage]);
const messageToSend = currentMessage.trim();
setCurrentMessage('');
setIsLoading(true);
try {
const response = await fetch(`${API_BASE_URL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
prompt: messageToSend,
temperature: settings.temperature,
max_tokens: settings.maxTokens
})
});
const data = await response.json();
if (data.success) {
const botMessage = {
id: Date.now() + 1,
type: 'assistant',
content: data.response,
timestamp: new Date().toLocaleTimeString(),
metadata: data.metadata
};
setMessages(prev => [...prev, botMessage]);
// Actualizar resumen de conversación
loadConversationSummary();
} else {
throw new Error(data.error || 'Error desconocido del servidor');
}
} catch (error) {
const errorMessage = {
id: Date.now() + 1,
type: 'error',
content: `❌ Error: ${error.message}`,
timestamp: new Date().toLocaleTimeString()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
// Limpiar conversación
const clearConversation = async () => {
try {
const response = await fetch(`${API_BASE_URL}/api/conversation/clear`, {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
setMessages([]);
setConversationSummary(null);
loadConversationSummary();
}
} catch (error) {
console.error('Error limpiando conversación:', error);
}
};
// Enviar consulta predefinida
const sendQuery = (query) => {
setCurrentMessage(query);
setTimeout(() => {
sendMessage();
}, 100);
};
// Manejar Enter
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
// Componente de estado del servidor
const ServerStatus = () => {
const statusConfig = {
connected: {
color: 'text-green-600',
icon: 'fas fa-circle',
text: 'Conectado',
bg: 'bg-green-100'
},
error: {
color: 'text-yellow-600',
icon: 'fas fa-exclamation-triangle',
text: 'Error AWS',
bg: 'bg-yellow-100'
},
offline: {
color: 'text-red-600',
icon: 'fas fa-times-circle',
text: 'Servidor Off',
bg: 'bg-red-100'
},
checking: {
color: 'text-gray-600',
icon: 'fas fa-spinner fa-spin',
text: 'Verificando...',
bg: 'bg-gray-100'
}
};
const config = statusConfig[serverStatus] || statusConfig.checking;
return (
<div className={`flex items-center space-x-2 px-3 py-1 rounded-full ${config.bg}`}>
<i className={`${config.icon} text-sm ${config.color}`}></i>
<span className={`text-sm font-medium ${config.color}`}>{config.text}</span>
{hasGlueIntegration && (
<span className="text-xs bg-purple-500 text-white px-2 py-1 rounded-full ml-2">
+Glue
</span>
)}
<span className="text-xs bg-blue-500 text-white px-2 py-1 rounded-full ml-1">
💬
</span>
</div>
);
};
// Panel de conversación
const ConversationPanel = () => {
if (!showConversationPanel) return null;
return (
<div className="lg:col-span-1">
<div className="bg-white rounded-xl shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<i className="fas fa-comments mr-2 text-blue-600"></i>
Conversación
</h3>
{conversationSummary && (
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
<div className="text-sm text-blue-800 space-y-1">
<div>💬 {conversationSummary.total_messages} mensajes</div>
<div>🆔 {sessionId}</div>
</div>
</div>
)}
<div className="space-y-2">
<button
onClick={clearConversation}
className="w-full text-left px-3 py-2 text-sm bg-red-50 hover:bg-red-100 rounded-lg transition-colors text-red-700"
>
🧹 Limpiar conversación
</button>
</div>
<div className="mt-4 p-3 bg-green-50 rounded-lg">
<div className="text-xs text-green-800">
💡 <strong>Memoria conversacional:</strong> El asistente recuerda toda nuestra conversación.
</div>
</div>
</div>
</div>
);
};
// Panel de Glue Catalog
const GluePanel = () => {
if (!hasGlueIntegration || !showGluePanel) return null;
return (
<div className="lg:col-span-1">
<div className="bg-white rounded-xl shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<i className="fas fa-database mr-2 text-purple-600"></i>
Glue Catalog
</h3>
{glueStats && (
<div className="mb-4 p-3 bg-purple-50 rounded-lg">
<div className="text-sm text-purple-800">
<div>📊 {glueStats.catalog_stats?.total_databases || 0} bases de datos</div>
<div>📋 {glueStats.catalog_stats?.total_tables || 0} tablas</div>
<div>🌍 {glueStats.region}</div>
</div>
</div>
)}
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Consultas rápidas:</h4>
<button
onClick={() => sendQuery('Lista todas las bases de datos y dame un resumen de lo que contienen')}
className="w-full text-left px-3 py-2 text-sm bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
>
📋 Explorar bases de datos
</button>
<button
onClick={() => sendQuery('Analiza las estadísticas del catálogo y explícame qué tipo de datos tengo disponibles')}
className="w-full text-left px-3 py-2 text-sm bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors"
>
📊 Análisis del catálogo
</button>
</div>
</div>
</div>
);
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Header */}
<div className="bg-white shadow-lg border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 p-3 rounded-xl">
<i className="fas fa-brain text-white text-xl"></i>
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">Bedrock MCP Agent</h1>
<p className="text-sm text-gray-600">
{FIXED_MODEL}
{hasGlueIntegration && (
<span className="ml-2 text-purple-600">+ AWS Glue</span>
)}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<ServerStatus />
<button
onClick={() => setShowConversationPanel(!showConversationPanel)}
className={`p-2 rounded-lg transition-all ${
showConversationPanel
? 'text-blue-700 bg-blue-100'
: 'text-gray-500 hover:text-blue-700 hover:bg-blue-50'
}`}
>
<i className="fas fa-comments"></i>
</button>
{hasGlueIntegration && (
<button
onClick={() => setShowGluePanel(!showGluePanel)}
className={`p-2 rounded-lg transition-all ${
showGluePanel
? 'text-purple-700 bg-purple-100'
: 'text-gray-500 hover:text-purple-700 hover:bg-purple-50'
}`}
>
<i className="fas fa-database"></i>
</button>
)}
<button
onClick={() => setShowSettings(!showSettings)}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-all"
>
<i className="fas fa-cog"></i>
</button>
<button
onClick={clearConversation}
className="p-2 text-gray-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-all"
>
<i className="fas fa-broom"></i>
</button>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 py-6">
<div className="grid grid-cols-1 lg:grid-cols-6 gap-6">
{/* Paneles laterales */}
{showSettings && (
<div className="lg:col-span-1">
<div className="bg-white rounded-xl shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
<i className="fas fa-sliders-h mr-2 text-blue-600"></i>
Configuración
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Temperatura: {settings.temperature}
</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={settings.temperature}
onChange={(e) => setSettings(prev => ({
...prev,
temperature: parseFloat(e.target.value)
}))}
className="w-full accent-blue-600"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tokens Máximos
</label>
<input
type="number"
min="50"
max="4000"
step="50"
value={settings.maxTokens}
onChange={(e) => setSettings(prev => ({
...prev,
maxTokens: parseInt(e.target.value) || 1000
}))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
</div>
)}
<ConversationPanel />
<GluePanel />
{/* Chat principal */}
<div className={`${
[showSettings, showConversationPanel, showGluePanel].filter(Boolean).length === 0 ? "lg:col-span-6" :
[showSettings, showConversationPanel, showGluePanel].filter(Boolean).length === 1 ? "lg:col-span-5" :
[showSettings, showConversationPanel, showGluePanel].filter(Boolean).length === 2 ? "lg:col-span-4" :
"lg:col-span-3"
}`}>
<div className="bg-white rounded-xl shadow-lg chat-container flex flex-col">
{/* Área de mensajes */}
<div className="flex-1 overflow-y-auto p-6">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-lg">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6">
<i className="fas fa-comments text-white text-3xl"></i>
</div>
<h3 className="text-2xl font-bold text-gray-900 mb-3">
¡Conversación inteligente!
</h3>
<p className="text-gray-600 mb-4">
{FIXED_MODEL} con memoria conversacional
{hasGlueIntegration && (
<span className="block text-purple-600 font-medium mt-1">
+ AWS Glue Catalog
</span>
)}
</p>
<div className="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-lg p-4 mb-4">
<ul className="text-sm text-blue-700 text-left space-y-1">
<li>• 🧠 Memoria completa de conversación</li>
<li>• 🔗 Referencias a mensajes anteriores</li>
<li>• 📊 Análisis con datos reales</li>
<li>• 💡 Recomendaciones personalizadas</li>
</ul>
</div>
{serverStatus === 'offline' && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-left">
<p className="text-red-700 text-sm mb-2">
Ejecuta: <code className="bg-gray-800 text-green-400 px-2 py-1 rounded">python app.py</code>
</p>
</div>
)}
</div>
</div>
) : (
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-4xl px-4 py-3 rounded-2xl ${
message.type === 'user'
? 'bg-blue-600 text-white'
: message.type === 'error'
? 'bg-red-50 text-red-800 border border-red-200'
: 'bg-gray-50 text-gray-900 border border-gray-200'
}`}
>
<div className="whitespace-pre-wrap">
{message.content}
</div>
{message.metadata && (
<div className="flex items-center space-x-3 mt-2 pt-2 border-t border-gray-200 text-xs opacity-75">
<span>⏱️ {message.metadata.processing_time_ms}ms</span>
{message.metadata.has_context && (
<span className="text-blue-600">🧠 Contexto</span>
)}
{message.metadata.glue_enhanced && (
<span className="text-purple-600">🗄️ Glue</span>
)}
{message.metadata.conversation_length > 0 && (
<span className="text-green-600">💬 {message.metadata.conversation_length}</span>
)}
</div>
)}
<div className="text-xs opacity-75 mt-2">
{message.timestamp}
</div>
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-50 text-gray-700 px-4 py-3 rounded-2xl border border-gray-200">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-600 border-t-transparent"></div>
<span>Generando respuesta...</span>
</div>
</div>
</div>
)}
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input de mensaje */}
<div className="border-t border-gray-200 p-4">
<div className="flex space-x-3">
<div className="flex-1">
<textarea
value={currentMessage}
onChange={(e) => setCurrentMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={
serverStatus === 'connected'
? "Chatea con memoria conversacional... (Enter para enviar)"
: "Servidor desconectado..."
}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
rows="2"
disabled={isLoading || serverStatus !== 'connected'}
/>
</div>
<button
onClick={sendMessage}
disabled={isLoading || !currentMessage.trim() || serverStatus !== 'connected'}
className="px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center"
>
{isLoading ? (
<i className="fas fa-spinner animate-spin"></i>
) : (
<i className="fas fa-paper-plane"></i>
)}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
// Renderizar la aplicación
ReactDOM.render(<BedrockMCPApp />, document.getElementById('root'));
</script>
</body>
</html>