diff --git a/docs/slides/index.html b/docs/slides/index.html index e9952d7..2076bbd 100644 --- a/docs/slides/index.html +++ b/docs/slides/index.html @@ -119,6 +119,238 @@ +
+

Backend Codes

+

WebSocket router setup in FastAPI.

+
+
app/websocket/chat.py
+
import json
+from datetime import datetime, timezone
+
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect
+
+from app.websocket.manager import manager
+
+router = APIRouter()
+
+ +
+ +
+

Message Payload Shape

+

Every server message uses one small JSON structure.

+
+
app/websocket/chat.py
+
def now_iso() -> str:
+    return datetime.now(timezone.utc).isoformat()
+
+def event(event_type, room_id, client_id, content):
+    return {
+        "type": event_type,
+        "room_id": room_id,
+        "client_id": client_id,
+        "content": content,
+        "timestamp": now_iso(),
+    }
+
+ +
+ +
+

WebSocket Endpoint

+

The route parameters select the room and the user identity.

+
+
app/websocket/chat.py
+
@router.websocket("/ws/{room_id}/{client_id}")
+async def websocket_endpoint(
+    websocket: WebSocket,
+    room_id: str,
+    client_id: str,
+):
+    room_id = clean_value(room_id, "general")
+    client_id = clean_value(client_id, "anonymous")
+
+    await manager.connect(websocket, room_id, client_id)
+
+ +
+ +
+

Join and Presence Events

+

After connection, the server informs the room and refreshes online users.

+
+
app/websocket/chat.py
+
await manager.broadcast_json(
+    event("system", room_id, "system", f"{client_id} joined {room_id}."),
+    room_id,
+)
+
+await manager.broadcast_json({
+    "type": "presence",
+    "users": manager.room_users(room_id),
+    "timestamp": now_iso(),
+}, room_id)
+
+ +
+ +
+

Receive Loop and Broadcast

+

The socket stays open and every valid message is broadcast to the room.

+
+
app/websocket/chat.py
+
while True:
+    raw_data = await websocket.receive_text()
+
+    try:
+        data = json.loads(raw_data)
+        message = str(data.get("content", "")).strip()
+    except json.JSONDecodeError:
+        message = raw_data.strip()
+
+    if message:
+        await manager.broadcast_json(event("message", room_id, client_id, message), room_id)
+
+ +
+ +
+

ConnectionManager

+

The manager groups sockets by room and removes dead connections.

+
+
app/websocket/manager.py
+
class ConnectionManager:
+    def __init__(self):
+        self.active_connections = {}
+
+    async def connect(self, websocket, room_id, client_id):
+        await websocket.accept()
+        self.active_connections.setdefault(room_id, [])
+        self.active_connections[room_id].append(ClientConnection(websocket, client_id))
+
+    async def broadcast_json(self, payload, room_id):
+        for connection in list(self.active_connections.get(room_id, [])):
+            await connection.websocket.send_json(payload)
+
+ +
+ +
+

Frontend Codes

+

browser state and DOM references.

+
+
app/static/app.js
+
const usernameInput = document.querySelector('#username');
+const roomInput = document.querySelector('#room');
+const connectBtn = document.querySelector('#connectBtn');
+const messageForm = document.querySelector('#messageForm');
+const messages = document.querySelector('#messages');
+const usersList = document.querySelector('#usersList');
+
+let socket = null;
+let currentUser = '';
+let currentRoom = '';
+
+ +
+ +
+

Building the WebSocket URL

+

The browser automatically switches between ws and wss.

+
+
app/static/app.js
+
function connect() {
+  currentUser = safeName(usernameInput.value, 'anonymous');
+  currentRoom = safeName(roomInput.value, 'general');
+
+  const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
+  const room = encodeURIComponent(currentRoom);
+  const user = encodeURIComponent(currentUser);
+  const wsUrl = `${wsProtocol}://${window.location.host}/ws/${room}/${user}`;
+
+  socket = new WebSocket(wsUrl);
+}
+
+ +
+ +
+

Connection Open Handler

+

When the socket opens, the UI moves into connected mode.

+
+
app/static/app.js
+
socket.addEventListener('open', () => {
+  setConnectedState(true);
+
+  roomTitle.textContent = `Room: ${currentRoom}`;
+  roomSubtitle.textContent = `You are chatting as ${currentUser}.`;
+
+  messageInput.focus();
+});
+
+ +
+ +
+

Handling Incoming Messages

+

Presence updates and chat messages are separated by type.

+
+
app/static/app.js
+
socket.addEventListener('message', (event) => {
+  const payload = JSON.parse(event.data);
+
+  if (payload.type === 'presence') {
+    updateUsers(payload.users || []);
+    return;
+  }
+
+  addMessage(payload);
+});
+
+ +
+ +
+

Rendering a Chat Message

+

The browser creates message elements without reloading the page.

+
+
app/static/app.js
+
function addMessage(payload) {
+  clearEmptyState();
+
+  const card = document.createElement('article');
+  card.className = 'message';
+
+  if (payload.client_id === currentUser) card.classList.add('mine');
+
+  content.textContent = payload.content || '';
+  card.append(meta, content);
+  messages.appendChild(card);
+  messages.scrollTop = messages.scrollHeight;
+}
+
+ +
+ +
+

Sending Messages

+

The form sends JSON only when the socket is open.

