Delete chat-popout.html

This commit is contained in:
fishtank-dashboard
2026-03-19 22:19:26 -07:00
committed by GitHub
parent 4f3624dd33
commit 55882b2ed4

View File

@@ -1,383 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FISHTANK // CHAT</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Bebas+Neue&family=DM+Sans:wght@300;400;500&display=swap');
:root {
--bg: #080b0f;
--panel: #0d1117;
--border: #1a2332;
--accent: #00e5ff;
--accent2: #ff3d71;
--accent3: #ffe600;
--text: #c9d4e0;
--muted: #4a5568;
--green: #00ff88;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: 'DM Sans', sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
header {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--panel);
flex-shrink: 0;
}
.logo {
font-family: 'Bebas Neue', sans-serif;
font-size: 18px;
letter-spacing: 3px;
color: var(--accent);
}
.logo span { color: var(--accent2); }
.status {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Share Tech Mono', monospace;
font-size: 10px;
color: var(--muted);
}
.dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--muted);
}
.dot.live {
background: var(--green);
box-shadow: 0 0 8px var(--green);
animation: pulse 2s infinite;
}
.dot.error { background: var(--accent2); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.msg-count {
font-family: 'Share Tech Mono', monospace;
font-size: 10px;
color: var(--muted);
}
#chatFeed {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 0;
}
#chatFeed::-webkit-scrollbar { width: 4px; }
#chatFeed::-webkit-scrollbar-track { background: transparent; }
#chatFeed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.msg {
padding: 7px 14px;
border-bottom: 1px solid rgba(26,35,50,0.5);
animation: slideIn 0.2s ease;
line-height: 1.4;
}
.msg:hover { background: rgba(255,255,255,0.02); }
@keyframes slideIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-header {
display: flex;
align-items: baseline;
gap: 7px;
margin-bottom: 2px;
flex-wrap: wrap;
}
.msg-user {
font-family: 'Share Tech Mono', monospace;
font-size: 11px;
font-weight: bold;
flex-shrink: 0;
}
.msg-clan {
font-family: 'Share Tech Mono', monospace;
font-size: 9px;
color: var(--muted);
background: rgba(255,255,255,0.05);
padding: 1px 4px;
border-radius: 2px;
}
.msg-medals {
display: flex;
gap: 2px;
align-items: center;
}
.msg-endorsement {
font-family: 'Share Tech Mono', monospace;
font-size: 9px;
font-weight: bold;
letter-spacing: 1px;
opacity: 0.85;
}
.msg-time {
font-family: 'Share Tech Mono', monospace;
font-size: 9px;
color: var(--muted);
margin-left: auto;
flex-shrink: 0;
}
.msg-text {
font-size: 13px;
color: var(--text);
word-break: break-word;
}
.msg-text .mention {
color: var(--accent);
font-weight: 500;
}
.msg.fish { border-left: 2px solid var(--accent3); background: rgba(255,230,0,0.04); }
.msg.admin { border-left: 2px solid var(--accent2); background: rgba(255,61,113,0.06); }
.msg.mod { border-left: 2px solid var(--green); background: rgba(0,255,136,0.05); }
.badge {
font-family: 'Share Tech Mono', monospace;
font-size: 8px;
padding: 1px 5px;
border-radius: 2px;
letter-spacing: 1px;
flex-shrink: 0;
}
.badge.admin { background: rgba(255,61,113,0.2); color: var(--accent2); border: 1px solid rgba(255,61,113,0.4); }
.badge.mod { background: rgba(0,255,136,0.1); color: var(--green); border: 1px solid rgba(0,255,136,0.3); }
.badge.fish { background: rgba(255,230,0,0.1); color: var(--accent3); border: 1px solid rgba(255,230,0,0.3); }
.badge.gm { background: rgba(255,61,113,0.15); color: var(--accent2); border: 1px solid rgba(255,61,113,0.3); }
.empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--muted);
font-family: 'Share Tech Mono', monospace;
font-size: 12px;
gap: 10px;
}
.empty-icon { font-size: 32px; opacity: 0.3; }
.scroll-btn {
position: fixed;
bottom: 12px;
right: 12px;
background: var(--accent);
color: var(--bg);
border: none;
border-radius: 4px;
font-family: 'Share Tech Mono', monospace;
font-size: 10px;
padding: 5px 10px;
cursor: pointer;
letter-spacing: 1px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 10;
}
.scroll-btn.visible { opacity: 1; pointer-events: auto; }
</style>
</head>
<body>
<header>
<div class="logo">FISH<span>TANK</span> // CHAT</div>
<div class="status">
<div class="dot" id="wsDot"></div>
<span id="wsStatus">CONNECTING</span>
</div>
<div class="msg-count" id="msgCount">0 messages</div>
</header>
<div id="chatFeed">
<div class="empty">
<div class="empty-icon">💬</div>
<span>Waiting for messages...</span>
</div>
</div>
<button class="scroll-btn" id="scrollBtn" onclick="scrollToBottom()">▼ NEW MESSAGES</button>
<script>
const feed = document.getElementById('chatFeed');
const scrollBtn = document.getElementById('scrollBtn');
let msgCount = 0;
let autoScroll = true;
let ws = null;
let reconnectTimer = null;
feed.addEventListener('scroll', () => {
const atBottom = feed.scrollHeight - feed.scrollTop - feed.clientHeight < 60;
autoScroll = atBottom;
scrollBtn.classList.toggle('visible', !atBottom);
});
function scrollToBottom() {
feed.scrollTop = feed.scrollHeight;
autoScroll = true;
scrollBtn.classList.remove('visible');
}
function setStatus(state) {
const dot = document.getElementById('wsDot');
const label = document.getElementById('wsStatus');
const map = {
connected: { cls: 'live', text: 'LIVE' },
disconnected: { cls: 'error', text: 'OFFLINE' },
connecting: { cls: '', text: 'CONNECTING' },
};
const s = map[state] || map.connecting;
dot.className = 'dot ' + s.cls;
label.textContent = s.text;
label.style.color = s.cls === 'live' ? 'var(--green)' : s.cls === 'error' ? 'var(--accent2)' : 'var(--muted)';
}
function formatTime(ts) {
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function isRealChatMessage(msg) {
// Filter out TTS messages (user.id === 'tts')
if (msg.user && msg.user.id === 'tts') return false;
// Filter out item use messages (metadata.type === 'item')
if (msg.metadata && msg.metadata.type === 'item') return false;
// Must have a real message string
if (!msg.message || typeof msg.message !== 'string') return false;
return true;
}
function addMessage(msg) {
const empty = feed.querySelector('.empty');
if (empty) empty.remove();
const user = msg.user || {};
const meta = msg.metadata || {};
const isFish = meta.isFish || false;
const isGM = meta.isGrandMarshall || false;
const isEpic = meta.isEpic || false;
const isAdmin = meta.isAdmin || false;
const isMod = meta.isMod || false;
const cls = ['msg',
isAdmin ? 'admin' : '',
isMod ? 'mod' : '',
isFish ? 'fish' : '',
isGM ? 'grand-marshall' : '',
isEpic ? 'epic' : '',
].filter(Boolean).join(' ');
// Username color: custom > endorsement color > default
let nameStyle = '';
if (user.customUsernameColor) {
nameStyle = `style="color:${user.customUsernameColor}"`;
}
// Endorsement tag (replaces medals)
const endorsement = user.endorsement
? `<span class="msg-endorsement" style="color:${user.endorsementColor || '#888'}">${user.endorsement}</span>`
: '';
// Role badges for special users only
const badges = [
isAdmin ? '<span class="badge admin">ADMIN</span>' : '',
isMod ? '<span class="badge mod">MOD</span>' : '',
isFish ? '<span class="badge fish">FISH</span>' : '',
isGM ? '<span class="badge gm">GM</span>' : '',
].filter(Boolean).join('');
const clan = user.clan ? `<span class="msg-clan">[${user.clan}]</span>` : '';
const ts = msg.timestamp ? formatTime(msg.timestamp) : '';
const div = document.createElement('div');
div.className = cls;
div.innerHTML = `
<div class="msg-header">
<span class="msg-user" ${nameStyle}>${user.displayName || 'unknown'}</span>
${clan}
${endorsement}
${badges}
${ts ? `<span class="msg-time">${ts}</span>` : ''}
</div>
<div class="msg-text">${escapeHtml(String(msg.message))}</div>`;
feed.appendChild(div);
msgCount++;
document.getElementById('msgCount').textContent = msgCount.toLocaleString() + ' messages';
if (autoScroll) feed.scrollTop = feed.scrollHeight;
}
function escapeHtml(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function connect() {
if (ws) { ws.close(); ws = null; }
setStatus('connecting');
ws = new WebSocket('ws://localhost:3000');
ws.addEventListener('open', () => setStatus('connected'));
ws.addEventListener('message', e => {
let msg;
try { msg = JSON.parse(e.data); } catch { return; }
if (msg._ft === 'ws_status') {
setStatus(msg.status === 'connected' ? 'connected' : 'disconnected');
return;
}
if (msg._ft === 'event' && msg.event === 'chat:message') {
const data = msg.data;
const messages = Array.isArray(data) ? data : [data];
messages.forEach(m => { if (isRealChatMessage(m)) addMessage(m); });
}
});
ws.addEventListener('close', () => {
setStatus('disconnected');
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 3000);
});
ws.addEventListener('error', () => setStatus('disconnected'));
}
connect();
</script>
</body>
</html>