agent-api / static /chat.html
github-actions[bot]
Sync GitHub snapshot to Hugging Face
9a1014e
Raw
History Blame Contribute Delete
8.12 kB
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Agente de Metalurgia</title>
<style>
:root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; background: #0c1117; color: #e7edf4; }
main { width: min(900px, 100%); height: 100dvh; margin: auto; display: grid; grid-template-rows: auto auto 1fr auto; padding: 20px; gap: 14px; }
h1 { margin: 0; font-size: 1.35rem; }
header p { margin: 5px 0 0; color: #91a0b2; font-size: .9rem; }
.controls { display: grid; grid-template-columns: 1fr 2fr auto; gap: 10px; }
select, textarea, button { font: inherit; }
select, .new-chat { min-height: 42px; border: 1px solid #2b3949; border-radius: 10px; background: #131b24; color: inherit; padding: 0 12px; }
.new-chat { background: #243243; color: inherit; }
#messages { overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding: 8px 2px; }
.message { max-width: 82%; padding: 12px 14px; border-radius: 14px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
.user { align-self: flex-end; background: #176b4d; }
.agent { align-self: flex-start; background: #18212c; border: 1px solid #263444; }
.error { color: #ffb4b4; border-color: #7c3030; }
form { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
textarea { resize: none; min-height: 54px; max-height: 160px; padding: 15px; border: 1px solid #2b3949; border-radius: 13px; background: #131b24; color: inherit; outline: none; }
textarea:focus { border-color: #35a677; }
button { border: 0; border-radius: 13px; padding: 0 22px; background: #2dba83; color: #07130e; font-weight: 700; cursor: pointer; }
button:disabled { opacity: .55; cursor: wait; }
@media (max-width: 560px) { main { padding: 12px; } .controls { grid-template-columns: 1fr; } .message { max-width: 92%; } button { padding: 10px 15px; } }
</style>
</head>
<body>
<main>
<header>
<h1>Agente de Metalurgia</h1>
<p>Conversaciones persistentes con contexto de ASTM E112</p>
</header>
<section class="controls">
<select id="user" aria-label="Usuario"></select>
<select id="conversation" aria-label="Conversación"></select>
<button id="new-chat" class="new-chat" type="button">Nueva conversación</button>
</section>
<section id="messages" aria-live="polite"></section>
<form id="chat-form">
<textarea id="prompt" rows="1" placeholder="Escribí tu consulta…" required autofocus></textarea>
<button id="send" type="submit">Enviar</button>
</form>
</main>
<script>
const form = document.querySelector('#chat-form');
const prompt = document.querySelector('#prompt');
const send = document.querySelector('#send');
const messages = document.querySelector('#messages');
const userSelect = document.querySelector('#user');
const conversationSelect = document.querySelector('#conversation');
const newChat = document.querySelector('#new-chat');
let conversationId = null;
function addMessage(text, type, extra = '') {
const item = document.createElement('div');
item.className = `message ${type} ${extra}`;
item.textContent = text;
messages.appendChild(item);
messages.scrollTop = messages.scrollHeight;
return item;
}
function resetMessages() {
messages.innerHTML = '';
addMessage('Hola. ¿En qué puedo ayudarte?', 'agent');
}
async function loadUsers() {
const response = await fetch('/chat/users');
if (!response.ok) throw new Error('No se pudieron cargar los usuarios');
const users = await response.json();
userSelect.innerHTML = users.map(item => `<option value="${item.id}">${item.username}</option>`).join('');
if (users.length) await loadConversations();
}
async function loadConversations(selectId = null) {
const response = await fetch(`/chat/conversations?user_id=${userSelect.value}`);
if (!response.ok) throw new Error('No se pudieron cargar las conversaciones');
const conversations = await response.json();
conversationSelect.innerHTML = '<option value="">Nueva conversación</option>' + conversations
.map(item => `<option value="${item.id}">${item.title || `Conversación ${item.id}`}</option>`).join('');
conversationSelect.value = selectId ? String(selectId) : '';
conversationId = selectId;
if (selectId) await loadMessages(); else resetMessages();
}
async function loadMessages() {
if (!conversationId) return resetMessages();
const response = await fetch(`/chat/conversations/${conversationId}/messages?user_id=${userSelect.value}`);
if (!response.ok) throw new Error('No se pudo cargar el historial');
const history = await response.json();
messages.innerHTML = '';
history.filter(item => item.role === 'user' || item.role === 'assistant')
.forEach(item => addMessage(item.content, item.role === 'user' ? 'user' : 'agent'));
}
prompt.addEventListener('keydown', event => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
form.requestSubmit();
}
});
userSelect.addEventListener('change', () => loadConversations());
conversationSelect.addEventListener('change', async () => {
conversationId = conversationSelect.value ? Number(conversationSelect.value) : null;
if (conversationId) await loadMessages(); else resetMessages();
});
newChat.addEventListener('click', () => {
conversationId = null;
conversationSelect.value = '';
resetMessages();
prompt.focus();
});
form.addEventListener('submit', async event => {
event.preventDefault();
const text = prompt.value.trim();
if (!text || send.disabled || !userSelect.value) return;
addMessage(text, 'user');
prompt.value = '';
send.disabled = true;
const pending = addMessage('Pensando…', 'agent');
try {
const response = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, user_id: Number(userSelect.value), conversation_id: conversationId })
});
if (!response.ok) throw new Error(`Error HTTP ${response.status}`);
if (!response.body) throw new Error('El navegador no recibio el stream.');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let answer = '';
pending.textContent = 'Conectando con el modelo...';
while (true) {
const { value, done } = await reader.read();
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
const data = JSON.parse(line);
if (data.type === 'metadata') {
if (!conversationId) {
conversationId = data.conversation_id;
await loadConversations(conversationId);
}
} else if (data.type === 'status') {
if (!answer) pending.textContent = data.text;
} else if (data.type === 'delta') {
answer += data.text;
pending.textContent = answer;
} else if (data.type === 'error') {
throw new Error(data.detail || 'Error durante la generacion');
}
}
if (done) break;
}
} catch (error) {
pending.textContent = `No se pudo obtener respuesta: ${error.message}`;
pending.classList.add('error');
} finally {
send.disabled = false;
prompt.focus();
}
});
loadUsers().catch(error => addMessage(error.message, 'agent', 'error'));
</script>
</body>
</html>