| |
| """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,'&').replace(/</g,'<').replace(/>/g,'>'); |
| 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 |
|
|
| def log_message(self, *args): |
| pass |
|
|
|
|
| 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() |
|
|