Initial commit
This commit is contained in:
243
server/web/templates/chats.html
Normal file
243
server/web/templates/chats.html
Normal file
@@ -0,0 +1,243 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Chats — {{ agent_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;flex-wrap:wrap">
|
||||
<h2 style="font-size:18px;font-weight:600;flex:1;min-width:120px">Chat History</h2>
|
||||
<input id="chats-search" type="text" class="form-input" placeholder="Search…"
|
||||
style="width:220px;height:34px;padding:5px 12px;font-size:13px"
|
||||
oninput="chatsSearch(this.value)">
|
||||
<button class="btn btn-ghost btn-small" id="sel-all-btn" onclick="toggleSelectAll()" style="display:none">Select all</button>
|
||||
<button class="btn btn-ghost btn-small" id="del-sel-btn" onclick="deleteSelected()"
|
||||
style="display:none;color:var(--danger,#dc3c3c)">Delete selected (<span id="sel-count">0</span>)</button>
|
||||
<button class="btn btn-ghost btn-small" id="del-all-btn" onclick="deleteAll()"
|
||||
style="color:var(--danger,#dc3c3c)">Delete all</button>
|
||||
<a class="btn btn-primary btn-small" href="/"
|
||||
onclick="localStorage.removeItem('current_session_id')">+ New Chat</a>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div id="chats-list"><p style="color:var(--text-dim)">Loading…</p></div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="chats-pagination" style="display:flex;justify-content:center;gap:8px;margin-top:20px"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rename modal -->
|
||||
<div class="modal-overlay hidden" id="rename-modal" onclick="if(event.target===this)closeRenameModal()">
|
||||
<div class="modal" style="max-width:420px;width:90%">
|
||||
<h3 style="font-size:15px;font-weight:600;margin-bottom:12px">Rename chat</h3>
|
||||
<input id="rename-input" type="text" class="form-input" style="margin-bottom:14px"
|
||||
placeholder="Chat title" maxlength="120">
|
||||
<div class="modal-buttons">
|
||||
<button class="btn btn-ghost" onclick="closeRenameModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="submitRename()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
let _chatsPage = 1;
|
||||
let _chatsQ = "";
|
||||
let _renameId = null;
|
||||
let _allIds = []; // ids on the current page
|
||||
let _selected = new Set();
|
||||
let _chatMap = {}; // id -> { id, title } for current page
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||
}
|
||||
|
||||
/* ── Load & render ─────────────────────────────────────────────────────── */
|
||||
|
||||
async function loadChats(page) {
|
||||
page = page || _chatsPage;
|
||||
_chatsPage = page;
|
||||
_selected.clear();
|
||||
updateSelectionUI();
|
||||
const list = document.getElementById("chats-list");
|
||||
list.innerHTML = "<p style='color:var(--text-dim)'>Loading…</p>";
|
||||
try {
|
||||
const params = new URLSearchParams({ page, per_page: 40 });
|
||||
if (_chatsQ) params.set("q", _chatsQ);
|
||||
const r = await fetch("/api/conversations?" + params);
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
renderChats(await r.json());
|
||||
} catch(e) {
|
||||
list.innerHTML = `<p style='color:var(--danger)'>Failed to load: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function chatsSearch(q) {
|
||||
_chatsQ = q;
|
||||
_chatsPage = 1;
|
||||
clearTimeout(chatsSearch._t);
|
||||
chatsSearch._t = setTimeout(() => loadChats(1), 300);
|
||||
}
|
||||
|
||||
function renderChats(data) {
|
||||
const list = document.getElementById("chats-list");
|
||||
_allIds = data.conversations.map(c => c.id);
|
||||
_chatMap = {};
|
||||
data.conversations.forEach(c => { _chatMap[c.id] = { id: c.id, title: c.title || "Untitled chat" }; });
|
||||
|
||||
if (!data.conversations.length) {
|
||||
list.innerHTML = "<p style='color:var(--text-dim)'>No saved chats yet. Start a conversation and it will appear here automatically.</p>";
|
||||
document.getElementById("chats-pagination").innerHTML = "";
|
||||
document.getElementById("sel-all-btn").style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("sel-all-btn").style.display = "";
|
||||
|
||||
list.innerHTML = data.conversations.map(c => {
|
||||
const title = c.title || "Untitled chat";
|
||||
const date = c.ended_at ? formatDate(c.ended_at) : "—";
|
||||
const turns = Math.floor((c.message_count || 0) / 2);
|
||||
const model = c.model
|
||||
? `<span style="font-size:11px;color:var(--text-dim);background:var(--bg2);border:1px solid var(--border);border-radius:3px;padding:1px 6px;white-space:nowrap">${escHtml(c.model)}</span>`
|
||||
: "";
|
||||
const turnLabel = turns
|
||||
? `<span style="font-size:12px;color:var(--text-dim)">${turns} turn${turns !== 1 ? "s" : ""}</span>`
|
||||
: "";
|
||||
return `
|
||||
<div class="chat-row" data-id="${c.id}"
|
||||
style="display:flex;align-items:center;gap:12px;padding:12px 14px;
|
||||
border:1px solid var(--border);border-radius:var(--radius);
|
||||
margin-bottom:8px;background:var(--bg1)">
|
||||
<input type="checkbox" class="chat-chk" data-id="${c.id}"
|
||||
style="flex-shrink:0;width:15px;height:15px;cursor:pointer;accent-color:var(--accent)"
|
||||
onchange="onCheckChange(this)">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:14px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:3px"
|
||||
title="${escHtml(title)}">${escHtml(title)}</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="font-size:12px;color:var(--text-dim)">${date}</span>
|
||||
${turnLabel}
|
||||
${model}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-shrink:0">
|
||||
<a class="btn btn-ghost btn-small" href="/?session=${c.id}"
|
||||
data-id="${c.id}" onclick="localStorage.setItem('current_session_id',this.dataset.id)">Open</a>
|
||||
<button class="btn btn-ghost btn-small"
|
||||
data-id="${c.id}" onclick="openRenameModal(this.dataset.id)">Rename</button>
|
||||
<button class="btn btn-ghost btn-small" style="color:var(--danger,#dc3c3c)"
|
||||
data-id="${c.id}" onclick="deleteSingle(this.dataset.id)">Delete</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
|
||||
// Pagination
|
||||
const pages = Math.ceil(data.total / data.per_page);
|
||||
const pg = document.getElementById("chats-pagination");
|
||||
if (pages <= 1) { pg.innerHTML = ""; return; }
|
||||
let html = "";
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
html += `<button class="btn btn-ghost btn-small${i === _chatsPage ? " btn-active" : ""}"
|
||||
onclick="loadChats(${i})">${i}</button>`;
|
||||
}
|
||||
pg.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ── Selection ─────────────────────────────────────────────────────────── */
|
||||
|
||||
function onCheckChange(cb) {
|
||||
if (cb.checked) _selected.add(cb.dataset.id);
|
||||
else _selected.delete(cb.dataset.id);
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const allSelected = _allIds.every(id => _selected.has(id));
|
||||
if (allSelected) {
|
||||
_selected.clear();
|
||||
} else {
|
||||
_allIds.forEach(id => _selected.add(id));
|
||||
}
|
||||
document.querySelectorAll(".chat-chk").forEach(cb => {
|
||||
cb.checked = _selected.has(cb.dataset.id);
|
||||
});
|
||||
updateSelectionUI();
|
||||
}
|
||||
|
||||
function updateSelectionUI() {
|
||||
const n = _selected.size;
|
||||
const allSelected = _allIds.length > 0 && _allIds.every(id => _selected.has(id));
|
||||
document.getElementById("sel-all-btn").textContent = allSelected ? "Deselect all" : "Select all";
|
||||
const delSelBtn = document.getElementById("del-sel-btn");
|
||||
delSelBtn.style.display = n > 0 ? "" : "none";
|
||||
document.getElementById("sel-count").textContent = n;
|
||||
}
|
||||
|
||||
/* ── Delete actions ────────────────────────────────────────────────────── */
|
||||
|
||||
async function deleteSingle(id) {
|
||||
const title = (_chatMap[id] || {}).title || id;
|
||||
if (!confirm(`Delete "${title}"?\nThis cannot be undone.`)) return;
|
||||
const r = await fetch("/api/conversations/" + id, { method: "DELETE" });
|
||||
if (r.ok) { _selected.delete(id); loadChats(); }
|
||||
else alert("Failed to delete.");
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
const ids = [..._selected];
|
||||
if (!ids.length) return;
|
||||
if (!confirm(`Delete ${ids.length} selected chat${ids.length !== 1 ? "s" : ""}?\nThis cannot be undone.`)) return;
|
||||
await Promise.all(ids.map(id => fetch("/api/conversations/" + id, { method: "DELETE" })));
|
||||
_selected.clear();
|
||||
loadChats();
|
||||
}
|
||||
|
||||
async function deleteAll() {
|
||||
if (!confirm("Delete ALL saved chats?\nThis cannot be undone.")) return;
|
||||
// Fetch all IDs (no pagination limit) then delete
|
||||
const r = await fetch("/api/conversations?per_page=10000");
|
||||
const data = await r.json();
|
||||
await Promise.all(data.conversations.map(c => fetch("/api/conversations/" + c.id, { method: "DELETE" })));
|
||||
loadChats();
|
||||
}
|
||||
|
||||
/* ── Rename ────────────────────────────────────────────────────────────── */
|
||||
|
||||
function openRenameModal(id) {
|
||||
const chat = _chatMap[id];
|
||||
if (!chat) return;
|
||||
_renameId = id;
|
||||
document.getElementById("rename-input").value = chat.title;
|
||||
document.getElementById("rename-modal").classList.remove("hidden");
|
||||
setTimeout(() => document.getElementById("rename-input").select(), 50);
|
||||
}
|
||||
|
||||
function closeRenameModal() {
|
||||
document.getElementById("rename-modal").classList.add("hidden");
|
||||
_renameId = null;
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
if (!_renameId) return;
|
||||
const title = document.getElementById("rename-input").value.trim();
|
||||
if (!title) return;
|
||||
const r = await fetch("/api/conversations/" + _renameId, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
if (r.ok) { closeRenameModal(); loadChats(); }
|
||||
else alert("Failed to rename.");
|
||||
}
|
||||
|
||||
document.getElementById("rename-input").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") submitRename();
|
||||
if (e.key === "Escape") closeRenameModal();
|
||||
});
|
||||
|
||||
loadChats();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user