Chat / index.html
Ramazanpro995's picture
Upload 160 files
db63cd7 verified
Raw
History Blame Contribute Delete
16.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Global Chat Admin</title>
<style>
:root {
color-scheme: dark;
--bg: #07100f;
--panel: #0d1b1a;
--panel2: #102321;
--text: #eefcf7;
--muted: #93aaa3;
--green: #13a65b;
--blue: #2476c8;
--yellow: #f5c542;
--red: #df3f3f;
--line: #21423d;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, Segoe UI, Arial, sans-serif;
background: linear-gradient(145deg, #0d5e32 0%, #062d25 48%, #04100f 100%);
color: var(--text);
min-height: 100vh;
display: grid;
grid-template-columns: 280px 1fr;
}
.login-screen {
position: fixed;
inset: 0;
z-index: 20;
background: linear-gradient(145deg, #0d5e32 0%, #062d25 50%, #04100f 100%);
display: grid;
place-items: center;
padding: 18px;
}
.login-card {
width: min(420px, 100%);
background: rgba(9, 24, 22, 0.94);
border: 1px solid var(--line);
border-radius: 12px;
padding: 22px;
display: grid;
gap: 14px;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
}
.hidden { display: none; }
aside {
background: rgba(5, 16, 15, 0.92);
border-right: 1px solid var(--line);
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
}
main {
display: grid;
grid-template-rows: auto 1fr auto;
min-width: 0;
height: 100vh;
}
h1, h2 { margin: 0; }
h1 { font-size: 24px; }
h2 { font-size: 15px; color: var(--muted); }
label { font-size: 13px; color: var(--muted); }
input, button {
width: 100%;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel2);
color: var(--text);
padding: 11px 12px;
font-size: 14px;
outline: none;
}
button {
cursor: pointer;
font-weight: 800;
border: 0;
transition: transform 120ms ease, opacity 120ms ease;
}
button:hover { transform: translateY(-1px); }
.green { background: var(--green); }
.red { background: var(--red); }
.dark { background: #182927; }
.stack { display: grid; gap: 8px; }
.stats {
display: grid;
gap: 8px;
margin-top: auto;
}
.stat {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
display: grid;
gap: 4px;
}
.stat strong { font-size: 14px; }
.stat span { color: var(--muted); font-size: 12px; }
header {
padding: 18px 22px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
background: rgba(8, 20, 18, 0.7);
}
.header-tools {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
flex-wrap: wrap;
}
.translate-controls {
display: grid;
grid-template-columns: repeat(2, minmax(92px, 1fr));
gap: 8px;
}
.translate-controls button { width: auto; padding: 9px 12px; }
.blue { background: var(--blue); }
button:disabled { cursor: default; opacity: 0.45; transform: none; }
#translateStatus {
color: #b9d9ff;
font-size: 12px;
font-weight: 800;
min-width: 150px;
text-align: right;
}
#status {
color: var(--muted);
font-weight: 700;
white-space: nowrap;
}
#messages {
overflow: auto;
scroll-behavior: smooth;
overscroll-behavior: contain;
padding: 22px;
display: flex;
flex-direction: column;
gap: 10px;
}
.bubble {
max-width: min(720px, 82%);
background: #f4f8f6;
color: #07110f;
border: 1px solid #cad8d2;
border-radius: 10px;
padding: 10px 12px;
line-height: 1.35;
overflow-wrap: anywhere;
}
.bubble.admin {
background: #f5c542;
color: #241a00;
margin-left: auto;
border-color: #d9a900;
}
.name {
font-size: 12px;
color: #08733f;
font-weight: 900;
margin-bottom: 4px;
}
.bubble.admin .name { color: #5c4000; }
.composer {
padding: 16px 22px;
display: grid;
grid-template-columns: 1fr 130px;
gap: 10px;
border-top: 1px solid var(--line);
background: rgba(8, 20, 18, 0.86);
}
@media (max-width: 820px) {
body { grid-template-columns: 1fr; }
aside { border-right: 0; border-bottom: 1px solid var(--line); }
main { height: auto; min-height: 70vh; }
header { align-items: flex-start; flex-direction: column; }
.header-tools { width: 100%; justify-content: space-between; }
#translateStatus { text-align: left; }
}
</style>
</head>
<body>
<div id="loginScreen" class="login-screen">
<div class="login-card">
<h1>&#128081; Admin Login</h1>
<h2>Enter the admin password to manage Global Chat.</h2>
<input id="password" name="global-chat-admin-secret" type="password" placeholder="Enter admin password" autocomplete="off" spellcheck="false">
<button id="connect" class="green">Enter Admin Panel</button>
<div id="loginError" style="color:#ffb199;font-weight:700;"></div>
</div>
</div>
<aside>
<div class="stack">
<h1>&#128081; Admin</h1>
<h2>Global Chat Control</h2>
</div>
<div class="stack">
<label>Room</label>
<input id="room" value="global">
</div>
<div class="stack">
<label>Settings</label>
<button id="pause" class="green">Pause Chat</button>
<button id="clear" class="dark">Clear Chat History</button>
</div>
<div class="stats">
<div class="stat"><strong id="cpu">Process CPU: --</strong><span>Measured from this Node process</span></div>
<div class="stat"><strong id="ram">Process RAM: --</strong><span>Real RAM used by the chat server</span></div>
<div class="stat"><strong id="disk">App Storage: --</strong><span>Size of deployed app files only</span></div>
<div class="stat"><strong id="online">Online: --</strong><span>Active players in this room</span></div>
</div>
</aside>
<main>
<header>
<div>
<h1>Messages</h1>
<h2>Admin messages appear in Unity as &#128081;Admin</h2>
</div>
<div class="header-tools">
<div>
<div class="translate-controls">
<button id="translate" class="blue">Translate</button>
<button id="original" class="dark" disabled>Original</button>
</div>
<div id="translateStatus"></div>
</div>
<div id="status">Disconnected</div>
</div>
</header>
<section id="messages"></section>
<div class="composer">
<input id="message" maxlength="600" placeholder="Write as Admin...">
<button id="send" class="green">Send</button>
</div>
</main>
<script>
const ADMIN_NAME = "\uD83D\uDC51Admin";
const wsUrl = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`;
let ws = null;
let paused = false;
let currentRoom = "global";
let heartbeatTimer = null;
let pendingAdminPassword = "";
let adminAuthenticated = false;
let translationEnabled = false;
let translationGeneration = 0;
let targetLanguage = "";
let messageViews = [];
const $ = (id) => document.getElementById(id);
function send(payload) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(JSON.stringify(payload));
}
function admin(action, extra = {}) {
const payload = {
type: "admin",
action,
room: $("room").value || "global",
...extra
};
if (!adminAuthenticated) payload.password = pendingAdminPassword;
send(payload);
}
function addMessage(name, message, isAdmin = false, filtered = false) {
const bubble = document.createElement("div");
bubble.className = `bubble ${isAdmin ? "admin" : ""}`;
bubble.innerHTML = `<div class="name"></div><div class="text"></div>`;
bubble.querySelector(".name").textContent = isAdmin ? ADMIN_NAME : (name || "Player");
const textElement = bubble.querySelector(".text");
textElement.textContent = message || "";
$("messages").appendChild(bubble);
$("messages").scrollTop = $("messages").scrollHeight;
const view = {
element: bubble,
textElement,
originalText: message || "",
translatedText: "",
translatedLanguage: "",
filtered: !!filtered,
translating: false
};
messageViews.push(view);
if (translationEnabled && !view.filtered) {
translateMessageView(view, translationGeneration);
}
}
function validLanguageCode(value) {
const code = String(value || "").toLowerCase().replace("_", "-").split(",")[0];
return /^[a-z]{2,3}(?:-[a-z]{2,4})?$/.test(code) ? code : "";
}
async function detectTargetLanguage() {
const browserLanguage = validLanguageCode(navigator.languages?.[0] || navigator.language);
if (browserLanguage) return browserLanguage;
try {
const response = await fetch("/locale");
const data = await response.json();
return validLanguageCode(data.language) || "en";
} catch {
return "en";
}
}
function updateTranslationButtons() {
$("translate").disabled = translationEnabled;
$("original").disabled = !translationEnabled;
}
async function translateMessageView(view, generation) {
if (!translationEnabled || generation !== translationGeneration || view.filtered || view.translating) return;
if (view.translatedText && view.translatedLanguage === targetLanguage) {
view.textElement.textContent = view.translatedText;
return;
}
view.translating = true;
try {
const response = await fetch("/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: view.originalText,
targetLanguage,
mode: "free",
provider: "google"
})
});
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.code || "translation_failed");
view.translatedText = data.translatedText;
view.translatedLanguage = targetLanguage;
if (translationEnabled && generation === translationGeneration) {
view.textElement.textContent = view.translatedText;
}
} catch (error) {
$("translateStatus").textContent = "Server could not translate.";
if (error.message === "invalid_api_key") {
console.error("Translation API key is invalid or missing.");
} else {
console.error("Translation server failed:", error.message);
}
} finally {
view.translating = false;
}
}
async function enableTranslation() {
if (translationEnabled) return;
translationEnabled = true;
translationGeneration++;
const generation = translationGeneration;
updateTranslationButtons();
$("translateStatus").textContent = "Detecting language...";
if (!targetLanguage) targetLanguage = await detectTargetLanguage();
if (!translationEnabled || generation !== translationGeneration) return;
$("translateStatus").textContent = `Translating to ${targetLanguage.toUpperCase()}...`;
for (const view of [...messageViews]) {
if (!translationEnabled || generation !== translationGeneration) return;
if (!view.filtered) await translateMessageView(view, generation);
}
if (translationEnabled && generation === translationGeneration) {
$("translateStatus").textContent = `Translated to ${targetLanguage.toUpperCase()}`;
}
}
function showOriginalMessages() {
translationEnabled = false;
translationGeneration++;
messageViews.forEach((view) => { view.textElement.textContent = view.originalText; });
$("translateStatus").textContent = "Showing original messages";
updateTranslationButtons();
}
function updateStats(status) {
if (!status) return;
$("online").textContent = `Online: ${status.clients ?? "--"}`;
const stats = status.stats;
if (!stats) return;
$("cpu").textContent = `Process CPU: ${stats.cpu.percent}%`;
$("ram").textContent = `Process RAM: ${stats.memory.rssText}`;
$("disk").textContent = `App Storage: ${stats.storage.usedText}`;
}
function updatePauseButton() {
$("pause").textContent = paused ? "Resume Chat" : "Pause Chat";
$("pause").className = paused ? "red" : "green";
}
function connectAdmin() {
$("loginError").textContent = "";
const enteredPassword = $("password").value;
if (!enteredPassword) {
$("loginError").textContent = "Enter the admin password.";
return;
}
pendingAdminPassword = enteredPassword;
$("password").value = "";
adminAuthenticated = false;
ws = new WebSocket(wsUrl);
$("status").textContent = "Connecting...";
ws.onopen = () => {
$("status").textContent = "Connected";
admin("auth");
clearInterval(heartbeatTimer);
heartbeatTimer = setInterval(() => send({ type: "ping" }), 4000);
};
ws.onclose = () => {
$("status").textContent = "Disconnected";
$("loginScreen").classList.remove("hidden");
pendingAdminPassword = "";
adminAuthenticated = false;
clearInterval(heartbeatTimer);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "message") {
addMessage(data.nickname, data.message, !!data.admin, !!data.filtered);
}
if (data.type === "history") {
$("messages").innerHTML = "";
messageViews = [];
(data.messages || []).forEach((msg) => addMessage(msg.nickname, msg.message, !!msg.admin, !!msg.filtered));
}
if (data.type === "adminReady") {
adminAuthenticated = true;
pendingAdminPassword = "";
$("password").value = "";
$("loginScreen").classList.add("hidden");
$("loginError").textContent = "";
paused = !!data.paused;
updatePauseButton();
updateStats(data);
currentRoom = $("room").value || "global";
send({
type: "join",
nickname: ADMIN_NAME,
room: currentRoom,
roomLimit: 0,
language: validLanguageCode(navigator.languages?.[0] || navigator.language) || "en"
});
}
if (data.type === "serverStatus") {
paused = !!data.paused;
updatePauseButton();
updateStats(data);
}
if (data.type === "roomInfo") {
$("online").textContent = `Online: ${data.online}`;
}
if (data.type === "error") {
if (data.message === "Wrong admin password.") {
$("loginError").textContent = "Wrong admin password.";
pendingAdminPassword = "";
adminAuthenticated = false;
ws.close();
} else {
addMessage("Error", data.message, false);
}
}
};
}
$("connect").onclick = connectAdmin;
$("password").addEventListener("keydown", (event) => {
if (event.key === "Enter") connectAdmin();
});
$("pause").onclick = () => admin(paused ? "resume" : "pause");
$("clear").onclick = () => admin("clear");
$("translate").onclick = enableTranslation;
$("original").onclick = showOriginalMessages;
$("send").onclick = () => {
const text = $("message").value.trim();
if (!text) return;
admin("message", { message: text });
$("message").value = "";
};
$("message").addEventListener("keydown", (event) => {
if (event.key === "Enter") $("send").click();
});
setInterval(async () => {
try {
const response = await fetch("/health");
updateStats(await response.json());
} catch {
}
}, 2500);
</script>
</body>
</html>