portal: add live wizard web portal + websocket persona chat

This commit is contained in:
Timmy 2026-07-08 19:47:16 +00:00
parent e1995bab11
commit a9000e8d4b
2 changed files with 83 additions and 0 deletions

79
portal.py Normal file
View File

@ -0,0 +1,79 @@
"""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)

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flask
flask-sock
PyYAML
requests