137 lines
5.9 KiB
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 => ({ '&':'&','<':'<','>':'>','"':'"','\'':''' }[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>
|
|
|