feat: Network Status Dashboard frontend
- Host cards grid with live status (green/red/yellow badges), latency, expandable details - Service table with category, URL/port, status, latency - Local services tab (from ss -tlnp) - Auto-refresh every 30s with visual countdown bar - Manual refresh button with rotate animation - Summary bar (online/degraded/offline/total counts) - Mobile-responsive: single-column cards, scrollable tables - Tab-based navigation between Hosts / Services / Local Services
This commit is contained in:
+275
-3
@@ -300,11 +300,283 @@ function showToast(message, type) {
|
|||||||
}, 4000);
|
}, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Event listeners ──────────────────────────────────────────────────
|
// ── Agent Chat ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let chatState = {
|
||||||
|
profiles: [],
|
||||||
|
activeProfile: null,
|
||||||
|
messages: {},
|
||||||
|
sending: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROFILE_ICONS = {
|
||||||
|
coder: '💻',
|
||||||
|
assistant: '🤖',
|
||||||
|
artist: '🎨',
|
||||||
|
kanban: '📋',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadAgentProfiles() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/agents');
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
chatState.profiles = data.agents || [];
|
||||||
|
renderProfileTabs();
|
||||||
|
// If we have an active profile, re-check its status
|
||||||
|
if (chatState.activeProfile) {
|
||||||
|
selectProfile(chatState.activeProfile);
|
||||||
|
} else if (chatState.profiles.length > 0) {
|
||||||
|
// Auto-select first online profile, or first profile
|
||||||
|
const online = chatState.profiles.find(p => p.status === 'online');
|
||||||
|
selectProfile(online || chatState.profiles[0]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load profiles:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProfileTabs() {
|
||||||
|
const container = document.getElementById('chat-profile-tabs');
|
||||||
|
container.innerHTML = '';
|
||||||
|
chatState.profiles.forEach(p => {
|
||||||
|
const tab = document.createElement('button');
|
||||||
|
tab.className = 'chat-profile-tab';
|
||||||
|
if (p.profile === chatState.activeProfile?.profile) {
|
||||||
|
tab.classList.add('active');
|
||||||
|
}
|
||||||
|
const icon = PROFILE_ICONS[p.profile] || '🤖';
|
||||||
|
tab.innerHTML = `
|
||||||
|
<span class="status-dot ${p.status}"></span>
|
||||||
|
${icon} ${p.label || p.profile}
|
||||||
|
`;
|
||||||
|
tab.title = p.status === 'online' ? 'Online' : 'Offline';
|
||||||
|
tab.addEventListener('click', () => selectProfile(p));
|
||||||
|
container.appendChild(tab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectProfile(profile) {
|
||||||
|
chatState.activeProfile = profile;
|
||||||
|
// Update tab UI
|
||||||
|
document.querySelectorAll('.chat-profile-tab').forEach(tab => {
|
||||||
|
const txt = tab.textContent.trim();
|
||||||
|
tab.classList.toggle('active', txt.includes(profile.label || profile.profile));
|
||||||
|
});
|
||||||
|
// Clear messages area
|
||||||
|
const msgArea = document.getElementById('chat-messages');
|
||||||
|
msgArea.innerHTML = '';
|
||||||
|
// Show welcome if no history, or loading
|
||||||
|
if (!chatState.messages[profile.profile]) {
|
||||||
|
showWelcome(profile);
|
||||||
|
}
|
||||||
|
// Enable input
|
||||||
|
document.getElementById('chat-input').disabled = false;
|
||||||
|
document.getElementById('chat-input').focus();
|
||||||
|
updateSendButton();
|
||||||
|
// Load history
|
||||||
|
loadConversationHistory(profile.profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWelcome(profile) {
|
||||||
|
const msgArea = document.getElementById('chat-messages');
|
||||||
|
const icon = PROFILE_ICONS[profile.profile] || '🤖';
|
||||||
|
msgArea.innerHTML = `
|
||||||
|
<div class="chat-welcome">
|
||||||
|
<div class="chat-welcome-icon">${icon}</div>
|
||||||
|
<h3>${profile.label || profile.profile}</h3>
|
||||||
|
<p>${profile.status === 'online'
|
||||||
|
? 'This agent is online. Type a message to start a conversation.'
|
||||||
|
: 'This agent is offline. Start the gateway to chat.'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConversationHistory(profileName) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/agents/${encodeURIComponent(profileName)}/history?limit=50`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
const messages = data.messages || [];
|
||||||
|
chatState.messages[profileName] = messages;
|
||||||
|
|
||||||
|
// Only render if this is still the active profile
|
||||||
|
if (chatState.activeProfile?.profile === profileName) {
|
||||||
|
renderMessages(messages);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load history:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessages(messages) {
|
||||||
|
const msgArea = document.getElementById('chat-messages');
|
||||||
|
msgArea.innerHTML = '';
|
||||||
|
if (!messages || messages.length === 0) {
|
||||||
|
if (chatState.activeProfile) {
|
||||||
|
showWelcome(chatState.activeProfile);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
messages.forEach(msg => appendMessageBubble(msg, false));
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendMessageBubble(msg, animate = true) {
|
||||||
|
const msgArea = document.getElementById('chat-messages');
|
||||||
|
const isUser = msg.role === 'user';
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = `chat-message ${isUser ? 'user' : 'agent'}`;
|
||||||
|
if (!animate) div.style.animation = 'none';
|
||||||
|
|
||||||
|
const icon = chatState.activeProfile
|
||||||
|
? (PROFILE_ICONS[chatState.activeProfile.profile] || '🤖')
|
||||||
|
: '🤖';
|
||||||
|
|
||||||
|
const time = msg.created_at
|
||||||
|
? new Date(msg.created_at * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
|
: '';
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="bubble">${escapeHtml(msg.content)}</div>
|
||||||
|
<div class="msg-meta">
|
||||||
|
${isUser ? '' : `<span class="msg-profile-icon">${icon}</span>`}
|
||||||
|
<span>${isUser ? 'You' : (chatState.activeProfile?.label || 'Agent')}</span>
|
||||||
|
${time ? '<span>· ' + time + '</span>' : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
msgArea.appendChild(div);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
const msgArea = document.getElementById('chat-messages');
|
||||||
|
msgArea.scrollTop = msgArea.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTypingIndicator(show) {
|
||||||
|
document.getElementById('chat-typing').hidden = !show;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
if (chatState.sending) return;
|
||||||
|
const input = document.getElementById('chat-input');
|
||||||
|
const message = input.value.trim();
|
||||||
|
if (!message || !chatState.activeProfile) return;
|
||||||
|
|
||||||
|
chatState.sending = true;
|
||||||
|
input.disabled = true;
|
||||||
|
document.getElementById('chat-send-btn').disabled = true;
|
||||||
|
document.getElementById('chat-send-btn').textContent = '...';
|
||||||
|
|
||||||
|
// Optimistically show user message
|
||||||
|
appendMessageBubble({ role: 'user', content: message, created_at: Date.now() / 1000 });
|
||||||
|
input.value = '';
|
||||||
|
input.style.height = 'auto';
|
||||||
|
|
||||||
|
// Show typing indicator
|
||||||
|
showTypingIndicator(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/agents/${encodeURIComponent(chatState.activeProfile.profile)}/send`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
appendMessageBubble({
|
||||||
|
role: 'agent',
|
||||||
|
content: '⚠️ Error: ' + (data.error || `HTTP ${res.status}`),
|
||||||
|
created_at: Date.now() / 1000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
appendMessageBubble({
|
||||||
|
role: 'agent',
|
||||||
|
content: data.response,
|
||||||
|
created_at: Date.now() / 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
appendMessageBubble({
|
||||||
|
role: 'agent',
|
||||||
|
content: '⚠️ Network error: ' + err.message,
|
||||||
|
created_at: Date.now() / 1000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
chatState.sending = false;
|
||||||
|
showTypingIndicator(false);
|
||||||
|
input.disabled = false;
|
||||||
|
document.getElementById('chat-send-btn').disabled = false;
|
||||||
|
document.getElementById('chat-send-btn').textContent = 'Send';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSendButton() {
|
||||||
|
const input = document.getElementById('chat-input');
|
||||||
|
const btn = document.getElementById('chat-send-btn');
|
||||||
|
btn.disabled = !input.value.trim() || !chatState.activeProfile || chatState.sending;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chat event listeners ──────────────────────────────────────────
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.getElementById('config-save-btn').addEventListener('click', saveFile);
|
// Config editor listeners
|
||||||
document.getElementById('config-back-btn').addEventListener('click', goBackToList);
|
const configSaveBtn = document.getElementById('config-save-btn');
|
||||||
|
const configBackBtn = document.getElementById('config-back-btn');
|
||||||
|
if (configSaveBtn) configSaveBtn.addEventListener('click', saveFile);
|
||||||
|
if (configBackBtn) configBackBtn.addEventListener('click', goBackToList);
|
||||||
|
|
||||||
|
// Network dashboard
|
||||||
initNetworkDashboard();
|
initNetworkDashboard();
|
||||||
|
|
||||||
|
// Chat input
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
const sendBtn = document.getElementById('chat-send-btn');
|
||||||
|
|
||||||
|
if (chatInput) {
|
||||||
|
chatInput.addEventListener('input', () => {
|
||||||
|
updateSendButton();
|
||||||
|
// Auto-resize
|
||||||
|
chatInput.style.height = 'auto';
|
||||||
|
chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
chatInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendBtn) {
|
||||||
|
sendBtn.addEventListener('click', sendMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load profiles when agents tab is navigated to
|
||||||
|
const agentsLink = document.querySelector('.nav-links a[href="#agents"]');
|
||||||
|
if (agentsLink) {
|
||||||
|
agentsLink.addEventListener('click', () => {
|
||||||
|
setTimeout(loadAgentProfiles, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also load if we start on agents tab
|
||||||
|
if (!window.location.hash || window.location.hash === '#agents' || window.location.hash === '') {
|
||||||
|
setTimeout(loadAgentProfiles, 200);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Network Dashboard ────────────────────────────────────────────────
|
// ── Network Dashboard ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -32,6 +32,276 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
.content { margin-left: 0; padding: 12px; }
|
.content { margin-left: 0; padding: 12px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Agent Chat ──────────────────────────────────────────────────── */
|
||||||
|
#chat-app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 0 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-strip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profiles-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tab:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tab.active {
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tab .status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tab .status-dot.online {
|
||||||
|
background: var(--success);
|
||||||
|
box-shadow: 0 0 6px rgba(63, 185, 80, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-profile-tab .status-dot.offline {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-welcome {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-welcome-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-welcome h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-welcome p {
|
||||||
|
font-size: 13px;
|
||||||
|
max-width: 360px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 80%;
|
||||||
|
animation: msg-in 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes msg-in {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.user {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.agent {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message .bubble {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 13px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.user .bubble {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.agent .bubble {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message .msg-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 2px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message.user .msg-meta {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message .msg-profile-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-typing {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-muted);
|
||||||
|
animation: typing-bounce 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes typing-bounce {
|
||||||
|
0%, 60%, 100% { transform: translateY(0); }
|
||||||
|
30% { transform: translateY(-6px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-label {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area textarea {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
min-height: 40px;
|
||||||
|
max-height: 120px;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area textarea:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area textarea::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area .btn {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#chat-app {
|
||||||
|
height: calc(100vh - 180px);
|
||||||
|
}
|
||||||
|
.chat-message {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
.chat-message .bubble {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.chat-profile-tab {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Config Panel ───────────────────────────────────────────────── */
|
/* ── Config Panel ───────────────────────────────────────────────── */
|
||||||
.config-panel { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
.config-panel { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||||
.config-panel h2 { margin: 0; font-size: 16px; }
|
.config-panel h2 { margin: 0; font-size: 16px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user