Spaces:
Running
Running
File size: 8,120 Bytes
9a1014e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | <!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>
|