+
+
app/static/app.js
+
messageForm.addEventListener('submit', (event) => {
+  event.preventDefault();
+
+  const content = messageInput.value.trim();
+  if (!content || !socket || socket.readyState !== WebSocket.OPEN) return;
+
+  socket.send(JSON.stringify({ content }));
+  messageInput.value = '';
+  messageInput.focus();
+});
+
+ +
+

Keynotes and Takeaways

The essential points to remember from the project.

diff --git a/docs/slides/styles.css b/docs/slides/styles.css index cfd8fab..7db4e6f 100644 --- a/docs/slides/styles.css +++ b/docs/slides/styles.css @@ -93,7 +93,8 @@ body { text-align: center; } .slide.active.protocol-slide, -.slide.active.image-slide { +.slide.active.image-slide, +.slide.active.code-slide { display: grid; grid-template-rows: auto auto minmax(0, 1fr); } @@ -131,7 +132,8 @@ h2 { font-size: clamp(27px, calc(46px * var(--slide-scale)), 66px); } letter-spacing: .01em; } .protocol-slide .subtitle, -.image-slide .subtitle { +.image-slide .subtitle, +.code-slide .subtitle { margin-bottom: clamp(10px, calc(14px * var(--slide-scale)), 22px); } @@ -635,6 +637,89 @@ pre { } .ltr { direction: ltr; text-align: left; } +.code-window { + min-height: 0; + border-radius: clamp(10px, calc(14px * var(--slide-scale)), 18px); + background: #0d1117; + border: 1px solid rgba(255,255,255,.13); + box-shadow: 0 24px 70px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.08); + overflow: hidden; + direction: ltr; + display: grid; + grid-template-rows: auto minmax(0, 1fr); +} +.code-titlebar { + height: clamp(38px, calc(44px * var(--slide-scale)), 56px); + background: linear-gradient(180deg, #1f2430, #151a24); + border-bottom: 1px solid rgba(255,255,255,.09); + display: flex; + align-items: center; + gap: 8px; + padding: 0 clamp(14px, calc(18px * var(--slide-scale)), 26px); +} +.code-titlebar span { + width: clamp(10px, calc(12px * var(--slide-scale)), 15px); + height: clamp(10px, calc(12px * var(--slide-scale)), 15px); + border-radius: 50%; + display: block; +} +.code-titlebar span:nth-child(1) { background: #ff5f57; } +.code-titlebar span:nth-child(2) { background: #febc2e; } +.code-titlebar span:nth-child(3) { background: #28c840; } +.code-titlebar strong { + margin-left: 10px; + color: #c9d1d9; + font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace; + font-size: clamp(12px, calc(14px * var(--slide-scale)), 19px); + font-weight: 600; +} +.code-window pre { + min-height: 0; + overflow: hidden; + background: + linear-gradient(90deg, rgba(125, 211, 252, .035), transparent 36%), + #0d1117; + border-radius: 0; + box-shadow: none; + padding: clamp(14px, calc(20px * var(--slide-scale)), 28px) 0; + white-space: pre; + font-size: clamp(11px, calc(14px * var(--slide-scale)), 19px); + line-height: 0; +} +.code-window code { + display: block; + background: transparent; + border-radius: 0; + color: #d6deeb; + padding: 0; + font-family: "Cascadia Code", "Consolas", "SFMono-Regular", monospace; + direction: ltr; + unicode-bidi: normal; +} +.code-window .line { + counter-increment: code-line; + display: block; + min-height: 1.55em; + line-height: 1.45; + padding: 0 clamp(18px, calc(28px * var(--slide-scale)), 42px) 0 0; +} +.code-window code { + counter-reset: code-line; +} +.code-window .line::before { + content: counter(code-line); + display: inline-block; + width: clamp(34px, calc(44px * var(--slide-scale)), 56px); + margin-right: clamp(12px, calc(18px * var(--slide-scale)), 26px); + color: #6e7681; + text-align: right; + user-select: none; +} +.code-window .kw { color: #ff7b72; } +.code-window .fn { color: #d2a8ff; } +.code-window .str { color: #a5d6ff; } +.code-window .num { color: #79c0ff; } + .version-row { display: grid; grid-template-columns: repeat(3, 1fr); @@ -1007,7 +1092,8 @@ code { .subtitle { font-size: clamp(14px, 4vw, 18px); } .kicker { font-size: clamp(12px, 3.7vw, 16px); } .slide.active.protocol-slide, - .slide.active.image-slide { display: block; } + .slide.active.image-slide, + .slide.active.code-slide { display: block; } .hero-grid, .split, .demo-layout, .version-row, .resource-grid, .mechanism-grid, .takeaway-grid { grid-template-columns: 1fr; } .takeaway-grid { grid-template-rows: none; min-height: 0; } .hero-grid, .split, .demo-layout, .mechanism-grid { gap: 18px; } @@ -1047,6 +1133,19 @@ code { .mini-points span { font-size: 12px; padding: 7px 10px; } .quote { font-size: 15px; } pre { font-size: clamp(10px, 2.8vw, 13px); padding: 14px; } + .code-window { display: block; } + .code-window pre { + overflow-x: auto; + font-size: clamp(10px, 2.7vw, 13px); + padding: 12px 0; + } + .code-window .line { + padding-right: 14px; + } + .code-window .line::before { + width: 32px; + margin-right: 12px; + } ol { font-size: 14px; line-height: 1.9; } .controls { left: 14px; bottom: 14px; } .controls button { width: 36px; height: 36px; font-size: 24px; }