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