agentchat/index.html

373 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>agentchat</title>
<style>
:root {
--bg: #0a0a0a;
--surface: #141414;
--border: #222;
--text: #e0e0e0;
--muted: #666;
--accent: #4a9eff;
--accent2: #7c5cff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro", "Segoe UI", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100dvh;
max-width: 720px;
margin: 0 auto;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
padding-top: max(12px, env(safe-area-inset-top));
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
header h1 { font-size: 17px; font-weight: 600; letter-spacing: -0.3px; }
header .dot {
width: 8px; height: 8px; border-radius: 50%;
background: #4caf50; margin-right: 8px; display: inline-block;
}
#status { font-size: 12px; color: var(--muted); }
#messages {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px;
-webkit-overflow-scrolling: touch;
}
.msg {
margin-bottom: 8px;
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } }
.msg .meta { font-size: 11px; color: var(--muted); margin-bottom: 2px; }
.msg .meta .name { font-weight: 600; margin-right: 6px; }
.msg .meta .name.human { color: #6fcf97; }
.msg .meta .name.agent { color: #b794f6; }
.msg .meta .name.system { color: #888; }
.msg .bubble {
display: inline-block; max-width: 100%;
padding: 8px 12px; border-radius: 14px;
font-size: 15px; line-height: 1.4;
word-wrap: break-word; white-space: pre-wrap;
}
.msg.self .bubble {
background: var(--accent); color: #fff;
border-bottom-right-radius: 4px; float: right;
}
.msg.self { text-align: right; }
.msg.self::after { content: ''; display: block; clear: both; }
.msg.other .bubble {
background: var(--surface); border: 1px solid var(--border);
border-bottom-left-radius: 4px;
}
.msg.agent-msg .bubble {
background: #1a1530; border: 1px solid #2d2450;
border-bottom-left-radius: 4px;
}
.msg.system-msg .bubble {
background: #1a1a1a; font-style: italic;
font-size: 13px; color: var(--muted);
}
.msg.status-msg .bubble {
background: transparent; border: 1px solid #333;
font-size: 13px; color: #8a8;
border-radius: 8px; padding: 4px 10px;
}
/* Thinking indicator */
#thinking-bar {
padding: 0 16px;
font-size: 12px;
color: var(--accent2);
height: 0;
overflow: hidden;
transition: height 0.2s, padding 0.2s;
}
#thinking-bar.active {
height: 22px;
padding: 3px 16px;
}
@keyframes pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
#thinking-bar span { animation: pulse 1.5s infinite; }
/* Input area */
#input-area {
display: flex; flex-direction: column; gap: 0;
padding: 8px 12px;
padding-bottom: max(8px, env(safe-area-inset-bottom));
background: var(--surface);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
#agent-bar {
display: flex; gap: 6px;
padding: 4px 0 8px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
#agent-bar::-webkit-scrollbar { display: none; }
.agent-btn {
flex-shrink: 0; padding: 4px 12px;
border-radius: 16px; border: 1px solid var(--border);
background: transparent; color: var(--muted);
font-size: 13px; cursor: pointer; transition: all 0.15s;
position: relative;
}
.agent-btn:hover { border-color: var(--accent); color: var(--text); }
.agent-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.agent-btn .thinking-dot {
display: none;
width: 6px; height: 6px; border-radius: 50%;
background: var(--accent2);
position: absolute; top: -2px; right: -2px;
animation: pulse 1s infinite;
}
.agent-btn.is-thinking .thinking-dot { display: block; }
#compose { display: flex; gap: 8px; align-items: flex-end; }
#msg-input {
flex: 1; background: var(--bg);
border: 1px solid var(--border); border-radius: 20px;
padding: 10px 16px; color: var(--text);
font-size: 15px; outline: none; resize: none;
max-height: 120px; line-height: 1.4;
}
#msg-input::placeholder { color: var(--muted); }
#msg-input:focus { border-color: var(--accent); }
#send-btn {
width: 40px; height: 40px; border-radius: 50%;
border: none; background: var(--accent); color: #fff;
font-size: 18px; cursor: pointer; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
transition: opacity 0.15s;
}
#send-btn:active { opacity: 0.7; }
#login {
position: fixed; inset: 0; background: var(--bg);
display: flex; align-items: center; justify-content: center; z-index: 100;
}
#login.hidden { display: none; }
#login-box { text-align: center; padding: 32px; }
#login-box h2 { font-size: 24px; margin-bottom: 24px; font-weight: 600; }
#login-box input {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 12px 20px; color: var(--text);
font-size: 17px; width: 260px; text-align: center; outline: none;
}
#login-box input:focus { border-color: var(--accent); }
#login-box button {
display: block; width: 260px; margin-top: 12px;
padding: 12px; border-radius: 12px; border: none;
background: var(--accent); color: #fff;
font-size: 16px; font-weight: 600; cursor: pointer;
}
</style>
</head>
<body>
<div id="login">
<div id="login-box">
<h2>agentchat</h2>
<input type="text" id="name-input" placeholder="Your name" autocomplete="off" autofocus>
<button onclick="doLogin()">Enter</button>
</div>
</div>
<div id="app">
<header>
<div><span class="dot"></span><h1 style="display:inline">agentchat</h1></div>
<span id="status">connecting...</span>
</header>
<div id="messages"></div>
<div id="thinking-bar"><span></span></div>
<div id="input-area">
<div id="agent-bar">
<button class="agent-btn active" data-to="" onclick="selectAgent(this)">All</button>
</div>
<div id="compose">
<textarea id="msg-input" rows="1" placeholder="Message..." enterkeyhint="send"></textarea>
<button id="send-btn" onclick="send()">&#9654;</button>
</div>
</div>
</div>
<script>
const AGENTS = {james: 'James', mira: 'Mira', hans: 'Hans'};
const thinkingAgents = new Set();
let ws, username, selectedAgent = '';
const bar = document.getElementById('agent-bar');
for (const [id, name] of Object.entries(AGENTS)) {
const btn = document.createElement('button');
btn.className = 'agent-btn';
btn.dataset.to = id;
btn.innerHTML = `${name}<span class="thinking-dot"></span>`;
btn.onclick = function() { selectAgent(this); };
bar.appendChild(btn);
}
function selectAgent(btn) {
document.querySelectorAll('.agent-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedAgent = btn.dataset.to;
document.getElementById('msg-input').placeholder =
selectedAgent ? `Message ${AGENTS[selectedAgent]}...` : 'Message...';
document.getElementById('msg-input').focus();
}
function updateThinkingBar() {
const bar = document.getElementById('thinking-bar');
if (thinkingAgents.size === 0) {
bar.classList.remove('active');
bar.querySelector('span').textContent = '';
} else {
const names = [...thinkingAgents].join(', ');
bar.classList.add('active');
bar.querySelector('span').textContent = `${names} thinking...`;
}
// Update button dots
document.querySelectorAll('.agent-btn[data-to]').forEach(btn => {
const agentName = AGENTS[btn.dataset.to];
btn.classList.toggle('is-thinking', agentName && thinkingAgents.has(agentName));
});
}
function doLogin() {
const name = document.getElementById('name-input').value.trim();
if (!name) return;
username = name;
localStorage.setItem('agentchat-user', name);
document.getElementById('login').classList.add('hidden');
connect();
}
const saved = localStorage.getItem('agentchat-user');
if (saved) {
username = saved;
document.getElementById('login').classList.add('hidden');
connect();
}
document.getElementById('name-input').addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}/ws?user=${encodeURIComponent(username)}`);
ws.onopen = () => {
document.getElementById('status').textContent = 'connected';
document.querySelector('.dot').style.background = '#4caf50';
};
ws.onclose = () => {
document.getElementById('status').textContent = 'reconnecting...';
document.querySelector('.dot').style.background = '#ff5252';
setTimeout(connect, 2000);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
handleMessage(msg);
};
}
function handleMessage(msg) {
if (msg.kind === 'thinking') {
thinkingAgents.add(msg.user);
updateThinkingBar();
return;
}
if (msg.kind === 'thinking-done') {
thinkingAgents.delete(msg.user);
updateThinkingBar();
return;
}
// Agent replied — clear thinking
if (Object.values(AGENTS).includes(msg.user)) {
thinkingAgents.delete(msg.user);
updateThinkingBar();
}
appendMessage(msg);
}
function appendMessage(msg) {
const el = document.createElement('div');
const isSelf = msg.user === username;
const isAgent = Object.values(AGENTS).includes(msg.user);
const isSystem = msg.user === 'system';
const isStatus = msg.kind === 'status';
el.className = 'msg';
if (isStatus) el.classList.add('status-msg');
else if (isSelf) el.classList.add('self');
else if (isAgent) el.classList.add('agent-msg');
else if (isSystem) el.classList.add('system-msg');
else el.classList.add('other');
const nameClass = isAgent ? 'agent' : isSystem ? 'system' : 'human';
el.innerHTML = `
<div class="meta">
<span class="name ${nameClass}">${esc(msg.user)}</span>
<span>${esc(msg.timestamp)}</span>
</div>
<div class="bubble">${esc(msg.text)}</div>
`;
const cont = document.getElementById('messages');
const atBottom = cont.scrollTop + cont.clientHeight >= cont.scrollHeight - 60;
cont.appendChild(el);
if (atBottom) cont.scrollTop = cont.scrollHeight;
}
function send() {
const input = document.getElementById('msg-input');
const text = input.value.trim();
if (!text || !ws || ws.readyState !== 1) return;
ws.send(JSON.stringify({ text, to: selectedAgent }));
input.value = '';
input.style.height = 'auto';
input.focus();
}
const input = document.getElementById('msg-input');
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
function esc(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
</script>
</body>
</html>