Redesign tracker chat UI with session sidebar and browser-local history.
This commit is contained in:
@@ -59,21 +59,84 @@
|
|||||||
.dashboard-tabs button.active { color:var(--accent); border-bottom-color:var(--accent); }
|
.dashboard-tabs button.active { color:var(--accent); border-bottom-color:var(--accent); }
|
||||||
.wide { grid-column:1 / -1; }
|
.wide { grid-column:1 / -1; }
|
||||||
section[hidden] { display:none !important; }
|
section[hidden] { display:none !important; }
|
||||||
.chat-shell { display:grid; grid-template-columns:minmax(0, 1.35fr) minmax(320px, 0.65fr); gap:12px; }
|
.chat-app {
|
||||||
.chat-pane { display:flex; flex-direction:column; gap:10px; min-width:0; }
|
display:grid; grid-template-columns:260px minmax(0, 1fr); gap:0;
|
||||||
.chat-panel { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:10px; }
|
min-height:560px; border:1px solid var(--border); border-radius:8px;
|
||||||
.chat-controls { display:flex; gap:10px; align-items:end; flex-wrap:wrap; }
|
overflow:hidden; background:var(--bg);
|
||||||
.chat-controls label { display:flex; flex-direction:column; gap:4px; color:var(--dim); }
|
}
|
||||||
.chat-controls select { min-width:220px; }
|
.chat-sidebar {
|
||||||
.chat-history { display:flex; flex-direction:column; gap:8px; min-height:220px; max-height:420px; overflow:auto; }
|
display:flex; flex-direction:column; min-height:0;
|
||||||
.chat-message { border:1px solid #21262d; border-radius:6px; padding:8px 10px; background:#10151d; }
|
border-right:1px solid var(--border); background:var(--panel);
|
||||||
.chat-role { color:var(--dim); font-size:11px; text-transform:uppercase; letter-spacing:.06em; margin-bottom:4px; }
|
}
|
||||||
.chat-role-user { color:var(--accent); }
|
.chat-sidebar-header {
|
||||||
.chat-role-assistant { color:var(--ok); }
|
display:flex; flex-direction:column; gap:8px; padding:12px;
|
||||||
.chat-role-error { color:var(--bad); }
|
border-bottom:1px solid var(--border);
|
||||||
.chat-compose { display:flex; flex-direction:column; gap:8px; }
|
}
|
||||||
.chat-compose textarea { min-height:112px; resize:vertical; width:100%; }
|
.chat-sidebar-header button { width:100%; text-align:left; }
|
||||||
.chat-status { color:var(--dim); font-size:12px; }
|
.chat-session-list {
|
||||||
|
flex:1; overflow:auto; padding:8px; display:flex; flex-direction:column; gap:4px;
|
||||||
|
}
|
||||||
|
.chat-session-item {
|
||||||
|
position:relative; display:block; width:100%; text-align:left;
|
||||||
|
padding:8px 28px 8px 10px; border:1px solid transparent; border-radius:6px;
|
||||||
|
background:transparent; color:var(--fg); cursor:pointer;
|
||||||
|
}
|
||||||
|
.chat-session-item:hover { background:#10151d; border-color:#21262d; }
|
||||||
|
.chat-session-item.active {
|
||||||
|
background:#10151d; border-color:var(--accent);
|
||||||
|
}
|
||||||
|
.chat-session-title {
|
||||||
|
font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||||
|
}
|
||||||
|
.chat-session-meta {
|
||||||
|
margin-top:2px; font-size:11px; color:var(--dim);
|
||||||
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||||
|
}
|
||||||
|
.chat-session-delete {
|
||||||
|
position:absolute; top:6px; right:4px; padding:0 5px; min-width:0;
|
||||||
|
border:0; background:transparent; color:var(--dim); line-height:1.2;
|
||||||
|
}
|
||||||
|
.chat-session-delete:hover { color:var(--bad); }
|
||||||
|
.chat-main { display:flex; flex-direction:column; min-height:560px; min-width:0; }
|
||||||
|
.chat-main-header {
|
||||||
|
display:flex; gap:12px; align-items:center; flex-wrap:wrap;
|
||||||
|
padding:10px 12px; border-bottom:1px solid var(--border); background:var(--panel);
|
||||||
|
}
|
||||||
|
.chat-main-header label {
|
||||||
|
display:flex; align-items:center; gap:8px; color:var(--dim); margin:0;
|
||||||
|
}
|
||||||
|
.chat-main-header select { min-width:220px; }
|
||||||
|
.chat-main-title { flex:1; min-width:120px; font-size:12px; color:var(--fg); }
|
||||||
|
.chat-status { color:var(--dim); font-size:12px; margin-left:auto; }
|
||||||
|
.chat-messages {
|
||||||
|
flex:1; overflow:auto; padding:16px; display:flex; flex-direction:column; gap:12px;
|
||||||
|
min-height:280px;
|
||||||
|
}
|
||||||
|
.chat-messages.empty {
|
||||||
|
justify-content:center; align-items:center; color:var(--dim); font-style:italic;
|
||||||
|
}
|
||||||
|
.chat-bubble-wrap { display:flex; flex-direction:column; gap:4px; max-width:88%; }
|
||||||
|
.chat-bubble-wrap.user { align-self:flex-end; align-items:flex-end; }
|
||||||
|
.chat-bubble-wrap.assistant, .chat-bubble-wrap.error { align-self:flex-start; align-items:flex-start; }
|
||||||
|
.chat-bubble-label {
|
||||||
|
font-size:10px; text-transform:uppercase; letter-spacing:.06em; color:var(--dim);
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
padding:10px 12px; border-radius:12px; line-height:1.45;
|
||||||
|
white-space:pre-wrap; word-break:break-word;
|
||||||
|
}
|
||||||
|
.chat-bubble.user { background:#1f3a5f; border:1px solid #264a73; border-bottom-right-radius:4px; }
|
||||||
|
.chat-bubble.assistant { background:#10151d; border:1px solid #21262d; border-bottom-left-radius:4px; }
|
||||||
|
.chat-bubble.error { background:#1a1012; border:1px solid #5c2020; color:#ffb4b4; border-bottom-left-radius:4px; }
|
||||||
|
.chat-compose {
|
||||||
|
display:flex; gap:8px; align-items:flex-end; padding:12px;
|
||||||
|
border-top:1px solid var(--border); background:var(--panel);
|
||||||
|
}
|
||||||
|
.chat-compose textarea {
|
||||||
|
flex:1; min-height:44px; max-height:160px; resize:none; width:auto;
|
||||||
|
}
|
||||||
|
.chat-compose button { align-self:flex-end; min-width:72px; }
|
||||||
|
.chat-hint { padding:0 12px 10px; color:var(--dim); font-size:11px; }
|
||||||
.console {
|
.console {
|
||||||
background:var(--bg); border:1px solid var(--border); border-radius:6px;
|
background:var(--bg); border:1px solid var(--border); border-radius:6px;
|
||||||
min-height:160px; max-height:280px; overflow:auto; padding:7px 9px;
|
min-height:160px; max-height:280px; overflow:auto; padding:7px 9px;
|
||||||
@@ -112,27 +175,28 @@
|
|||||||
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
|
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
|
||||||
<section data-tab="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
|
<section data-tab="overview" class="wide"><h2>Call wall</h2><div id="call-wall" class="empty">loading...</div></section>
|
||||||
<section data-tab="chat" class="wide">
|
<section data-tab="chat" class="wide">
|
||||||
<h2>Chat / inference</h2>
|
<h2>Chat</h2>
|
||||||
<div class="chat-shell">
|
<div class="chat-app">
|
||||||
<div class="chat-pane">
|
<aside class="chat-sidebar">
|
||||||
<div class="chat-panel chat-controls">
|
<div class="chat-sidebar-header">
|
||||||
|
<button type="button" onclick="createNewChatSession()">+ New chat</button>
|
||||||
|
</div>
|
||||||
|
<div id="chat-session-list" class="chat-session-list empty">no chats yet</div>
|
||||||
|
</aside>
|
||||||
|
<div class="chat-main">
|
||||||
|
<div class="chat-main-header">
|
||||||
<label>Model
|
<label>Model
|
||||||
<select id="chat-model" onchange="selectChatModel(this.value)"></select>
|
<select id="chat-model" onchange="selectChatModel(this.value)"></select>
|
||||||
</label>
|
</label>
|
||||||
<button class="small" onclick="clearChatHistory()">clear history</button>
|
<div id="chat-main-title" class="chat-main-title">New chat</div>
|
||||||
</div>
|
|
||||||
<div class="chat-panel chat-compose">
|
|
||||||
<textarea id="chat-prompt" placeholder="Ask a question or describe the task"></textarea>
|
|
||||||
<div class="form-row">
|
|
||||||
<button onclick="sendChat()" id="chat-send">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-pane">
|
|
||||||
<div class="chat-panel">
|
|
||||||
<div id="chat-status" class="chat-status">select a model to start</div>
|
<div id="chat-status" class="chat-status">select a model to start</div>
|
||||||
<div id="chat-history" class="chat-history empty">no messages yet</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="chat-history" class="chat-messages empty">Send a message to start this conversation.</div>
|
||||||
|
<div class="chat-compose">
|
||||||
|
<textarea id="chat-prompt" placeholder="Message…" rows="1"></textarea>
|
||||||
|
<button type="button" onclick="sendChat()" id="chat-send">Send</button>
|
||||||
|
</div>
|
||||||
|
<div class="chat-hint">Enter to send · Shift+Enter for a new line · Sessions are saved in this browser</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -604,7 +668,198 @@ let lastStats = null;
|
|||||||
let availableModels = [];
|
let availableModels = [];
|
||||||
let chatHistory = [];
|
let chatHistory = [];
|
||||||
let chatBusy = false;
|
let chatBusy = false;
|
||||||
|
let chatSessions = [];
|
||||||
|
let activeChatSessionId = "";
|
||||||
let selectedChatModel = localStorage.getItem("meshnet_chat_model") || "";
|
let selectedChatModel = localStorage.getItem("meshnet_chat_model") || "";
|
||||||
|
const CHAT_SESSIONS_KEY = "meshnet_chat_sessions_v1";
|
||||||
|
const CHAT_ACTIVE_SESSION_KEY = "meshnet_chat_active_session_v1";
|
||||||
|
const CHAT_SESSIONS_LIMIT = 50;
|
||||||
|
|
||||||
|
function newChatSessionId() {
|
||||||
|
if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
|
||||||
|
return "chat-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadChatSessionsStore() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CHAT_SESSIONS_KEY);
|
||||||
|
const parsed = raw ? JSON.parse(raw) : [];
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChatSessionsStore() {
|
||||||
|
localStorage.setItem(CHAT_SESSIONS_KEY, JSON.stringify(chatSessions));
|
||||||
|
if (activeChatSessionId) {
|
||||||
|
localStorage.setItem(CHAT_ACTIVE_SESSION_KEY, activeChatSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatSessionTitle(session) {
|
||||||
|
const firstUser = (session.messages || []).find(msg => msg.role === "user");
|
||||||
|
if (!firstUser || !firstUser.content) return "New chat";
|
||||||
|
const text = String(firstUser.content).trim().replace(/\s+/g, " ");
|
||||||
|
return text.length > 42 ? text.slice(0, 42) + "…" : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSessionTime(iso) {
|
||||||
|
if (!iso) return "";
|
||||||
|
const date = new Date(iso);
|
||||||
|
if (Number.isNaN(date.getTime())) return "";
|
||||||
|
const now = new Date();
|
||||||
|
const sameDay = date.toDateString() === now.toDateString();
|
||||||
|
if (sameDay) return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||||
|
return date.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveChatSession() {
|
||||||
|
return chatSessions.find(session => session.id === activeChatSessionId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistActiveChatSession() {
|
||||||
|
const session = getActiveChatSession();
|
||||||
|
if (!session) return;
|
||||||
|
session.messages = chatHistory.slice();
|
||||||
|
session.model = selectedChatModel || session.model || "";
|
||||||
|
session.title = chatSessionTitle(session);
|
||||||
|
session.updatedAt = new Date().toISOString();
|
||||||
|
chatSessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
||||||
|
if (chatSessions.length > CHAT_SESSIONS_LIMIT) {
|
||||||
|
chatSessions = chatSessions.slice(0, CHAT_SESSIONS_LIMIT);
|
||||||
|
if (!chatSessions.some(item => item.id === activeChatSessionId)) {
|
||||||
|
activeChatSessionId = chatSessions[0].id;
|
||||||
|
chatHistory = chatSessions[0].messages.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveChatSessionsStore();
|
||||||
|
renderChatSessionList();
|
||||||
|
renderChatMainTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewChatSession() {
|
||||||
|
if (chatBusy) return;
|
||||||
|
const session = {
|
||||||
|
id: newChatSessionId(),
|
||||||
|
title: "New chat",
|
||||||
|
model: selectedChatModel || "",
|
||||||
|
messages: [],
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
chatSessions.unshift(session);
|
||||||
|
activeChatSessionId = session.id;
|
||||||
|
chatHistory = [];
|
||||||
|
saveChatSessionsStore();
|
||||||
|
renderChatSessionList();
|
||||||
|
renderChatHistory();
|
||||||
|
renderChatMainTitle();
|
||||||
|
renderChatAuthHint();
|
||||||
|
const promptEl = $("chat-prompt");
|
||||||
|
if (promptEl) promptEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectChatSession(sessionId) {
|
||||||
|
if (chatBusy || sessionId === activeChatSessionId) return;
|
||||||
|
const session = chatSessions.find(item => item.id === sessionId);
|
||||||
|
if (!session) return;
|
||||||
|
activeChatSessionId = session.id;
|
||||||
|
chatHistory = (session.messages || []).slice();
|
||||||
|
if (session.model) {
|
||||||
|
selectedChatModel = session.model;
|
||||||
|
localStorage.setItem("meshnet_chat_model", selectedChatModel);
|
||||||
|
const select = $("chat-model");
|
||||||
|
if (select) select.value = selectedChatModel;
|
||||||
|
}
|
||||||
|
localStorage.setItem(CHAT_ACTIVE_SESSION_KEY, activeChatSessionId);
|
||||||
|
renderChatSessionList();
|
||||||
|
renderChatHistory();
|
||||||
|
renderChatMainTitle();
|
||||||
|
renderChatAuthHint();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteChatSession(sessionId, event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
if (chatBusy) return;
|
||||||
|
const index = chatSessions.findIndex(item => item.id === sessionId);
|
||||||
|
if (index < 0) return;
|
||||||
|
chatSessions.splice(index, 1);
|
||||||
|
if (activeChatSessionId === sessionId) {
|
||||||
|
if (chatSessions.length) {
|
||||||
|
activeChatSessionId = chatSessions[0].id;
|
||||||
|
chatHistory = (chatSessions[0].messages || []).slice();
|
||||||
|
if (chatSessions[0].model) {
|
||||||
|
selectedChatModel = chatSessions[0].model;
|
||||||
|
localStorage.setItem("meshnet_chat_model", selectedChatModel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saveChatSessionsStore();
|
||||||
|
createNewChatSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveChatSessionsStore();
|
||||||
|
renderChatSessionList();
|
||||||
|
renderChatHistory();
|
||||||
|
renderChatMainTitle();
|
||||||
|
renderChatModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initChatSessions() {
|
||||||
|
chatSessions = loadChatSessionsStore();
|
||||||
|
activeChatSessionId = localStorage.getItem(CHAT_ACTIVE_SESSION_KEY) || "";
|
||||||
|
const active = chatSessions.find(session => session.id === activeChatSessionId);
|
||||||
|
if (!active) {
|
||||||
|
if (chatSessions.length) {
|
||||||
|
activeChatSessionId = chatSessions[0].id;
|
||||||
|
chatHistory = (chatSessions[0].messages || []).slice();
|
||||||
|
if (chatSessions[0].model) selectedChatModel = chatSessions[0].model;
|
||||||
|
} else {
|
||||||
|
createNewChatSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chatHistory = (active.messages || []).slice();
|
||||||
|
if (active.model) selectedChatModel = active.model;
|
||||||
|
}
|
||||||
|
renderChatSessionList();
|
||||||
|
renderChatMainTitle();
|
||||||
|
renderChatHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChatMainTitle() {
|
||||||
|
const el = $("chat-main-title");
|
||||||
|
if (!el) return;
|
||||||
|
const session = getActiveChatSession();
|
||||||
|
el.textContent = session ? chatSessionTitle(session) : "New chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChatSessionList() {
|
||||||
|
const list = $("chat-session-list");
|
||||||
|
if (!list) return;
|
||||||
|
if (!chatSessions.length) {
|
||||||
|
list.classList.add("empty");
|
||||||
|
list.innerHTML = "no chats yet";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.classList.remove("empty");
|
||||||
|
list.innerHTML = chatSessions.map(session => {
|
||||||
|
const active = session.id === activeChatSessionId ? " active" : "";
|
||||||
|
const title = esc(chatSessionTitle(session));
|
||||||
|
const model = esc(short(session.model || "no model", 18));
|
||||||
|
const when = esc(formatSessionTime(session.updatedAt || session.createdAt));
|
||||||
|
const id = JSON.stringify(session.id);
|
||||||
|
return `<button type="button" class="chat-session-item${active}" onclick="selectChatSession(${id})">` +
|
||||||
|
`<div class="chat-session-title">${title}</div>` +
|
||||||
|
`<div class="chat-session-meta">${model}${when ? " · " + when : ""}</div>` +
|
||||||
|
`<span class="chat-session-delete" role="button" title="Delete chat" onclick="deleteChatSession(${id}, event)">×</span>` +
|
||||||
|
`</button>`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function switchDashboardTab(name) {
|
function switchDashboardTab(name) {
|
||||||
if (name === "admin" && !isAdmin) name = "overview";
|
if (name === "admin" && !isAdmin) name = "overview";
|
||||||
@@ -632,17 +887,21 @@ function renderChatStatus(text) {
|
|||||||
|
|
||||||
function renderChatHistory() {
|
function renderChatHistory() {
|
||||||
const history = $("chat-history");
|
const history = $("chat-history");
|
||||||
|
if (!history) return;
|
||||||
if (!chatHistory.length) {
|
if (!chatHistory.length) {
|
||||||
history.classList.add("empty");
|
history.classList.add("empty");
|
||||||
history.innerHTML = "no messages yet";
|
history.innerHTML = "Send a message to start this conversation.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
history.classList.remove("empty");
|
history.classList.remove("empty");
|
||||||
history.innerHTML = chatHistory.map(msg => {
|
history.innerHTML = chatHistory.map(msg => {
|
||||||
const roleClass = msg.role === "user" ? "chat-role-user" : msg.role === "assistant" ? "chat-role-assistant" : "chat-role-error";
|
const role = msg.role === "user" ? "user" : msg.role === "assistant" ? "assistant" : "error";
|
||||||
const label = msg.role === "user" ? "user" : msg.role === "assistant" ? "assistant" : "error";
|
const label = role === "user" ? "You" : role === "assistant" ? "Assistant" : "Error";
|
||||||
const meta = msg.model ? ` <span class="dim">· ${esc(short(msg.model, 24))}</span>` : "";
|
const meta = msg.model ? ` <span class="dim">· ${esc(short(msg.model, 24))}</span>` : "";
|
||||||
return `<div class="chat-message"><div class="chat-role ${roleClass}">${label}${meta}</div><div>${esc(msg.content)}</div></div>`;
|
return `<div class="chat-bubble-wrap ${role}">` +
|
||||||
|
`<div class="chat-bubble-label">${label}${meta}</div>` +
|
||||||
|
`<div class="chat-bubble ${role}">${esc(msg.content)}</div>` +
|
||||||
|
`</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
history.scrollTop = history.scrollHeight;
|
history.scrollTop = history.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -674,12 +933,13 @@ function renderChatModels() {
|
|||||||
function selectChatModel(value) {
|
function selectChatModel(value) {
|
||||||
selectedChatModel = value || "";
|
selectedChatModel = value || "";
|
||||||
localStorage.setItem("meshnet_chat_model", selectedChatModel);
|
localStorage.setItem("meshnet_chat_model", selectedChatModel);
|
||||||
}
|
const session = getActiveChatSession();
|
||||||
|
if (session) {
|
||||||
function clearChatHistory() {
|
session.model = selectedChatModel;
|
||||||
chatHistory = [];
|
session.updatedAt = new Date().toISOString();
|
||||||
renderChatHistory();
|
saveChatSessionsStore();
|
||||||
renderChatStatus("history cleared");
|
renderChatSessionList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chatAuthToken() {
|
function chatAuthToken() {
|
||||||
@@ -937,6 +1197,7 @@ async function sendChat() {
|
|||||||
promptEl.value = "";
|
promptEl.value = "";
|
||||||
chatHistory.push({ role: "user", content: prompt, model: selectedChatModel });
|
chatHistory.push({ role: "user", content: prompt, model: selectedChatModel });
|
||||||
renderChatHistory();
|
renderChatHistory();
|
||||||
|
persistActiveChatSession();
|
||||||
renderChatStatus("sending request…");
|
renderChatStatus("sending request…");
|
||||||
const r = await apiCall("/v1/chat/completions", "POST", body, bearerToken);
|
const r = await apiCall("/v1/chat/completions", "POST", body, bearerToken);
|
||||||
chatBusy = false;
|
chatBusy = false;
|
||||||
@@ -947,6 +1208,7 @@ async function sendChat() {
|
|||||||
: "request failed";
|
: "request failed";
|
||||||
chatHistory.push({ role: "error", content: error, model: selectedChatModel });
|
chatHistory.push({ role: "error", content: error, model: selectedChatModel });
|
||||||
renderChatHistory();
|
renderChatHistory();
|
||||||
|
persistActiveChatSession();
|
||||||
renderChatStatus(error);
|
renderChatStatus(error);
|
||||||
promptEl.focus();
|
promptEl.focus();
|
||||||
return;
|
return;
|
||||||
@@ -959,12 +1221,29 @@ async function sendChat() {
|
|||||||
model: selectedChatModel,
|
model: selectedChatModel,
|
||||||
});
|
});
|
||||||
renderChatHistory();
|
renderChatHistory();
|
||||||
|
persistActiveChatSession();
|
||||||
renderChatStatus(usage
|
renderChatStatus(usage
|
||||||
? `done: ${usage.total_tokens ?? "?"} tokens`
|
? `done: ${usage.total_tokens ?? "?"} tokens`
|
||||||
: "done");
|
: "done");
|
||||||
promptEl.focus();
|
promptEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindChatPromptShortcuts() {
|
||||||
|
const promptEl = $("chat-prompt");
|
||||||
|
if (!promptEl || promptEl.dataset.bound === "1") return;
|
||||||
|
promptEl.dataset.bound = "1";
|
||||||
|
promptEl.addEventListener("keydown", event => {
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
sendChat();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
promptEl.addEventListener("input", () => {
|
||||||
|
promptEl.style.height = "auto";
|
||||||
|
promptEl.style.height = Math.min(promptEl.scrollHeight, 160) + "px";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function renderAdminPanel() {
|
async function renderAdminPanel() {
|
||||||
const r = await apiCall("/v1/admin/accounts");
|
const r = await apiCall("/v1/admin/accounts");
|
||||||
if (!r.ok) { setAdminMode(false); return; }
|
if (!r.ok) { setAdminMode(false); return; }
|
||||||
@@ -1034,6 +1313,8 @@ async function refresh() {
|
|||||||
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
$("refreshed").textContent = "refreshed " + new Date().toLocaleTimeString();
|
||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
|
initChatSessions();
|
||||||
|
bindChatPromptShortcuts();
|
||||||
renderAccountPanel();
|
renderAccountPanel();
|
||||||
renderChatModels();
|
renderChatModels();
|
||||||
renderChatHistory();
|
renderChatHistory();
|
||||||
|
|||||||
Reference in New Issue
Block a user