new chat layout
This commit is contained in:
@@ -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 & 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";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user