RS_Agent / index.html
ntphuc149's picture
Update index.html
13cb35a verified
Raw
History Blame Contribute Delete
18.6 kB
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Research Agent UI</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #22263a;
--border: #2e3348;
--accent: #5c7cfa;
--accent2: #38d9a9;
--warn: #f6a623;
--error: #ff6b6b;
--text: #e2e8f0;
--muted: #7a849e;
--tool-bg: #0d1f2d;
--result-bg: #0d2218;
--round-bg: #1c1a2e;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Header ── */
header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
header h1 { font-size: 15px; font-weight: 600; color: var(--accent); letter-spacing: .3px; }
header span { font-size: 12px; color: var(--muted); }
#resetBtn {
background: none; border: 1px solid var(--border); color: var(--muted);
border-radius: 6px; padding: 5px 12px; font-size: 12px; cursor: pointer;
transition: border-color .15s, color .15s;
}
#resetBtn:hover { border-color: var(--error); color: var(--error); }
/* ── Main layout ── */
main {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Left: chat ── */
#chatPane {
width: 420px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
}
#messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.msg {
max-width: 88%;
padding: 10px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.msg.user {
align-self: flex-end;
background: var(--accent);
color: #fff;
border-bottom-right-radius: 3px;
}
.msg.assistant {
align-self: flex-start;
background: var(--surface2);
border-bottom-left-radius: 3px;
}
.msg.system-note {
align-self: center;
background: none;
color: var(--muted);
font-size: 12px;
font-style: italic;
}
.msg.waiting {
align-self: flex-start;
background: var(--surface2);
color: var(--warn);
font-size: 13px;
border-left: 3px solid var(--warn);
}
/* typing indicator */
.typing { display: flex; gap: 5px; align-items: center; padding: 12px 16px; }
.typing span {
width: 7px; height: 7px; border-radius: 50%;
background: var(--muted); animation: bounce .9s infinite;
}
.typing span:nth-child(2) { animation-delay: .15s; }
.typing span:nth-child(3) { animation-delay: .30s; }
@keyframes bounce {
0%,80%,100% { transform: translateY(0); }
40% { transform: translateY(-6px); }
}
/* input row */
#inputRow {
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--border);
background: var(--surface);
}
#userInput {
flex: 1;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 9px 12px;
color: var(--text);
font-size: 14px;
resize: none;
outline: none;
line-height: 1.4;
max-height: 120px;
overflow-y: auto;
}
#userInput:focus { border-color: var(--accent); }
#sendBtn {
background: var(--accent);
border: none;
border-radius: 8px;
color: #fff;
padding: 9px 16px;
font-size: 14px;
cursor: pointer;
transition: opacity .15s;
align-self: flex-end;
}
#sendBtn:disabled { opacity: .4; cursor: not-allowed; }
/* ── Right: debug panel ── */
#debugPane {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#debugHeader {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface);
flex-shrink: 0;
}
#debugHeader h2 { font-size: 13px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
#turnBadge {
font-size: 11px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 4px;
padding: 2px 8px;
color: var(--muted);
}
#debugScroll {
flex: 1;
overflow-y: auto;
padding: 14px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--muted);
font-size: 13px;
gap: 8px;
text-align: center;
}
.empty-state svg { opacity: .3; }
/* round block */
.round-block {
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
background: var(--round-bg);
}
.round-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: var(--surface2);
border-bottom: 1px solid var(--border);
font-size: 12px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .4px;
}
.round-pill {
background: var(--accent);
color: #fff;
border-radius: 20px;
padding: 1px 8px;
font-size: 11px;
font-weight: 700;
}
/* assistant thinking text */
.think-block {
padding: 10px 14px;
font-size: 13px;
color: var(--muted);
font-style: italic;
border-bottom: 1px solid var(--border);
}
/* tool call card */
.tool-call-card {
margin: 10px 12px 0;
border-radius: 8px;
overflow: hidden;
border: 1px solid #1e3a5f;
}
.tool-call-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--tool-bg);
cursor: pointer;
user-select: none;
}
.tool-name {
font-size: 13px;
font-weight: 600;
color: var(--accent2);
font-family: 'Fira Code', monospace;
}
.tool-arrow { font-size: 11px; color: var(--muted); margin-left: auto; transition: transform .2s; }
.tool-arrow.open { transform: rotate(90deg); }
.tool-args {
padding: 10px 12px;
background: #091826;
border-top: 1px solid #1e3a5f;
}
.tool-args pre {
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 12px;
color: #93c5fd;
white-space: pre-wrap;
word-break: break-all;
}
/* tool result card */
.tool-result-card {
margin: 6px 12px 12px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #1a3d2b;
}
.tool-result-header {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
background: var(--result-bg);
cursor: pointer;
user-select: none;
}
.result-label { font-size: 12px; font-weight: 600; color: var(--accent2); opacity: .7; }
.result-badge-ok {
font-size: 10px; background: #1a4d38; color: var(--accent2);
border-radius: 4px; padding: 1px 6px;
}
.result-badge-err {
font-size: 10px; background: #4d1a1a; color: var(--error);
border-radius: 4px; padding: 1px 6px;
}
.result-badge-wait {
font-size: 10px; background: #4d3a1a; color: var(--warn);
border-radius: 4px; padding: 1px 6px;
}
.tool-result-body {
padding: 10px 12px;
background: #060f09;
border-top: 1px solid #1a3d2b;
}
.tool-result-body pre {
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 12px;
color: #86efac;
white-space: pre-wrap;
word-break: break-all;
max-height: 260px;
overflow-y: auto;
}
/* status banner */
.status-banner {
margin: 0 0 4px;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.status-answered { background: #1a3d28; color: var(--accent2); }
.status-waiting { background: #3d2e0a; color: var(--warn); }
.status-error { background: #3d0a0a; color: var(--error); }
.status-max { background: #2a2a3d; color: var(--muted); }
/* scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
</style>
</head>
<body>
<header>
<h1>🔬 Research Agent UI</h1>
<span id="modelLabel">openrouter · gpt-4o-mini</span>
<button id="resetBtn">↺ Reset</button>
</header>
<main>
<!-- ── Chat pane ── -->
<div id="chatPane">
<div id="messages">
<div class="msg system-note">Agent sẵn sàng. Hãy thử hỏi gì đó!</div>
</div>
<div id="inputRow">
<textarea id="userInput" rows="1" placeholder="Nhập câu hỏi… (Enter để gửi)"></textarea>
<button id="sendBtn">Gửi</button>
</div>
</div>
<!-- ── Debug pane ── -->
<div id="debugPane">
<div id="debugHeader">
<h2>Tool Execution Trace</h2>
<span id="turnBadge"></span>
</div>
<div id="debugScroll">
<div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>
<span>Gửi tin nhắn để xem<br>tool calls xuất hiện tại đây</span>
</div>
</div>
</div>
</main>
<script>
// ── Backend URL ───────────────────────────────────────────────────────────────
// When hosting this file on HF Spaces (or any static host), set BACKEND_URL to
// your ngrok / tunnel URL (e.g. "https://abc123.ngrok-free.app").
// Leave as empty string "" to use the same origin (local dev).
const BACKEND_URL = "https://simon-detector-easy-architectural.trycloudflare.com;
const messagesEl = document.getElementById('messages');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const debugScroll = document.getElementById('debugScroll');
const turnBadge = document.getElementById('turnBadge');
let turnCount = 0;
// ── helpers ──────────────────────────────────────────────────────────────────
function scrollBottom(el) { el.scrollTop = el.scrollHeight; }
function addMessage(text, role) {
const div = document.createElement('div');
div.className = `msg ${role}`;
div.textContent = text;
messagesEl.appendChild(div);
scrollBottom(messagesEl);
return div;
}
function showTyping() {
const d = document.createElement('div');
d.className = 'typing'; d.id = 'typingDot';
d.innerHTML = '<span></span><span></span><span></span>';
messagesEl.appendChild(d);
scrollBottom(messagesEl);
}
function hideTyping() {
document.getElementById('typingDot')?.remove();
}
function prettyJson(obj) {
return JSON.stringify(obj, null, 2);
}
function toggleBody(header, bodyId, arrowEl) {
const body = document.getElementById(bodyId);
if (!body) return;
const open = body.style.display !== 'none';
body.style.display = open ? 'none' : 'block';
if (arrowEl) arrowEl.classList.toggle('open', !open);
}
// ── render debug trace ────────────────────────────────────────────────────────
function renderDebug(data) {
debugScroll.innerHTML = '';
turnCount++;
turnBadge.textContent = `Turn ${turnCount}`;
// status banner
const banner = document.createElement('div');
const sMap = {
answered: ['status-answered', '✅ answered'],
waiting_for_user: ['status-waiting', '⏳ waiting for user'],
max_tool_rounds: ['status-max', '⚠️ max tool rounds reached'],
error: ['status-error', '❌ error'],
};
const [cls, label] = sMap[data.status] || ['status-error', data.status];
banner.className = `status-banner ${cls}`;
banner.textContent = label;
debugScroll.appendChild(banner);
// rounds
if (!data.rounds || data.rounds.length === 0) {
const note = document.createElement('div');
note.className = 'empty-state';
note.style.height = 'auto';
note.style.paddingTop = '24px';
note.textContent = 'No tool calls — agent answered directly.';
debugScroll.appendChild(note);
return;
}
data.rounds.forEach((round, ri) => {
const block = document.createElement('div');
block.className = 'round-block';
const rHeader = document.createElement('div');
rHeader.className = 'round-header';
rHeader.innerHTML = `<span class="round-pill">Round ${round.round}</span>
<span>${round.tool_calls?.length || 0} tool call(s)</span>`;
block.appendChild(rHeader);
// assistant thinking text
if (round.assistant_text) {
const think = document.createElement('div');
think.className = 'think-block';
think.textContent = `💭 ${round.assistant_text}`;
block.appendChild(think);
}
// tool calls + results
const calls = round.tool_calls || [];
const results = round.tool_results || [];
calls.forEach((call, ci) => {
const callId = `call-args-${ri}-${ci}`;
const resId = `call-res-${ri}-${ci}`;
const result = results[ci] || null;
// call card
const callCard = document.createElement('div');
callCard.className = 'tool-call-card';
const callHeader = document.createElement('div');
callHeader.className = 'tool-call-header';
const arrow = document.createElement('span');
arrow.className = 'tool-arrow open';
arrow.textContent = '▶';
callHeader.innerHTML = `
<span style="font-size:14px">🔧</span>
<span class="tool-name">${call.name}()</span>`;
callHeader.appendChild(arrow);
callHeader.addEventListener('click', () => toggleBody(callHeader, callId, arrow));
callCard.appendChild(callHeader);
const callArgs = document.createElement('div');
callArgs.className = 'tool-args'; callArgs.id = callId;
callArgs.innerHTML = `<pre>${prettyJson(call.args)}</pre>`;
callCard.appendChild(callArgs);
block.appendChild(callCard);
// result card
if (result) {
const resCard = document.createElement('div');
resCard.className = 'tool-result-card';
const resHeader = document.createElement('div');
resHeader.className = 'tool-result-header';
const resArrow = document.createElement('span');
resArrow.className = 'tool-arrow open'; resArrow.textContent = '▶';
let badge = '';
const r = result.result || {};
if (r.error) badge = `<span class="result-badge-err">error: ${r.error}</span>`;
else if (r.awaiting_user) badge = `<span class="result-badge-wait">awaiting user</span>`;
else badge = `<span class="result-badge-ok">ok</span>`;
resHeader.innerHTML = `<span class="result-label">↳ result</span>${badge}`;
resHeader.appendChild(resArrow);
resHeader.addEventListener('click', () => toggleBody(resHeader, resId, resArrow));
resCard.appendChild(resHeader);
const resBody = document.createElement('div');
resBody.className = 'tool-result-body'; resBody.id = resId;
resBody.innerHTML = `<pre>${prettyJson(result.result)}</pre>`;
resCard.appendChild(resBody);
block.appendChild(resCard);
}
});
debugScroll.appendChild(block);
});
scrollBottom(debugScroll);
}
// ── send ──────────────────────────────────────────────────────────────────────
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
userInput.value = '';
userInput.style.height = 'auto';
sendBtn.disabled = true;
addMessage(text, 'user');
showTyping();
try {
const res = await fetch(`${BACKEND_URL}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text }),
});
const data = await res.json();
hideTyping();
if (data.error) {
addMessage(`❌ ${data.error}`, 'msg system-note');
} else if (data.status === 'waiting_for_user') {
addMessage(data.assistant_text, 'waiting');
} else {
addMessage(data.assistant_text, 'assistant');
}
renderDebug(data);
} catch (e) {
hideTyping();
addMessage(`❌ Network error: ${e.message}`, 'system-note');
}
sendBtn.disabled = false;
userInput.focus();
}
// ── reset ─────────────────────────────────────────────────────────────────────
document.getElementById('resetBtn').addEventListener('click', async () => {
await fetch(`${BACKEND_URL}/reset`, { method: 'POST' });
messagesEl.innerHTML = '<div class="msg system-note">Conversation reset.</div>';
debugScroll.innerHTML = `<div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>
<span>Gửi tin nhắn để xem<br>tool calls xuất hiện tại đây</span>
</div>`;
turnBadge.textContent = '–';
turnCount = 0;
});
// ── input auto-resize + Enter ──────────────────────────────────────────────────
userInput.addEventListener('input', () => {
userInput.style.height = 'auto';
userInput.style.height = Math.min(userInput.scrollHeight, 120) + 'px';
});
userInput.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
sendBtn.addEventListener('click', sendMessage);
</script>
</body>
</html>