diff --git a/README.md b/README.md index 2b21d75..77e73ff 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,22 @@ Add this to your `services.yaml`: | --- | --- | --- | | `PORT` | `3000` | The port the web dashboard listens on. Since `--network host` is used, this port opens directly on the host. | | `MAX_PEERS` | `10000` | Maximum number of peers to track in the swarm. Unless you're expecting the entire internet to join, the default is probably fine. | +| `ENABLE_CHAT` | `false` | Set to `true` to enable the ephemeral P2P chat terminal. | + +## » Features + +### 1. The Counter +It counts. That's the main thing. + +### 2. Ephemeral Chat +**New:** A completely decentralized, ephemeral chat system built directly on top of the swarm topology. + +* **Ephemeral:** No database. No history. If you refresh, it's gone. +* **Restricted:** You can only talk to your ~32 direct connections. +* **Chaotic:** Every 30 seconds, the network rotates your connections. You might be mid-sentence and—*poof*—your audience changes. +* **Anonymous:** You are identified only by the last 4 characters of your node ID. + +To enable this feature, set `ENABLE_CHAT=true`. ## » Usage diff --git a/public/app.js b/public/app.js index 248747c..594c309 100644 --- a/public/app.js +++ b/public/app.js @@ -99,11 +99,114 @@ document.addEventListener('keydown', (e) => { } }); +const terminal = document.getElementById('terminal'); +const terminalOutput = document.getElementById('terminal-output'); +const terminalInput = document.getElementById('terminal-input'); +const promptEl = document.querySelector('.prompt'); +let myId = null; +let myChatHistory = []; + +const updatePromptStatus = () => { + const now = Date.now(); + myChatHistory = myChatHistory.filter(t => now - t < 10000); + + if (myChatHistory.length >= 5) { + promptEl.style.color = 'orange'; + } else { + promptEl.style.color = '#4ade80'; + } +}; + +setInterval(updatePromptStatus, 500); + +const getColorFromId = (id) => { + if (!id) return '#666'; + let hash = 0; + for (let i = 0; i < id.length; i++) { + hash = id.charCodeAt(i) + ((hash << 5) - hash); + } + const c = (hash & 0x00FFFFFF).toString(16).toUpperCase(); + return '#' + "00000".substring(0, 6 - c.length) + c; +} + +const appendMessage = (msg) => { + const div = document.createElement('div'); + + if (msg.type === 'SYSTEM') { + div.className = 'msg-system'; + div.innerText = `[SYSTEM] ${msg.content}`; + } else if (msg.type === 'CHAT') { + const senderColor = getColorFromId(msg.sender); + const senderName = msg.sender === myId ? 'You' : msg.sender.slice(-4); + + const senderSpan = document.createElement('span'); + senderSpan.className = 'msg-sender'; + senderSpan.style.color = senderColor; + senderSpan.innerText = `[${senderName}]`; + + const contentSpan = document.createElement('span'); + contentSpan.className = 'msg-content'; + contentSpan.innerText = ` > ${msg.content}`; + + div.appendChild(senderSpan); + div.appendChild(contentSpan); + } + + terminalOutput.appendChild(div); + terminalOutput.scrollTop = terminalOutput.scrollHeight; +} + +terminalInput.addEventListener('keypress', async (e) => { + if (e.key === 'Enter') { + const content = terminalInput.value.trim(); + if (!content) return; + + terminalInput.value = ''; + + try { + const res = await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }) + }); + + if (res.ok) { + myChatHistory.push(Date.now()); + updatePromptStatus(); + } else if (res.status === 429) { + // Force update if we hit the limit unexpectedly + // Add a dummy timestamp to force the limit state if not already there + if (myChatHistory.length < 5) { + myChatHistory.push(Date.now()); + } + updatePromptStatus(); + } + } catch (err) { + console.error('Failed to send message', err); + } + } +}); + const evtSource = new EventSource("/events"); evtSource.onmessage = (event) => { const data = JSON.parse(event.data); + if (data.type === 'CHAT' || data.type === 'SYSTEM') { + appendMessage(data); + return; + } + + if (data.chatEnabled) { + terminal.classList.remove('hidden'); + document.body.classList.add('chat-active'); + } else { + terminal.classList.add('hidden'); + document.body.classList.remove('chat-active'); + } + + if (data.id) myId = data.id; + updateParticles(data.count); if (countEl.innerText != data.count) { diff --git a/public/index.html b/public/index.html index 77d90c0..8c4314b 100644 --- a/public/index.html +++ b/public/index.html @@ -65,6 +65,13 @@ +