portal: add live wizard web portal + websocket persona chat
This commit is contained in:
parent
e1995bab11
commit
a9000e8d4b
79
portal.py
Normal file
79
portal.py
Normal 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
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
flask
|
||||
flask-sock
|
||||
PyYAML
|
||||
requests
|
||||
Loading…
Reference in New Issue
Block a user