Files
claude_session/playground/web/index.html
T
agent 1a8415950c feat(playground): chat web sobre claude_session (caliente, con memoria + restart)
Backend Go (web/server.go) que lanza UN claude_session al boot y lo mantiene
vivo: por mensaje escribe {cmd:send} a su stdin y reenvia los eventos NDJSON
(text_delta + result) como SSE. Endpoint /restart -> {cmd:restart}. Frontend
vanilla (web/index.html) con boton Nueva conversacion.

A diferencia de los chats de pipe/wire (proceso nuevo por mensaje), este reusa
la sesion -> memoria entre turnos + ~2.7s/mensaje. Validado end-to-end:
recuerda un dato (42), lo recupera en el turno siguiente, y restart limpia la
memoria. Puerto 8101.
2026-06-04 20:46:46 +02:00

154 lines
6.6 KiB
HTML

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>claude_session · chat caliente</title>
<style>
:root {
--bg: #14100a; --panel: #1a150d; --border: #2c2516;
--user: #3a2c10; --assist: #1c170f; --text: #efe9df;
--muted: #9a8c70; --accent: #f59e0b;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: var(--bg); color: var(--text);
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex; flex-direction: column; height: 100vh;
}
header {
padding: 12px 18px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 10px; background: var(--panel);
}
header .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 8px var(--accent); }
header h1 { font-size: 14px; margin: 0; font-weight: 600; }
header .sub { font-size: 12px; color: var(--muted); margin-left: auto; }
header button.restart {
background: transparent; color: var(--muted); border: 1px solid var(--border);
border-radius: 8px; padding: 4px 10px; font-size: 12px; cursor: pointer;
}
header button.restart:hover { color: var(--text); border-color: var(--accent); }
#log { flex: 1; overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; }
.msg { max-width: 760px; padding: 10px 14px; border-radius: 12px; white-space: pre-wrap; word-wrap: break-word; }
.msg.user { align-self: flex-end; background: var(--user); border: 1px solid #5a4316; }
.msg.assistant { align-self: flex-start; background: var(--assist); border: 1px solid var(--border); font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 14px; }
.msg.assistant.pending::after { content: "▋"; color: var(--accent); animation: blink 1s steps(2) infinite; }
.msg .role { font-size: 11px; color: var(--muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: .5px; }
.msg.err { align-self: center; background: #3a1620; border: 1px solid #7f1d1d; color: #fecaca; font-size: 13px; }
.msg.sysline { align-self: center; background: transparent; color: var(--muted); font-size: 12px; padding: 2px; }
@keyframes blink { 50% { opacity: 0; } }
form { display: flex; gap: 10px; padding: 14px 18px; border-top: 1px solid var(--border); background: var(--panel); }
#prompt {
flex: 1; resize: none; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; font: inherit;
}
#prompt:focus { outline: none; border-color: var(--accent); }
button.send {
background: var(--accent); color: #1a1206; border: none; border-radius: 10px;
padding: 0 18px; font-weight: 600; cursor: pointer;
}
button.send:disabled { opacity: .5; cursor: default; }
.hint { font-size: 11px; color: var(--muted); padding: 0 18px 10px; background: var(--panel); }
</style>
</head>
<body>
<header>
<span class="dot"></span>
<h1>claude_session · chat caliente</h1>
<button class="restart" id="restart" title="Empezar una conversación nueva">↻ Nueva conversación</button>
<span class="sub">sesión viva · con memoria · ~2.7s/msg</span>
</header>
<div id="log">
<div class="msg assistant">
<div class="role">sistema</div>Sesión claude caliente y persistente. Mantiene contexto entre mensajes (pruébalo: pregunta algo y luego refiérete a ello). Cada respuesta llega del SSE de la red, exacta, en ~2-3s. "Nueva conversación" reinicia la sesión sin memoria.
</div>
</div>
<form id="form">
<textarea id="prompt" rows="2" placeholder="Escribe tu mensaje… (Enter envía, Shift+Enter salto de línea)" autofocus></textarea>
<button class="send" id="send" type="submit">Enviar</button>
</form>
<div class="hint">Un único daemon claude_session vivo detrás. Mensajes secuenciales. La memoria persiste hasta que reinicies.</div>
<script>
const log = document.getElementById('log');
const form = document.getElementById('form');
const promptEl = document.getElementById('prompt');
const sendBtn = document.getElementById('send');
const restartBtn = document.getElementById('restart');
let busy = false;
function addMsg(role, text, cls) {
const div = document.createElement('div');
div.className = 'msg ' + (cls || role);
if (cls !== 'sysline') {
const r = document.createElement('div');
r.className = 'role'; r.textContent = role; div.appendChild(r);
}
div.appendChild(document.createTextNode(text || ''));
log.appendChild(div);
log.scrollTop = log.scrollHeight;
return div;
}
function setBody(div, text) {
while (div.childNodes.length > 1) div.removeChild(div.lastChild);
div.appendChild(document.createTextNode(text));
log.scrollTop = log.scrollHeight;
}
form.addEventListener('submit', (e) => { e.preventDefault(); send(); });
promptEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
restartBtn.addEventListener('click', restart);
function send() {
if (busy) return;
const prompt = promptEl.value.trim();
if (!prompt) return;
addMsg('tú', prompt, 'user');
promptEl.value = '';
setBusy(true);
const bubble = addMsg('claude', '', 'assistant');
bubble.classList.add('pending');
let acc = '';
const es = new EventSource('/chat?prompt=' + encodeURIComponent(prompt));
es.onmessage = (ev) => {
let obj; try { obj = JSON.parse(ev.data); } catch { return; }
if (obj.type === 'text_delta') { acc += obj.text || ''; setBody(bubble, acc); }
else if (obj.type === 'result') { if (obj.result) { acc = obj.result; setBody(bubble, acc); } }
else if (obj.type === 'error') { setBody(bubble, '⚠ ' + (obj.message || 'error')); }
};
es.addEventListener('done', () => { bubble.classList.remove('pending'); if (!acc) setBody(bubble, '(vacío)'); es.close(); setBusy(false); });
es.addEventListener('error', () => { bubble.classList.remove('pending'); es.close(); setBusy(false); });
es.onerror = () => { if (busy) { bubble.classList.remove('pending'); es.close(); setBusy(false); } };
}
async function restart() {
if (busy) return;
setBusy(true);
addMsg('', '↻ reiniciando sesión…', 'sysline');
try {
await fetch('/restart', { method: 'POST' });
log.innerHTML = '';
addMsg('', '— conversación nueva (sin memoria) —', 'sysline');
} catch (e) {
addMsg('error', 'fallo al reiniciar', 'err');
}
setBusy(false);
}
function setBusy(b) {
busy = b;
sendBtn.disabled = b; restartBtn.disabled = b;
sendBtn.textContent = b ? '…' : 'Enviar';
if (!b) promptEl.focus();
}
</script>
</body>
</html>