feat: add web UI dashboard SPA with dashboard, chat, sessions, and settings pages
- Add SPA shell with hash-based router, sidebar navigation, and WebSocket RPC client - Add dashboard page with system health cards, channel status, and auto-refresh - Add chat page with session selector, streaming tool events, and markdown rendering - Add sessions page with list, history viewer, and delete functionality - Add settings page with hook pattern editor, tool list, and config viewer - Add backend handlers: sessions.delete, sessions.switch, system.channels, system.usage - Wire channelRegistry into gateway server for channel status reporting - Extend static file server with .mjs, .png, .ico, .woff2 content types
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Flynn Sessions Page
|
||||
*
|
||||
* Lists all sessions, allows viewing history and deleting sessions.
|
||||
*/
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let _client = null;
|
||||
let _el = null;
|
||||
|
||||
async function loadSessionList() {
|
||||
if (!_client || !_el) return;
|
||||
|
||||
const listContainer = _el.querySelector('#sessions-list');
|
||||
const detailContainer = _el.querySelector('#session-detail');
|
||||
if (detailContainer) detailContainer.innerHTML = '';
|
||||
|
||||
try {
|
||||
const result = await _client.call('sessions.list');
|
||||
const sessions = result.sessions ?? [];
|
||||
|
||||
if (sessions.length === 0) {
|
||||
listContainer.innerHTML = '<div class="empty-state">No sessions found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Messages</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
for (const s of sessions) {
|
||||
html += `
|
||||
<tr>
|
||||
<td><a href="#" class="session-view-link" data-id="${escapeHtml(s.id)}">${escapeHtml(s.id)}</a></td>
|
||||
<td>${s.messageCount ?? 0}</td>
|
||||
<td class="session-actions">
|
||||
<button class="btn btn-secondary session-view-btn" data-id="${escapeHtml(s.id)}">View</button>
|
||||
<button class="btn btn-danger session-delete-btn" data-id="${escapeHtml(s.id)}">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
// Bind view buttons
|
||||
listContainer.querySelectorAll('.session-view-btn, .session-view-link').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
viewSession(btn.dataset.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Bind delete buttons
|
||||
listContainer.querySelectorAll('.session-delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
deleteSession(btn.dataset.id);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
listContainer.innerHTML = `<div class="empty-state">Failed to load sessions: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function viewSession(sessionId) {
|
||||
const detailContainer = _el.querySelector('#session-detail');
|
||||
if (!detailContainer) return;
|
||||
|
||||
detailContainer.innerHTML = '<div class="empty-state"><span class="spinner"></span> Loading...</div>';
|
||||
|
||||
try {
|
||||
const result = await _client.call('sessions.history', { sessionId });
|
||||
const messages = result.messages ?? [];
|
||||
|
||||
let html = `
|
||||
<div class="session-detail">
|
||||
<div class="session-detail-header">
|
||||
<h2 class="section-title">${escapeHtml(sessionId)}</h2>
|
||||
<span class="text-muted text-sm">${messages.length} messages</span>
|
||||
</div>
|
||||
<div class="message-history">
|
||||
`;
|
||||
|
||||
if (messages.length === 0) {
|
||||
html += '<div class="empty-state">No messages in this session</div>';
|
||||
} else {
|
||||
for (const msg of messages) {
|
||||
const role = msg.role ?? 'system';
|
||||
const content = msg.content ?? msg.text ?? '';
|
||||
html += `<div class="message ${escapeHtml(role)}">${escapeHtml(content)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
detailContainer.innerHTML = html;
|
||||
} catch (err) {
|
||||
detailContainer.innerHTML = `<div class="empty-state">Failed to load session: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(sessionId) {
|
||||
if (!confirm(`Delete session "${sessionId}"? This will clear all message history.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _client.call('sessions.delete', { sessionId });
|
||||
await loadSessionList();
|
||||
} catch (err) {
|
||||
alert(`Failed to delete session: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionsPage = {
|
||||
async render(el, client) {
|
||||
_client = client;
|
||||
_el = el;
|
||||
|
||||
el.innerHTML = `
|
||||
<h1 class="page-title">Sessions</h1>
|
||||
<div id="sessions-list"></div>
|
||||
<div id="session-detail"></div>
|
||||
`;
|
||||
|
||||
await loadSessionList();
|
||||
},
|
||||
|
||||
teardown() {
|
||||
_client = null;
|
||||
_el = null;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user