chat UI
This commit is contained in:
@@ -127,9 +127,9 @@
|
||||
display:flex; align-items:center; gap:8px; color:var(--dim); margin:0;
|
||||
}
|
||||
.chat-toolbar select {
|
||||
min-width:220px; max-width:min(420px, 50vw);
|
||||
min-width:280px; max-width:min(520px, 60vw);
|
||||
color:var(--fg); background:var(--chat-input-bg); border:1px solid var(--border);
|
||||
border-radius:6px; padding:6px 8px;
|
||||
border-radius:6px; padding:6px 8px; font-size:13px;
|
||||
}
|
||||
.chat-status { color:var(--dim); font-size:12px; margin-left:auto; }
|
||||
.chat-messages {
|
||||
@@ -831,6 +831,7 @@ let isLoggedIn = false;
|
||||
let accountApiKeys = [];
|
||||
let accountUsageRecords = [];
|
||||
let lastStats = null;
|
||||
let lastRouting = null;
|
||||
let availableModels = [];
|
||||
let chatHistory = [];
|
||||
let chatBusy = false;
|
||||
@@ -956,11 +957,7 @@ function selectChatSession(sessionId) {
|
||||
renderChatAuthHint();
|
||||
}
|
||||
|
||||
function deleteChatSession(sessionId, event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
function deleteChatSession(sessionId) {
|
||||
if (chatBusy) return;
|
||||
const index = chatSessions.findIndex(item => item.id === sessionId);
|
||||
if (index < 0) return;
|
||||
@@ -1020,18 +1017,43 @@ function renderChatSessionList() {
|
||||
const active = session.id === activeChatSessionId ? " active" : "";
|
||||
const title = esc(chatSessionTitle(session));
|
||||
const when = esc(formatSessionTime(session.updatedAt || session.createdAt));
|
||||
const id = JSON.stringify(session.id);
|
||||
const sessionId = esc(session.id);
|
||||
return `<div class="chat-session-item${active}" role="button" tabindex="0"` +
|
||||
` onclick="selectChatSession(${id})"` +
|
||||
` onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();selectChatSession(${id});}">` +
|
||||
` data-session-id="${sessionId}">` +
|
||||
`<div class="chat-session-title">${title}</div>` +
|
||||
(when ? `<div class="chat-session-meta">${when}</div>` : "") +
|
||||
`<button type="button" class="chat-session-delete" title="Delete chat"` +
|
||||
` onclick="deleteChatSession(${id}, event)">×</button>` +
|
||||
` data-delete-session="${sessionId}">×</button>` +
|
||||
`</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function bindChatSessionList() {
|
||||
const list = $("chat-session-list");
|
||||
if (!list || list.dataset.bound) return;
|
||||
list.dataset.bound = "1";
|
||||
list.addEventListener("click", event => {
|
||||
const deleteBtn = event.target.closest("[data-delete-session]");
|
||||
if (deleteBtn) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
deleteChatSession(deleteBtn.getAttribute("data-delete-session"));
|
||||
return;
|
||||
}
|
||||
const item = event.target.closest("[data-session-id]");
|
||||
if (item) selectChatSession(item.getAttribute("data-session-id"));
|
||||
});
|
||||
list.addEventListener("keydown", event => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return;
|
||||
const deleteBtn = event.target.closest("[data-delete-session]");
|
||||
if (deleteBtn) return;
|
||||
const item = event.target.closest("[data-session-id]");
|
||||
if (!item) return;
|
||||
event.preventDefault();
|
||||
selectChatSession(item.getAttribute("data-session-id"));
|
||||
});
|
||||
}
|
||||
|
||||
function switchDashboardTab(name) {
|
||||
if (name === "admin" && !isAdmin) name = "overview";
|
||||
if (name === "billing" && !isLoggedIn) name = "overview";
|
||||
@@ -1079,6 +1101,48 @@ function renderChatHistory() {
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}
|
||||
|
||||
function findRoutingForModel(model, routing) {
|
||||
const models = (routing && routing.models) || {};
|
||||
const keys = [model.id, model.name, model.hf_repo, ...(model.aliases || [])].filter(Boolean);
|
||||
for (const key of keys) {
|
||||
if (models[key]) return models[key];
|
||||
const aliasKey = modelAliasKey(key);
|
||||
for (const [routeKey, info] of Object.entries(models)) {
|
||||
if (modelAliasKey(routeKey) === aliasKey) return info;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function chatModelHealthLabel(coverage) {
|
||||
if (coverage === null || coverage === undefined) return "unknown";
|
||||
const pct = Math.round(Number(coverage));
|
||||
if (pct >= 100) return "healthy";
|
||||
if (pct > 0) return `${pct}% cov`;
|
||||
return "no coverage";
|
||||
}
|
||||
|
||||
function chatModelTypicalTps(model, routing) {
|
||||
const info = findRoutingForModel(model, routing);
|
||||
const routes = (info && info.routes) || [];
|
||||
if (!routes.length) return null;
|
||||
const proven = routes.filter(r => r.status === "proven" && Number(r.tps) > 0);
|
||||
if (proven.length) return Math.max(...proven.map(r => Number(r.tps)));
|
||||
const priors = routes.map(r => Number(r.prior_tps ?? r.tps ?? 0)).filter(v => v > 0);
|
||||
return priors.length ? Math.max(...priors) : null;
|
||||
}
|
||||
|
||||
function chatModelOptionLabel(model, routing) {
|
||||
const base = model.name && model.name !== model.id
|
||||
? `${model.name} (${model.id})`
|
||||
: model.id;
|
||||
const health = chatModelHealthLabel(model.coverage);
|
||||
const speed = chatModelTypicalTps(model, routing);
|
||||
const speedText = speed === null ? "?" : `${tps(speed)} tok/s`;
|
||||
const suffix = model.recommended ? " · recommended" : "";
|
||||
return `${base} · ${health} · ${speedText}${suffix}`;
|
||||
}
|
||||
|
||||
function renderChatModels() {
|
||||
const select = $("chat-model");
|
||||
if (!select) return;
|
||||
@@ -1094,11 +1158,8 @@ function renderChatModels() {
|
||||
selectedChatModel = preferred.id;
|
||||
localStorage.setItem("meshnet_chat_model", selectedChatModel);
|
||||
select.innerHTML = models.map(model => {
|
||||
const label = model.name && model.name !== model.id
|
||||
? `${model.name} (${model.id})`
|
||||
: model.id;
|
||||
const suffix = model.recommended ? " [recommended]" : "";
|
||||
return `<option value="${esc(model.id)}"${model.id === selectedChatModel ? " selected" : ""}>${esc(label + suffix)}</option>`;
|
||||
const label = chatModelOptionLabel(model, lastRouting);
|
||||
return `<option value="${esc(model.id)}"${model.id === selectedChatModel ? " selected" : ""}>${esc(label)}</option>`;
|
||||
}).join("");
|
||||
select.value = selectedChatModel;
|
||||
}
|
||||
@@ -1575,12 +1636,14 @@ async function refresh() {
|
||||
]);
|
||||
const [summary, settlements, wallets] = adminData;
|
||||
lastStats = stats;
|
||||
lastRouting = routing;
|
||||
availableModels = ((models && models.data) || []).map(model => ({
|
||||
id: model.id,
|
||||
name: model.name || model.id,
|
||||
hf_repo: model.hf_repo,
|
||||
recommended: Boolean(model.recommended),
|
||||
aliases: model.aliases || [],
|
||||
coverage: model.shard_coverage_percentage,
|
||||
})).filter(model => model.id);
|
||||
renderHive(raft);
|
||||
renderNodes(map);
|
||||
@@ -1610,6 +1673,7 @@ async function refreshIfIdle() {
|
||||
}
|
||||
|
||||
refresh();
|
||||
bindChatSessionList();
|
||||
initChatSessions();
|
||||
bindChatPromptShortcuts();
|
||||
renderAccountPanel();
|
||||
|
||||
Reference in New Issue
Block a user