FR-Start / web.py
FahrenheitResearch's picture
Upload web.py with huggingface_hub
07a16a7 verified
Raw
History Blame Contribute Delete
6.25 kB
#!/usr/bin/env python3
"""FR-Start web UI — stdlib server, streams from local Ollama. No cloud, no deps beyond `ollama`.
Run: python web.py → http://localhost:8420
"""
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from fr_start import MODEL, build_system, stream_reply
PORT = 8420
SYSTEM = build_system()
PAGE = r"""<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>FR-Start</title>
<style>
:root{--bg:#0f1115;--panel:#191c23;--me:#2b3a55;--bot:#20242e;--fg:#e8e6e1;--dim:#8a8f98;--accent:#e0a458}
*{box-sizing:border-box}body{margin:0;font:16px/1.55 -apple-system,system-ui,sans-serif;background:var(--bg);color:var(--fg);display:flex;flex-direction:column;height:100vh}
header{padding:14px 20px;border-bottom:1px solid #262a33}header h1{margin:0;font-size:17px}header h1 span{color:var(--accent)}
header p{margin:2px 0 0;font-size:12px;color:var(--dim)}
#log{flex:1;overflow-y:auto;padding:20px;max-width:820px;width:100%;margin:0 auto}
.msg{padding:10px 14px;border-radius:10px;margin:8px 0;overflow-wrap:break-word}
.me{background:var(--me);margin-left:15%;white-space:pre-wrap}.bot{background:var(--bot);margin-right:5%}
.bot p{margin:8px 0}.bot ul{margin:8px 0;padding-left:22px}.bot li{margin:3px 0}
.bot h3{margin:14px 0 6px;font-size:15px;color:var(--accent)}
.bot table{border-collapse:collapse;margin:10px 0;width:100%;font-size:14px}
.bot th,.bot td{border:1px solid #333845;padding:7px 10px;text-align:left;vertical-align:top}
.bot th{background:#232833;color:var(--accent);font-weight:600}
.bot tr:nth-child(even) td{background:#1c202a}
form{display:flex;gap:8px;padding:14px 20px;border-top:1px solid #262a33;max-width:820px;width:100%;margin:0 auto}
input{flex:1;padding:12px;border-radius:8px;border:1px solid #333845;background:var(--panel);color:var(--fg);font-size:15px}
button{padding:12px 20px;border-radius:8px;border:0;background:var(--accent);color:#141414;font-weight:600;cursor:pointer}
button:disabled{opacity:.5}
footer{text-align:center;font-size:11px;color:var(--dim);padding:0 0 10px}
</style></head><body>
<header><h1>FR-<span>Start</span> — incorporation advisor</h1>
<p>US · India · UAE · Singapore · UK — runs fully local. Not legal or tax advice.</p></header>
<div id="log"></div>
<form id="f"><input id="q" placeholder="e.g. SaaS founder in Bangalore raising from US VCs — where do I incorporate?" autofocus autocomplete="off">
<button id="b">Ask</button></form>
<footer>Fahrenheit Research · answers carry as-of dates — verify with a qualified professional</footer>
<script>
const log=document.getElementById('log'),f=document.getElementById('f'),q=document.getElementById('q'),b=document.getElementById('b');
let history=[];
// minimal markdown renderer: escape HTML, then bold / headers / lists / tables
function md(src){
const esc=s=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const inline=s=>esc(s).replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/(^|\s)\*(?=\s|$)/g,'$1');
const lines=src.split('\n');let out='',i=0;
while(i<lines.length){
const l=lines[i];
if(/^\s*\|.*\|\s*$/.test(l)){
const rows=[];while(i<lines.length&&/^\s*\|.*\|\s*$/.test(lines[i])){rows.push(lines[i]);i++}
const cells=r=>r.trim().replace(/^\||\|$/g,'').split('|').map(c=>inline(c.trim()));
let first=true,t='<table>';
for(const r of rows){
if(/^[\s|:\-]+$/.test(r))continue;
const tag=first?'th':'td';first=false;
t+='<tr>'+cells(r).map(c=>`<${tag}>${c}</${tag}>`).join('')+'</tr>';
}
out+=t+'</table>';continue;
}
if(/^#{1,4}\s/.test(l)){out+='<h3>'+inline(l.replace(/^#+\s*/,''))+'</h3>';i++;continue}
if(/^\s*[-*]\s/.test(l)){
let ul='<ul>';
while(i<lines.length&&/^\s*[-*]\s/.test(lines[i])){ul+='<li>'+inline(lines[i].replace(/^\s*[-*]\s/,''))+'</li>';i++}
out+=ul+'</ul>';continue;
}
if(l.trim()===''){i++;continue}
out+='<p>'+inline(l)+'</p>';i++;
}
return out;
}
function add(cls,text){const d=document.createElement('div');d.className='msg '+cls;d.textContent=text;log.appendChild(d);log.scrollTop=log.scrollHeight;return d}
f.onsubmit=async e=>{
e.preventDefault();const text=q.value.trim();if(!text)return;
q.value='';b.disabled=true;add('me',text);history.push({role:'user',content:text});
const d=add('bot','…');
try{
const r=await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:history})});
const reader=r.body.getReader(),dec=new TextDecoder();let reply='';
while(true){const{done,value}=await reader.read();if(done)break;reply+=dec.decode(value,{stream:true});d.innerHTML=md(reply);log.scrollTop=log.scrollHeight}
history.push({role:'assistant',content:reply});
}catch(err){d.textContent='Error: '+err+' — is the server running?'}
b.disabled=false;q.focus();
};
</script></body></html>"""
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = PAGE.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self):
if self.path != "/chat":
self.send_error(404)
return
length = int(self.headers.get("Content-Length", 0))
history = json.loads(self.rfile.read(length))["messages"]
messages = [{"role": "system", "content": SYSTEM}] + history
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
try:
for piece in stream_reply(messages):
self.wfile.write(piece.encode())
self.wfile.flush()
except BrokenPipeError:
pass # client closed the tab mid-stream
def log_message(self, *args):
pass # quiet
if __name__ == "__main__":
print(f"FR-Start web UI → http://localhost:{PORT} (model: {MODEL}, Ctrl-C to stop)")
ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()