80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""Minimal live wizard portal: persona-aware chat over WebSocket."""
|
|
import os, json
|
|
from pathlib import Path
|
|
from flask import Flask, render_template_string
|
|
from flask_sock import Sock
|
|
import yaml
|
|
|
|
app = Flask(__name__)
|
|
sock = Sock(app)
|
|
ROOT = Path(os.getenv("PORTAL_ROOT", Path(__file__).resolve().parent))
|
|
PERSONA_PATH = ROOT / "persona.yaml"
|
|
|
|
with open(PERSONA_PATH, "r", encoding="utf-8") as f:
|
|
persona = yaml.safe_load(f) or {}
|
|
|
|
PAGE = """<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Timmy Time — Live Wizard Portal</title>
|
|
<style>
|
|
body { background:#0b0c10; color:#c5c6c7; font-family: system-ui, sans-serif; margin:0; display:flex; height:100vh; }
|
|
#c { flex:1; display:flex; flex-direction:column; padding:16px; }
|
|
#o { flex:1; overflow:auto; padding:16px; }
|
|
#i { display:flex; gap:8px; }
|
|
#m { flex:1; padding:10px 12px; border-radius:8px; border:1px solid #30363d; background:#0d1117; color:#e6edf3; }
|
|
#b { padding:10px 14px; border-radius:8px; border:0; background:#8a2be2; color:white; cursor:pointer; }
|
|
.u { color:#66d9ef; }
|
|
.w { color:#fd971f; margin-top:8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="c">
|
|
<div id="o"></div>
|
|
<div id="i">
|
|
<input id="m" placeholder="Speak to the wizard..." autocomplete="off" />
|
|
<button id="b">Send</button>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
const o=document.getElementById('o'), m=document.getElementById('m'), b=document.getElementById('b');
|
|
const ws=new WebSocket(`ws://${location.host}/ws`);
|
|
ws.onopen=()=>log('Portal open. Timmy Time is listening.','w');
|
|
ws.onmessage=(e)=>{ const d=JSON.parse(e.data); if(d && d.text){ log(d.text,'w'); } };
|
|
function send(){ const t=m.value.trim(); if(!t) return; log(t,'u'); ws.send(JSON.stringify({type:'text',text:t})); m.value=''; }
|
|
b.onclick=send; m.onkeydown=(e)=>{ if(e.key==='Enter') send(); };
|
|
function log(t,cls){ const d=document.createElement('div'); d.className=cls; d.textContent=t; o.appendChild(d); o.scrollTop=o.scrollHeight; }
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
@app.get("/")
|
|
def index():
|
|
return render_template_string(PAGE)
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status":"ok","persona":persona.get("wizard",{}).get("name","Unknown")}
|
|
|
|
@sock.route("/ws")
|
|
def ws(ws):
|
|
wizard = persona.get("wizard", {})
|
|
catchphrases = wizard.get("catchphrases", [])
|
|
while True:
|
|
msg = ws.receive()
|
|
if msg is None:
|
|
break
|
|
try:
|
|
data = json.loads(msg)
|
|
except Exception:
|
|
continue
|
|
if data.get("type") == "text":
|
|
text = data.get("text", "").strip()
|
|
prefix = catchphrases[0] if catchphrases else "The mists reveal:"
|
|
reply = f"{prefix} {text}"
|
|
ws.send(json.dumps({"text": reply}))
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8787)
|