new chat layout

This commit is contained in:
Dobromir Popov
2026-07-07 18:42:05 +02:00
parent cdd2699e63
commit 16614855bc

View File

@@ -8,14 +8,20 @@
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#c9d1d9;
--dim:#8b949e; --accent:#58a6ff; --ok:#3fb950; --bad:#f85149; --warn:#d29922; }
* { box-sizing:border-box; }
html, body { height:100%; }
body { margin:0; background:var(--bg); color:var(--fg);
font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
body.chat-tab-active { overflow:hidden; height:100dvh; display:flex; flex-direction:column; }
header { display:flex; align-items:baseline; gap:14px; padding:14px 20px;
border-bottom:1px solid var(--border); }
border-bottom:1px solid var(--border); flex-shrink:0; }
header h1 { font-size:16px; margin:0; color:var(--accent); }
header .meta { color:var(--dim); font-size:12px; }
main { display:grid; grid-template-columns:repeat(auto-fit,minmax(340px,1fr));
gap:14px; padding:14px 20px; }
body.chat-tab-active main {
flex:1; min-height:0; display:flex; flex-direction:column;
padding:0; gap:0; overflow:hidden;
}
section { background:var(--panel); border:1px solid var(--border);
border-radius:8px; padding:12px 14px; min-height:80px; }
section h2 { margin:0 0 8px; font-size:12px; text-transform:uppercase;
@@ -53,90 +59,116 @@
.tabs { display:flex; gap:10px; margin-bottom:8px; }
.tabs a { color:var(--dim); cursor:pointer; }
.tabs a.active { color:var(--accent); border-bottom:1px solid var(--accent); }
.dashboard-tabs { display:flex; gap:10px; padding:10px 20px 0; border-bottom:1px solid var(--border); }
.dashboard-tabs { display:flex; gap:10px; padding:10px 20px 0; border-bottom:1px solid var(--border); flex-shrink:0; }
.dashboard-tabs button { border:0; border-bottom:1px solid transparent; border-radius:0;
background:transparent; color:var(--dim); padding:5px 0 8px; }
.dashboard-tabs button.active { color:var(--accent); border-bottom-color:var(--accent); }
.wide { grid-column:1 / -1; }
section[hidden] { display:none !important; }
section.chat-section {
padding:0; border:0; border-radius:0; background:var(--bg); min-height:0;
}
body.chat-tab-active section.chat-section {
flex:1; display:flex !important; flex-direction:column; min-height:0;
}
.chat-app {
display:grid; grid-template-columns:260px minmax(0, 1fr); gap:0;
min-height:560px; border:1px solid var(--border); border-radius:8px;
overflow:hidden; background:var(--bg);
flex:1; min-height:0; overflow:hidden; background:var(--bg);
}
.chat-sidebar {
display:flex; flex-direction:column; min-height:0;
border-right:1px solid var(--border); background:var(--panel);
}
.chat-sidebar-header {
display:flex; flex-direction:column; gap:8px; padding:12px;
border-bottom:1px solid var(--border);
.chat-new-btn {
margin:12px; width:calc(100% - 24px); text-align:left;
border:1px solid var(--border); border-radius:8px; padding:10px 12px;
background:transparent; color:var(--fg);
}
.chat-sidebar-header button { width:100%; text-align:left; }
.chat-new-btn:hover { background:#10151d; border-color:var(--accent); }
.chat-session-list {
flex:1; overflow:auto; padding:8px; display:flex; flex-direction:column; gap:4px;
flex:1; overflow:auto; padding:0 8px 12px; display:flex; flex-direction:column; gap:2px;
}
.chat-session-list.empty-state {
justify-content:center; align-items:center; color:var(--dim); font-style:italic;
padding:24px 12px;
}
.chat-session-item {
position:relative; display:block; width:100%; text-align:left;
padding:8px 28px 8px 10px; border:1px solid transparent; border-radius:6px;
padding:10px 32px 10px 12px; border:1px solid transparent; border-radius:8px;
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-item:hover { background:#10151d; }
.chat-session-item.active { background:#10151d; border-color:#30363d; }
.chat-session-title {
font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.chat-session-meta {
margin-top:2px; font-size:11px; color:var(--dim);
margin-top:3px; 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;
position:absolute; top:50%; right:6px; transform:translateY(-50%);
padding:2px 6px; min-width:0; border:0; border-radius:4px;
background:transparent; color:var(--dim); line-height:1.2; opacity:0;
}
.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-session-item:hover .chat-session-delete,
.chat-session-item.active .chat-session-delete { opacity:1; }
.chat-session-delete:hover { color:var(--bad); background:#1a1012; }
.chat-main { display:flex; flex-direction:column; min-height:0; min-width:0; }
.chat-toolbar {
display:flex; gap:12px; align-items:center; flex-shrink:0;
padding:10px 16px; border-bottom:1px solid var(--border); background:var(--panel);
}
.chat-main-header label {
.chat-toolbar 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-toolbar select { min-width:220px; max-width:min(420px, 50vw); }
.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;
flex:1; overflow:auto; padding:24px 16px; min-height:0;
}
.chat-messages.empty {
justify-content:center; align-items:center; color:var(--dim); font-style:italic;
.chat-messages-inner {
max-width:768px; margin:0 auto; display:flex; flex-direction:column; gap:20px;
}
.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-messages.empty .chat-messages-inner {
min-height:100%; justify-content:center; align-items:center;
color:var(--dim); font-size:14px;
}
.chat-row { display:flex; width:100%; }
.chat-row.user { justify-content:flex-end; }
.chat-row.assistant, .chat-row.error { justify-content:flex-start; }
.chat-bubble {
padding:10px 12px; border-radius:12px; line-height:1.45;
white-space:pre-wrap; word-break:break-word;
max-width:85%; padding:12px 14px; border-radius:16px; line-height:1.55;
white-space:pre-wrap; word-break:break-word; font-size:13px;
}
.chat-bubble.user {
background:#1f3a5f; border:1px solid #264a73; border-bottom-right-radius:4px;
}
.chat-bubble.assistant {
background:transparent; border:0; padding-left:0; padding-right:0; max-width:100%;
}
.chat-bubble.error {
background:#1a1012; border:1px solid #5c2020; color:#ffb4b4; border-bottom-left-radius:4px;
}
.chat-compose-wrap {
flex-shrink:0; padding:12px 16px 16px; border-top:1px solid var(--border);
background:var(--panel);
}
.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);
display:flex; gap:8px; align-items:flex-end; max-width:768px; margin:0 auto;
padding:10px 12px; border:1px solid var(--border); border-radius:16px;
background:var(--bg);
}
.chat-compose:focus-within { border-color:var(--accent); }
.chat-compose textarea {
flex:1; min-height:44px; max-height:160px; resize:none; width:auto;
flex:1; min-height:24px; max-height:200px; resize:none; width:auto;
border:0; background:transparent; padding:2px 0; outline:none;
}
.chat-compose button { align-self:flex-end; min-width:72px; }
.chat-hint { padding:0 12px 10px; color:var(--dim); font-size:11px; }
.chat-compose button {
flex-shrink:0; min-width:36px; height:36px; padding:0;
border-radius:8px; border:1px solid var(--border);
}
.chat-compose button:disabled { opacity:.45; cursor:not-allowed; }
.console {
background:var(--bg); border:1px solid var(--border); border-radius:6px;
min-height:160px; max-height:280px; overflow:auto; padding:7px 9px;
@@ -174,29 +206,28 @@
<section data-tab="overview"><h2>Nodes &amp; coverage</h2><div id="nodes" 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="chat" class="wide">
<h2>Chat</h2>
<section data-tab="chat" class="wide chat-section">
<div class="chat-app">
<aside class="chat-sidebar">
<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>
<button type="button" class="chat-new-btn" onclick="createNewChatSession()">+ New chat</button>
<div id="chat-session-list" class="chat-session-list empty-state">No chats yet</div>
</aside>
<div class="chat-main">
<div class="chat-main-header">
<div class="chat-toolbar">
<label>Model
<select id="chat-model" onchange="selectChatModel(this.value)"></select>
</label>
<div id="chat-main-title" class="chat-main-title">New chat</div>
<div id="chat-status" class="chat-status">select a model to start</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 id="chat-history" class="chat-messages empty">
<div class="chat-messages-inner">Send a message to start this conversation.</div>
</div>
<div class="chat-compose-wrap">
<div class="chat-compose">
<textarea id="chat-prompt" placeholder="Message…" rows="1" aria-label="Message"></textarea>
<button type="button" onclick="sendChat()" id="chat-send" title="Send (Enter)"></button>
</div>
</div>
<div class="chat-hint">Enter to send · Shift+Enter for a new line · Sessions are saved in this browser</div>
</div>
</div>
</section>
@@ -735,7 +766,13 @@ function persistActiveChatSession() {
}
saveChatSessionsStore();
renderChatSessionList();
renderChatMainTitle();
}
function clearChatPrompt() {
const promptEl = $("chat-prompt");
if (!promptEl) return;
promptEl.value = "";
promptEl.style.height = "auto";
}
function createNewChatSession() {
@@ -751,21 +788,23 @@ function createNewChatSession() {
chatSessions.unshift(session);
activeChatSessionId = session.id;
chatHistory = [];
clearChatPrompt();
saveChatSessionsStore();
renderChatSessionList();
renderChatHistory();
renderChatMainTitle();
renderChatAuthHint();
const promptEl = $("chat-prompt");
if (promptEl) promptEl.focus();
}
function selectChatSession(sessionId) {
if (chatBusy || sessionId === activeChatSessionId) return;
if (chatBusy) return;
const session = chatSessions.find(item => item.id === sessionId);
if (!session) return;
if (sessionId === activeChatSessionId) return;
activeChatSessionId = session.id;
chatHistory = (session.messages || []).slice();
clearChatPrompt();
if (session.model) {
selectedChatModel = session.model;
localStorage.setItem("meshnet_chat_model", selectedChatModel);
@@ -775,7 +814,6 @@ function selectChatSession(sessionId) {
localStorage.setItem(CHAT_ACTIVE_SESSION_KEY, activeChatSessionId);
renderChatSessionList();
renderChatHistory();
renderChatMainTitle();
renderChatAuthHint();
}
@@ -792,6 +830,7 @@ function deleteChatSession(sessionId, event) {
if (chatSessions.length) {
activeChatSessionId = chatSessions[0].id;
chatHistory = (chatSessions[0].messages || []).slice();
clearChatPrompt();
if (chatSessions[0].model) {
selectedChatModel = chatSessions[0].model;
localStorage.setItem("meshnet_chat_model", selectedChatModel);
@@ -805,7 +844,6 @@ function deleteChatSession(sessionId, event) {
saveChatSessionsStore();
renderChatSessionList();
renderChatHistory();
renderChatMainTitle();
renderChatModels();
}
@@ -827,37 +865,31 @@ function initChatSessions() {
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";
list.className = "chat-session-list empty-state";
list.innerHTML = "No chats yet";
return;
}
list.classList.remove("empty");
list.className = "chat-session-list";
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})">` +
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});}">` +
`<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>`;
(when ? `<div class="chat-session-meta">${when}</div>` : "") +
`<button type="button" class="chat-session-delete" title="Delete chat"` +
` onclick="deleteChatSession(${id}, event)">×</button>` +
`</div>`;
}).join("");
}
@@ -865,11 +897,16 @@ function switchDashboardTab(name) {
if (name === "admin" && !isAdmin) name = "overview";
if (name === "billing" && !isLoggedIn) name = "overview";
dashboardTab = name;
document.body.classList.toggle("chat-tab-active", name === "chat");
updateSectionVisibility();
for (const tabName of ["overview", "chat", "billing", "admin"]) {
const button = $("tab-" + tabName);
if (button) button.classList.toggle("active", tabName === dashboardTab);
}
if (name === "chat") {
const promptEl = $("chat-prompt");
if (promptEl) promptEl.focus();
}
}
function updateSectionVisibility() {
@@ -889,20 +926,16 @@ function renderChatHistory() {
const history = $("chat-history");
if (!history) return;
if (!chatHistory.length) {
history.classList.add("empty");
history.innerHTML = "Send a message to start this conversation.";
history.className = "chat-messages empty";
history.innerHTML = '<div class="chat-messages-inner">Send a message to start this conversation.</div>';
return;
}
history.classList.remove("empty");
history.innerHTML = chatHistory.map(msg => {
history.className = "chat-messages";
const rows = chatHistory.map(msg => {
const role = 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>` : "";
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>`;
return `<div class="chat-row ${role}"><div class="chat-bubble ${role}">${esc(msg.content)}</div></div>`;
}).join("");
history.innerHTML = `<div class="chat-messages-inner">${rows}</div>`;
history.scrollTop = history.scrollHeight;
}
@@ -1195,6 +1228,7 @@ async function sendChat() {
chatBusy = true;
$("chat-send").disabled = true;
promptEl.value = "";
promptEl.style.height = "auto";
chatHistory.push({ role: "user", content: prompt, model: selectedChatModel });
renderChatHistory();
persistActiveChatSession();
@@ -1240,7 +1274,7 @@ function bindChatPromptShortcuts() {
});
promptEl.addEventListener("input", () => {
promptEl.style.height = "auto";
promptEl.style.height = Math.min(promptEl.scrollHeight, 160) + "px";
promptEl.style.height = Math.min(promptEl.scrollHeight, 200) + "px";
});
}