add files
This commit is contained in:
commit
556fb8bcf8
204
app.py
Normal file
204
app.py
Normal file
@ -0,0 +1,204 @@
|
||||
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)
|
||||
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
n8n:
|
||||
image: n8nio/n8n:latest
|
||||
container_name: n8n
|
||||
restart: always
|
||||
ports:
|
||||
- "5678:5678"
|
||||
environment:
|
||||
- GENERIC_TIMEZONE=America/New_York # adjust to your timezone
|
||||
- N8N_BASIC_AUTH_ACTIVE=true
|
||||
- N8N_BASIC_AUTH_USER=admin
|
||||
- N8N_BASIC_AUTH_PASSWORD=admin123
|
||||
- N8N_SECURE_COOKIE=false
|
||||
volumes:
|
||||
- n8n_data:/home/node/.n8n
|
||||
|
||||
volumes:
|
||||
n8n_data:
|
||||
20
dockerfile
Normal file
20
dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
# Minimal, production-ready image
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Prevent Python from writing .pyc files; ensure unbuffered logs
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install only what we need
|
||||
RUN pip install --no-cache-dir fastapi "uvicorn[standard]"
|
||||
|
||||
# Copy the application
|
||||
COPY app.py /app/app.py
|
||||
|
||||
# Expose the app port
|
||||
EXPOSE 8000
|
||||
|
||||
# Start the server
|
||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
Loading…
Reference in New Issue
Block a user