File size: 6,250 Bytes
4ba0891
 
 
 
 
 
 
 
07a16a7
4ba0891
 
 
 
07a16a7
4ba0891
 
 
 
 
 
 
 
5037497
 
 
 
 
 
 
 
4ba0891
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5037497
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4ba0891
 
 
 
 
 
 
 
5037497
4ba0891
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
07a16a7
 
4ba0891
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/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()