` +
`explore share:
${esc(String(cfg.explore_share ?? "?"))} · ` +
`traffic ∝ tps^
${esc(String(cfg.weight_alpha ?? "?"))} · ` +
@@ -594,7 +728,7 @@ function renderRouting(routing) {
html += `
${esc(model)} ` +
`(epoch ${esc(String(info.epoch ?? 0))} · ${routes.length} route${routes.length === 1 ? "" : "s"})
`;
html += table(["route", "tps", "coeff", "share", "samples", "status"], routes.map(r => {
- const hops = (r.hops || []).map(h => `${short(h.node_id, 12)}[${h.shard}]`).join(" → ");
+ const hops = (r.hops || []).map(h => `${nodeDisplayName(h.node_id, nameMap)}[${h.shard}]`).join(" → ");
const statusCls = r.status === "proven" ? "ok" : r.status === "stale" ? "warn" : "dim";
const coeff = (r.coefficient === null || r.coefficient === undefined)
? "—" : Number(r.coefficient).toFixed(2) + "×";
@@ -833,6 +967,7 @@ let accountApiKeys = [];
let accountUsageRecords = [];
let lastStats = null;
let lastRouting = null;
+let lastNetworkMap = null;
let availableModels = [];
let chatHistory = [];
let chatBusy = false;
@@ -932,7 +1067,7 @@ function createNewChatSession() {
clearChatPrompt();
saveChatSessionsStore();
renderChatSessionList();
- renderChatHistory();
+ renderChatHistory(true);
renderChatAuthHint();
const promptEl = $("chat-prompt");
if (promptEl) promptEl.focus();
@@ -954,7 +1089,7 @@ function selectChatSession(sessionId) {
}
localStorage.setItem(CHAT_ACTIVE_SESSION_KEY, activeChatSessionId);
renderChatSessionList();
- renderChatHistory();
+ renderChatHistory(true);
renderChatAuthHint();
}
@@ -980,7 +1115,7 @@ function deleteChatSession(sessionId) {
}
saveChatSessionsStore();
renderChatSessionList();
- renderChatHistory();
+ renderChatHistory(true);
renderChatModels();
}
@@ -1002,31 +1137,54 @@ function initChatSessions() {
if (active.model) selectedChatModel = active.model;
}
renderChatSessionList();
- renderChatHistory();
+ renderChatHistory(true);
}
function renderChatSessionList() {
const list = $("chat-session-list");
- if (!list) return;
- if (!chatSessions.length) {
- list.className = "chat-session-list empty-state";
- list.innerHTML = "No chats yet";
- return;
+ syncKeyedList(
+ list,
+ chatSessions,
+ session => session.id,
+ session => `${session.id}|${session.updatedAt || ""}|${chatSessionTitle(session)}|${session.id === activeChatSessionId}`,
+ createSessionRowElement,
+ patchSessionRowElement,
+ );
+}
+
+function createSessionRowElement(session) {
+ const el = document.createElement("div");
+ el.className = "chat-session-item";
+ el.dataset.sessionId = session.id;
+ el.setAttribute("role", "button");
+ el.tabIndex = 0;
+ const title = document.createElement("div");
+ title.className = "chat-session-title";
+ const meta = document.createElement("div");
+ meta.className = "chat-session-meta";
+ const del = document.createElement("button");
+ del.type = "button";
+ del.className = "chat-session-delete";
+ del.title = "Delete chat";
+ del.dataset.deleteSession = session.id;
+ del.textContent = "×";
+ el.append(title, meta, del);
+ patchSessionRowElement(el, session);
+ return el;
+}
+
+function patchSessionRowElement(el, session) {
+ el.classList.toggle("active", session.id === activeChatSessionId);
+ el.querySelector(".chat-session-title").textContent = chatSessionTitle(session);
+ const when = formatSessionTime(session.updatedAt || session.createdAt);
+ const meta = el.querySelector(".chat-session-meta");
+ if (when) {
+ meta.textContent = when;
+ meta.hidden = false;
+ } else {
+ meta.textContent = "";
+ meta.hidden = true;
}
- list.className = "chat-session-list";
- list.innerHTML = chatSessions.map(session => {
- const active = session.id === activeChatSessionId ? " active" : "";
- const title = esc(chatSessionTitle(session));
- const when = esc(formatSessionTime(session.updatedAt || session.createdAt));
- const sessionId = esc(session.id);
- return `
` +
- `
${title}
` +
- (when ? `
${when}
` : "") +
- `
` +
- `
`;
- }).join("");
}
function bindChatSessionList() {
@@ -1069,6 +1227,7 @@ function switchDashboardTab(name) {
const promptEl = $("chat-prompt");
if (promptEl) promptEl.focus();
}
+ refreshActiveTab(true);
}
function updateSectionVisibility() {
@@ -1084,22 +1243,33 @@ function renderChatStatus(text) {
$("chat-status").textContent = text;
}
-function renderChatHistory() {
+function renderChatHistory(force) {
const history = $("chat-history");
if (!history) return;
+ if (!force && chatHistory.some(msg => msg.streaming)) return;
if (!chatHistory.length) {
+ if (history.dataset.historySig === "empty") return;
+ history.dataset.historySig = "empty";
history.className = "chat-messages empty";
history.innerHTML = '
Send a message to start this conversation.
';
return;
}
+ const sig = stableSig(chatHistory.map(msg => ({
+ role: msg.role,
+ content: msg.content,
+ streaming: Boolean(msg.streaming),
+ })));
+ if (!force && history.dataset.historySig === sig) return;
+ history.dataset.historySig = sig;
history.className = "chat-messages";
+ const nearBottom = history.scrollHeight - history.scrollTop - history.clientHeight < 80;
const rows = chatHistory.map(msg => {
const role = msg.role === "user" ? "user" : msg.role === "assistant" ? "assistant" : "error";
const streaming = msg.streaming ? " streaming" : "";
return `
`;
}).join("");
history.innerHTML = `
${rows}
`;
- history.scrollTop = history.scrollHeight;
+ if (nearBottom || force) history.scrollTop = history.scrollHeight;
}
function findRoutingForModel(model, routing) {
@@ -1140,9 +1310,17 @@ function chatModelOptionLabel(model, routing) {
return `${base} · ${health} · ${speedText}${suffix}`;
}
-function renderChatModels() {
+function renderChatModels(force) {
const select = $("chat-model");
if (!select) return;
+ const sig = stableSig(chatModelsSignature());
+ if (!force && panelSig.chatModels === sig) return;
+ if (!force && isEditing(select)) {
+ pendingChatModelRefresh = true;
+ return;
+ }
+ pendingChatModelRefresh = false;
+ panelSig.chatModels = sig;
const models = availableModels.slice();
if (!models.length) {
select.innerHTML = '
';
@@ -1161,6 +1339,15 @@ function renderChatModels() {
select.value = selectedChatModel;
}
+function bindChatModelSelect() {
+ const select = $("chat-model");
+ if (!select || select.dataset.bound) return;
+ select.dataset.bound = "1";
+ select.addEventListener("blur", () => {
+ if (pendingChatModelRefresh) renderChatModels(true);
+ });
+}
+
function selectChatModel(value) {
selectedChatModel = value || "";
localStorage.setItem("meshnet_chat_model", selectedChatModel);
@@ -1231,7 +1418,8 @@ function renderAuthForms(errorMsg) {
`
${label}`;
const identityFields =
'
' +
- '
';
+ '
' +
+ '
';
const form = authTab === "login"
? '
' +
'
' +
@@ -1253,12 +1441,14 @@ function switchAuthTab(name) { authTab = name; renderAuthForms(); }
async function doRegister() {
const email = $("auth-email").value.trim();
const wallet = $("auth-wallet").value.trim();
+ const nickname = $("auth-nickname").value.trim();
const password = $("auth-password").value;
const r = await apiCall("/v1/auth/register", "POST",
- { email: email || null, wallet: wallet || null, password });
+ { email: email || null, wallet: wallet || null, nickname: nickname || null, password });
if (!r.ok) { renderAuthForms(r.data.error || "registration failed"); return; }
setSession(r.data.session_token);
- await renderAccountPanel();
+ await loadAccountSummary(true);
+ await refreshActiveTab(true);
}
async function doLogin() {
@@ -1267,7 +1457,8 @@ async function doLogin() {
const r = await apiCall("/v1/auth/login", "POST", { identifier, password });
if (!r.ok) { renderAuthForms(r.data.error || "login failed"); return; }
setSession(r.data.session_token);
- await renderAccountPanel();
+ await loadAccountSummary(true);
+ await refreshActiveTab(true);
}
async function doLogout() {
@@ -1278,19 +1469,19 @@ async function doLogout() {
async function createKey() {
const r = await apiCall("/v1/account/keys", "POST", {});
- if (r.ok) await renderAccountPanel();
+ if (r.ok) await loadAccountSummary(true);
}
async function revokeKey(key) {
if (!confirm("Revoke this API key? Clients using it will stop working.")) return;
await apiCall("/v1/account/keys/revoke", "POST", { api_key: key });
- await renderAccountPanel();
+ await loadAccountSummary(true);
}
async function topupKey(key) {
const r = await apiCall("/v1/account/topup", "POST", { api_key: key });
if (!r.ok) alert(r.data.error || "top-up failed");
- await renderAccountPanel();
+ await loadAccountSummary(true);
}
const COPY_TOOLTIP_MS = 2000;
@@ -1354,54 +1545,112 @@ function renderChatAuthHint() {
}
}
-async function renderAccountPanel() {
+let accountTopupAmount = 0;
+
+function buildAccountPanelShell() {
+ $("account").innerHTML =
+ `
` +
+ `
` +
+ `
` +
+ `
` +
+ `` +
+ `` +
+ `
` +
+ `
balance: USDT · requests: · ` +
+ `tokens: · spent: USDT
` +
+ `
API keys ` +
+ `
` +
+ `
`;
+}
+
+function renderAccountKeys(api_keys, balances, topup_amount) {
+ const el = $("account-keys");
+ if (!el) return;
+ if (!api_keys.length) {
+ el.innerHTML = '
no active keys
';
+ return;
+ }
+ let html = "";
+ for (const key of api_keys) {
+ html += `
` +
+ `${esc(key)}` +
+ `(${usdt(balances[key] ?? 0)} USDT)` +
+ `` +
+ (topup_amount > 0
+ ? ` `
+ : "") +
+ `
`;
+ }
+ el.innerHTML = html;
+}
+
+function patchAccountPanelView(account, api_keys, balances, total_balance, usage, topup_amount) {
+ const root = $("account-panel-root");
+ if (!root) return false;
+ accountTopupAmount = topup_amount || 0;
+ root.querySelector('[data-bind="who"]').textContent = accountDisplayName(account);
+ root.querySelector('[data-bind="role"]').textContent = account.role;
+ const bal = root.querySelector('[data-bind="balance"]');
+ bal.textContent = usdt(total_balance);
+ bal.className = total_balance > 0 ? "ok" : "bad";
+ root.querySelector('[data-bind="requests"]').textContent = String(usage.requests);
+ root.querySelector('[data-bind="tokens"]').textContent = String(usage.total_tokens);
+ root.querySelector('[data-bind="spent"]').textContent = usdt(usage.total_cost);
+ const nick = $("account-nickname");
+ if (nick && !isEditing(nick)) nick.value = account.nickname || "";
+ renderIfChanged("account-keys", { api_keys, balances, topup_amount }, data => {
+ renderAccountKeys(data.api_keys, data.balances, data.topup_amount);
+ });
+ return true;
+}
+
+async function saveNickname() {
+ const nickname = $("account-nickname").value.trim();
+ const r = await apiCall("/v1/account/profile", "POST", { nickname: nickname || null });
+ if (!r.ok) alert(r.data.error || "nickname update failed");
+ else await loadAccountSummary(true);
+}
+
+function applyAccountSummary(data, force) {
+ const { account, api_keys, balances, total_balance, usage, topup_amount } = data;
+ accountApiKeys = Array.isArray(api_keys) ? api_keys.slice() : [];
+ if (force || !patchAccountPanelView(account, api_keys, balances, total_balance, usage, topup_amount)) {
+ buildAccountPanelShell();
+ patchAccountPanelView(account, api_keys, balances, total_balance, usage, topup_amount);
+ }
+ renderChatAuthHint();
+ setLoggedInMode(true);
+ setAdminMode(account.role === "admin");
+}
+
+async function loadAccountSummary(force) {
const r = await apiCall("/v1/account");
- if (r.status === 404) { // accounts disabled on this tracker
+ if (r.status === 404) {
$("account-section").style.display = "none";
accountApiKeys = [];
accountUsageRecords = [];
renderChatAuthHint();
setLoggedInMode(false);
setAdminMode(false);
- return;
+ return false;
}
- if (!r.ok) { setSession(null); renderAuthForms(); return; }
- const { account, api_keys, balances, total_balance, usage, topup_amount } = r.data;
- accountApiKeys = Array.isArray(api_keys) ? api_keys.slice() : [];
- accountUsageRecords = (usage && (usage.records || usage.recent)) || [];
- const who = account.email || account.wallet || account.account_id;
- let html =
- `
${esc(who)} ${esc(account.role)} ` +
- `
` +
- `
balance: ` +
- `${usdt(total_balance)} USDT · requests: ${usage.requests} · ` +
- `tokens: ${usage.total_tokens} · spent: ${usdt(usage.total_cost)} USDT
`;
- html += '
API keys ' +
- '
';
- if (api_keys.length) {
- for (const key of api_keys) {
- html += `
` +
- `${esc(key)}` +
- `(${usdt(balances[key] ?? 0)} USDT)` +
- `` +
- (topup_amount > 0
- ? ` `
- : "") +
- `
`;
- }
- } else {
- html += '
no active keys
';
- }
- $("account").innerHTML = html;
- renderUsageSummary(accountUsageRecords);
- renderNodeThroughput(lastStats);
- renderBillingUsage(accountUsageRecords);
- renderChatAuthHint();
- renderChatModels();
- renderChatHistory();
- setLoggedInMode(true);
- setAdminMode(account.role === "admin");
- if (account.role === "admin") await renderAdminPanel();
+ if (!r.ok) { setSession(null); renderAuthForms(); return false; }
+ applyAccountSummary(r.data, force);
+ if (dashboardTab === "chat" && !refreshBlocked()) renderChatModels(force);
+ return true;
+}
+
+async function loadAccountUsage() {
+ const r = await apiCall("/v1/account/usage");
+ if (!r.ok) return;
+ accountUsageRecords = (r.data.records || r.data.recent) || [];
+ renderIfChanged("usage-summary", accountUsageRecords, () => renderUsageSummary(accountUsageRecords));
+ renderIfChanged("billing-usage", accountUsageRecords, () => renderBillingUsage(accountUsageRecords));
+}
+
+/** @deprecated use loadAccountSummary */
+async function renderAccountPanel(force) {
+ return loadAccountSummary(force);
}
let chatAbortController = null;
@@ -1423,7 +1672,7 @@ function updateStreamingChatBubble(content) {
if (!history) return;
const bubbles = history.querySelectorAll(".chat-bubble.assistant.streaming");
const bubble = bubbles[bubbles.length - 1];
- if (!bubble) { renderChatHistory(); return; }
+ if (!bubble) { renderChatHistory(true); return; }
bubble.textContent = content;
const nearBottom = history.scrollHeight - history.scrollTop - history.clientHeight < 80;
if (nearBottom) history.scrollTop = history.scrollHeight;
@@ -1433,7 +1682,7 @@ function finalizeAssistantMessage(msg) {
delete msg.streaming;
if (!chatHistory.includes(msg)) chatHistory.push(msg);
if (!msg.content) msg.content = "(empty response)";
- renderChatHistory();
+ renderChatHistory(true);
persistActiveChatSession();
}
@@ -1441,7 +1690,7 @@ function removeAssistantMessage(msg) {
const index = chatHistory.indexOf(msg);
if (index >= 0) {
chatHistory.splice(index, 1);
- renderChatHistory();
+ renderChatHistory(true);
}
}
@@ -1472,7 +1721,7 @@ async function sendChat() {
chatHistory.push({ role: "user", content: prompt, model: selectedChatModel });
const assistantMessage = { role: "assistant", content: "", model: selectedChatModel, streaming: true };
chatHistory.push(assistantMessage);
- renderChatHistory();
+ renderChatHistory(true);
persistActiveChatSession();
renderChatStatus("sending request…");
let tokens = 0;
@@ -1555,7 +1804,7 @@ async function sendChat() {
removeAssistantMessage(assistantMessage);
const error = (err && err.message) || "request failed";
chatHistory.push({ role: "error", content: error, model: selectedChatModel });
- renderChatHistory();
+ renderChatHistory(true);
persistActiveChatSession();
renderChatStatus(error);
}
@@ -1583,29 +1832,13 @@ function bindChatPromptShortcuts() {
});
}
-async function renderAdminPanel() {
- const r = await apiCall("/v1/admin/accounts");
- if (!r.ok) { setAdminMode(false); return; }
- const rows = (r.data.accounts || []).map(a => {
- const balance = Object.values(a.balances || {}).reduce((x, y) => x + y, 0);
- return [
- esc(short(a.email || a.wallet || a.account_id, 24)),
- `
${esc(a.role)}`,
- String((a.api_keys || []).length),
- `
${usdt(balance)}`,
- new Date(a.created_ts * 1000).toLocaleDateString(),
- ];
- });
- $("admin").innerHTML = table(["account", "role", "keys", "balance (USDT)", "created"], rows);
-}
-
async function cancelProxyRequest(requestId) {
const r = await apiCall(
`/v1/proxy/requests/${encodeURIComponent(requestId)}/cancel`,
"POST",
{},
);
- if (r.ok) refresh();
+ if (r.ok) await refreshCallWall();
}
$("call-wall").addEventListener("click", (event) => {
@@ -1616,74 +1849,166 @@ $("call-wall").addEventListener("click", (event) => {
if (requestId) cancelProxyRequest(requestId);
});
-async function refresh() {
- $("self-url").textContent = location.host;
- const [raft, map, stats, models, consoleData, routing, adminData] = await Promise.all([
- fetchJson("/v1/raft/status"),
- fetchJson("/v1/network/map"),
- fetchJson("/v1/stats"),
- fetchJson("/v1/models"),
- fetchJson("/v1/console"),
- fetchJson("/v1/routing"),
- isAdmin ? Promise.all([
- fetchJson("/v1/billing/summary"),
- fetchJson("/v1/billing/settlements"),
- fetchJson("/v1/registry/wallets"),
- ]) : Promise.resolve([null, null, null]),
- ]);
- 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,
- servedCopies: model.served_model_copies,
- })).filter(model => model.id);
- renderHive(raft);
- renderNodes(map);
- renderBilling(summary);
- renderSettlements(settlements);
- renderFraud(wallets, summary);
- renderStats(stats);
- renderRouting(routing);
- renderCallWall(consoleData, stats, map);
- renderConsole(consoleData);
- renderNodeThroughput(stats);
- renderChatModels();
- renderChatHistory();
+function applyAvailableModels(models, map) {
+ lastNetworkMap = map || lastNetworkMap;
+ availableModels = ((models && models.data) || []).map(model => {
+ const entry = {
+ 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,
+ servedCopies: model.served_model_copies,
+ };
+ entry.servedCopies = modelServedCopiesFromMap(lastNetworkMap, entry);
+ return entry;
+ }).filter(model => model.id);
+}
+
+function markRefreshed() {
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
}
-const REFRESH_MS = 10000;
+async function fetchOverviewTab() {
+ const fetches = [
+ fetchJson("/v1/raft/status"),
+ fetchJson("/v1/network/map"),
+ fetchJson("/v1/stats"),
+ fetchJson("/v1/routing"),
+ fetchJson("/v1/console"),
+ ];
+ if (isLoggedIn) fetches.push(apiCall("/v1/account"));
+ const results = await Promise.all(fetches);
+ const [raft, map, stats, routing, consoleData, accountResp] = results;
+ lastStats = stats;
+ lastRouting = routing;
+ lastNetworkMap = map;
+ if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
+ renderIfChanged("hive", raft, renderHive);
+ renderIfChanged("nodes", map, renderNodes);
+ renderIfChanged("stats", stats, renderStats);
+ renderIfChanged("routing", routing, renderRouting);
+ renderIfChanged("call-wall", { consoleData, stats, map }, data => renderCallWall(data.consoleData, data.stats, data.map));
+}
+
+async function fetchChatTab() {
+ const fetches = [
+ fetchJson("/v1/network/map"),
+ fetchJson("/v1/models"),
+ fetchJson("/v1/routing"),
+ ];
+ if (isLoggedIn) fetches.push(apiCall("/v1/account"));
+ const [map, models, routing, accountResp] = await Promise.all(fetches);
+ lastRouting = routing;
+ applyAvailableModels(models, map);
+ if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
+ if (!refreshBlocked()) {
+ renderChatModels(true);
+ } else {
+ pendingChatModelRefresh = true;
+ }
+}
+
+async function fetchBillingTab() {
+ const [stats, , accountResp, adminData] = await Promise.all([
+ fetchJson("/v1/stats"),
+ loadAccountUsage(),
+ isLoggedIn ? apiCall("/v1/account") : Promise.resolve(null),
+ isAdmin ? Promise.all([
+ fetchJson("/v1/billing/summary"),
+ fetchJson("/v1/billing/settlements"),
+ ]) : Promise.resolve(null),
+ ]);
+ lastStats = stats;
+ if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
+ renderIfChanged("node-throughput", stats, renderNodeThroughput);
+ if (adminData) {
+ const [summary, settlements] = adminData;
+ renderIfChanged("billing-summary", summary, renderBilling);
+ renderIfChanged("settlements", settlements, renderSettlements);
+ }
+}
+
+async function fetchAdminTab() {
+ const fetches = [
+ fetchJson("/v1/console"),
+ fetchJson("/v1/billing/summary"),
+ fetchJson("/v1/registry/wallets"),
+ ];
+ if (isAdmin) fetches.push(apiCall("/v1/admin/accounts"));
+ const results = await Promise.all(fetches);
+ const [consoleData, summary, wallets, adminResp] = results;
+ renderIfChanged("console", consoleData, renderConsole);
+ renderIfChanged("billing-summary", summary, data => renderBilling(data));
+ renderIfChanged("fraud", { wallets, summary }, data => renderFraud(data.wallets, data.summary));
+ if (adminResp && adminResp.ok) {
+ renderIfChanged("admin", adminResp.data.accounts || [], accounts => {
+ const rows = accounts.map(a => {
+ const balance = Object.values(a.balances || {}).reduce((x, y) => x + y, 0);
+ return [
+ esc(short(accountDisplayName(a), 24)),
+ `
${esc(a.role)}`,
+ String((a.api_keys || []).length),
+ `
${usdt(balance)}`,
+ new Date(a.created_ts * 1000).toLocaleDateString(),
+ ];
+ });
+ $("admin").innerHTML = table(["account", "role", "keys", "balance (USDT)", "created"], rows);
+ });
+ }
+}
+
+async function refreshCallWall() {
+ const [consoleData, map] = await Promise.all([
+ fetchJson("/v1/console"),
+ fetchJson("/v1/network/map"),
+ ]);
+ if (map) lastNetworkMap = map;
+ renderIfChanged("call-wall", { consoleData, stats: lastStats, map }, data =>
+ renderCallWall(data.consoleData, data.stats, data.map));
+}
+
+const TAB_FETCHERS = {
+ overview: fetchOverviewTab,
+ chat: fetchChatTab,
+ billing: fetchBillingTab,
+ admin: fetchAdminTab,
+};
+
+async function refreshActiveTab(force) {
+ $("self-url").textContent = location.host;
+ if (refreshBlocked() && !force) return;
+ const fetcher = TAB_FETCHERS[dashboardTab];
+ if (fetcher) await fetcher();
+ markRefreshed();
+}
+
+const CALL_WALL_POLL_MS = 5000;
+
+async function pollCallWallIfIdle() {
+ if (dashboardTab !== "overview" || refreshBlocked()) return;
+ await refreshCallWall();
+}
function selectionActive() {
const sel = window.getSelection();
return sel && !sel.isCollapsed && sel.toString().length > 0;
}
-async function refreshIfIdle() {
- if (selectionActive()) return;
- await refresh();
-}
-
-refresh();
bindChatSessionList();
+bindChatModelSelect();
initChatSessions();
bindChatPromptShortcuts();
-renderAccountPanel();
-renderChatModels();
-renderChatHistory();
-renderChatAuthHint();
-setInterval(refreshIfIdle, REFRESH_MS);
-setInterval(() => {
- if (!sessionToken && !isLoggedIn) return;
- if (selectionActive()) return;
- renderAccountPanel();
-}, REFRESH_MS);
+(async () => {
+ if (sessionToken) await loadAccountSummary(true);
+ else renderAuthForms();
+ await refreshActiveTab(true);
+ renderChatHistory(true);
+ renderChatAuthHint();
+ if (dashboardTab === "chat") renderChatModels(true);
+})();
+setInterval(pollCallWallIfIdle, CALL_WALL_POLL_MS);