llm_ui/app.py
2025-09-15 19:44:06 +00:00

205 lines
5.3 KiB
Python

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.responses import HTMLResponse
import asyncio, json
from typing import Set
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active: Set[WebSocket] = set()
self._lock = asyncio.Lock()
async def connect(self, ws: WebSocket):
await ws.accept()
async with self._lock:
self.active.add(ws)
async def disconnect(self, ws: WebSocket):
async with self._lock:
self.active.discard(ws)
async def broadcast(self, message: str):
data = json.dumps({"text": message})
async with self._lock:
dead = []
for ws in self.active:
try:
await ws.send_text(data)
except Exception:
dead.append(ws)
for ws in dead:
self.active.discard(ws)
manager = ConnectionManager()
@app.get("/", response_class=HTMLResponse)
async def index():
return """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Messages</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="https://unpkg.com/hack@0.8.1/dist/hack.css">
<link rel="stylesheet" href="https://unpkg.com/hack@0.8.1/dist/dark.css">
<style>
#log {
white-space: pre-wrap;
font-size: 1rem;
height: 70vh;
overflow-y: auto;
border: 1px solid #2a2a35;
border-radius: 14px;
padding: 1rem 1.25rem;
background: rgba(20, 20, 26, 0.7);
backdrop-filter: blur(6px);
scroll-behavior: smooth;
}
.line {
display: flex;
align-items: flex-start;
margin: 0.25rem 0;
line-height: 1.4;
animation: lineIn 200ms ease-out;
}
@keyframes lineIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.icon {
flex: 0 0 1.5rem;
text-align: center;
opacity: 0.7;
margin-right: 0.5rem;
}
.text {
flex: 1;
}
.typing::after {
content: "";
display: inline-block;
margin-left: 2px;
animation: blink 1s steps(1, end) infinite;
color: rgba(255,255,255,0.8);
}
@keyframes blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.char {
opacity: 0;
transform: translateY(4px);
transition: opacity 140ms ease-out, transform 140ms ease-out;
}
.char.in {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body class="hack dark">
<div class="container">
<h2>Messages</h2>
<div id="log"></div>
</div>
<script>
const log = document.getElementById('log');
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(proto + '://' + location.host + '/ws');
const nearBottom = () => (log.scrollHeight - log.scrollTop - log.clientHeight) < 40;
const scrollToBottom = () => { log.scrollTop = log.scrollHeight; };
function typeText(text) {
const line = document.createElement("div");
line.className = "line";
const icon = document.createElement("div");
icon.className = "icon";
icon.textContent = ""; // 🔑 change this to 💬 ➤ 👉 etc.
const textDiv = document.createElement("div");
textDiv.className = "text typing";
line.appendChild(icon);
line.appendChild(textDiv);
log.appendChild(line);
let i = 0;
const interval = setInterval(() => {
if (i >= text.length) {
clearInterval(interval);
textDiv.classList.remove("typing");
if (nearBottom()) scrollToBottom();
return;
}
const span = document.createElement("span");
span.className = "char";
span.textContent = text[i++];
textDiv.appendChild(span);
requestAnimationFrame(() => span.classList.add("in"));
if (nearBottom()) scrollToBottom();
}, 50);
}
ws.onmessage = e => {
try {
const data = JSON.parse(e.data);
typeText(data.text || "");
} catch {
typeText("");
}
};
ws.onclose = () => {
const line = document.createElement("div");
line.className = "line";
const icon = document.createElement("div");
icon.className = "icon";
icon.textContent = "⚠️";
const textDiv = document.createElement("div");
textDiv.className = "text";
textDiv.textContent = "Disconnected";
line.appendChild(icon);
line.appendChild(textDiv);
log.appendChild(line);
scrollToBottom();
};
</script>
</body>
</html>
"""
@app.post("/send")
async def send(request: Request):
text = ""
try:
payload = await request.json()
if isinstance(payload, dict):
text = payload.get("text", "")
except Exception:
try:
form = await request.form()
text = form.get("text", "")
except Exception:
pass
await manager.broadcast(text or "")
return {"message": text or ""}
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
await manager.connect(ws)
try:
while True:
await ws.receive_text()
except WebSocketDisconnect:
await manager.disconnect(ws)