vetrag/ui_admin_chats.html

137 lines
5.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Chat Interactions Admin</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>pre{white-space:pre-wrap;word-break:break-word}</style>
</head>
<body class="bg-gray-50 min-h-screen p-4">
<div class="max-w-7xl mx-auto">
<h1 class="text-2xl font-bold mb-4 text-green-700">Chat Interactions</h1>
<div class="flex flex-wrap gap-2 items-end mb-4">
<div>
<label class="block text-xs font-semibold">Limit</label>
<input id="limitInput" type="number" value="50" class="border rounded px-2 py-1 w-24" />
</div>
<div>
<label class="block text-xs font-semibold">Offset</label>
<input id="offsetInput" type="number" value="0" class="border rounded px-2 py-1 w-24" />
</div>
<div>
<button id="loadBtn" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Load</button>
</div>
<div class="ml-auto">
<input id="filterInput" placeholder="Filter message / visit..." class="border rounded px-2 py-1" />
</div>
</div>
<div class="overflow-auto border rounded bg-white shadow">
<table class="min-w-full text-sm" id="chatsTable">
<thead class="bg-gray-100">
<tr>
<th class="p-2 text-left">Correlation</th>
<th class="p-2 text-left">Message</th>
<th class="p-2 text-left">Match</th>
<th class="p-2 text-left">Price</th>
<th class="p-2 text-left">Duration</th>
<th class="p-2 text-left">Created</th>
<th class="p-2">Events</th>
</tr>
</thead>
<tbody id="chatsBody"></tbody>
</table>
</div>
<div id="paginationInfo" class="text-xs text-gray-600 mt-2"></div>
<h2 class="text-xl font-semibold mt-8 mb-2">Raw LLM Events</h2>
<div id="eventsBox" class="border rounded bg-white shadow p-3 min-h-[120px]">
<p class="text-gray-500" id="eventsEmpty">Select a row to view events.</p>
<div id="eventsContainer" class="hidden"></div>
</div>
</div>
<script>
const chatsBody = document.getElementById('chatsBody');
const limitInput = document.getElementById('limitInput');
const offsetInput = document.getElementById('offsetInput');
const loadBtn = document.getElementById('loadBtn');
const paginationInfo = document.getElementById('paginationInfo');
const filterInput = document.getElementById('filterInput');
const eventsContainer = document.getElementById('eventsContainer');
const eventsEmpty = document.getElementById('eventsEmpty');
let chats = [];
function fmtDate(s) { try { return new Date(s).toLocaleString(); } catch { return s; } }
function escapeHtml(t) { return t.replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;' }[c])); }
async function loadChats() {
const limit = Number(limitInput.value) || 50;
const offset = Number(offsetInput.value) || 0;
const resp = await fetch(`/admin/chats?limit=${limit}&offset=${offset}`);
const data = await resp.json();
chats = data.items || [];
renderChats();
paginationInfo.textContent = `Showing ${chats.length} (limit=${limit} offset=${offset})`;
}
function renderChats() {
const f = filterInput.value.toLowerCase();
chatsBody.innerHTML = '';
chats
.filter(r => {
if(!f) return true;
return (r.user_message || '').toLowerCase().includes(f) || (r.best_visit_id || '').toLowerCase().includes(f);
})
.forEach(r => {
const tr = document.createElement('tr');
tr.className = 'border-b hover:bg-green-50';
tr.innerHTML = `
<td class='p-2 align-top font-mono text-[11px] break-all'>${escapeHtml(r.correlation_id)}</td>
<td class='p-2 align-top'>${escapeHtml((r.user_message||'').slice(0,140))}</td>
<td class='p-2 align-top'>${escapeHtml(r.best_visit_id||'')}</td>
<td class='p-2 align-top'>${r.total_price||0}</td>
<td class='p-2 align-top'>${r.total_duration||0}</td>
<td class='p-2 align-top whitespace-nowrap'>${fmtDate(r.created_at)}</td>
<td class='p-2 align-top'><button class='text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700' data-corr='${r.correlation_id}'>View</button></td>
`;
chatsBody.appendChild(tr);
});
document.querySelectorAll('#chatsBody button[data-corr]').forEach(btn => {
btn.onclick = () => loadEvents(btn.getAttribute('data-corr'));
});
}
async function loadEvents(corr) {
eventsEmpty.classList.add('hidden');
eventsContainer.classList.remove('hidden');
eventsContainer.innerHTML = '<div class="text-xs text-gray-500">Loading events...</div>';
const resp = await fetch(`/admin/chats/events?correlation_id=${encodeURIComponent(corr)}`);
if(!resp.ok) {
eventsContainer.innerHTML = '<div class="text-red-600 text-sm">Failed to load events</div>';
return;
}
const data = await resp.json();
const items = data.items || [];
if(items.length === 0) {
eventsContainer.innerHTML = '<div class="text-gray-500 text-sm">No events found.</div>';
return;
}
eventsContainer.innerHTML = '';
items.forEach(ev => {
const pre = document.createElement('pre');
pre.className = 'border rounded mb-3 p-2 bg-gray-900 text-green-200 text-xs overflow-auto';
let raw;
try { raw = JSON.stringify(JSON.parse(ev.raw_json), null, 2); } catch { raw = ev.raw_json; }
pre.innerHTML = `Phase: ${escapeHtml(ev.phase)}\nTime: ${fmtDate(ev.created_at)}\n---\n${escapeHtml(raw)}`;
eventsContainer.appendChild(pre);
});
}
loadBtn.onclick = loadChats;
filterInput.oninput = renderChats;
loadChats();
</script>
</body>
</html>