Spaces:
Sleeping
Sleeping
Download interface.py from Willlzh/COFOS-Chat: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/spaces/Willlzh/COFOS-Chat/resolve/main/interface.py
- Command line
-
hf download hf://spaces/Willlzh/COFOS-Chat/interface.py
-
curl -L -o interface.py https://huggingface.co/spaces/Willlzh/COFOS-Chat/resolve/main/interface.py
14.3 kB
| """Terminal styling, browser routing and a source-grounded SVG evidence graph.""" | |
| import html | |
| import math | |
| import re | |
| from retrieval import DATASET, REVISION | |
| CSS = ''' | |
| :root {--co-bg:#f4f5ef;--co-panel:#ffffff;--co-input:#fafbf7;--co-text:#20291d;--co-muted:#53634b;--co-border:#c6cebe;--co-accent:#426525;--co-button:#426525;--co-on-button:#ffffff;--co-hover:#e5ebdd;--co-source:#236e82;--co-entity:#8a4e20;color-scheme:light} | |
| :root[data-cofos-theme="dark"] {--co-bg:#111510;--co-panel:#1b2118;--co-input:#131910;--co-text:#edf2e5;--co-muted:#b5c3a7;--co-border:#53614a;--co-accent:#c2e297;--co-button:#c2e297;--co-on-button:#18240f;--co-hover:#2d3825;--co-source:#8bd3e5;--co-entity:#f0bd83;color-scheme:dark} | |
| body,.gradio-container{background:var(--co-bg)!important;color:var(--co-text)!important;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace!important} | |
| .gradio-container{max-width:1500px!important;padding:24px!important;--body-text-color:var(--co-text);--body-text-color-subdued:var(--co-muted);--body-background-fill:var(--co-bg);--block-background-fill:var(--co-panel);--input-background-fill:var(--co-input);--input-text-color:var(--co-text);--block-label-text-color:var(--co-muted)} | |
| .gradio-container *{color:var(--co-text)!important} | |
| .gradio-container svg{color:var(--co-text)!important} | |
| footer{display:none!important} | |
| .block,.form,.panel{background:transparent!important;border-color:var(--co-border)!important} | |
| #masthead{border-bottom:1px solid var(--co-border);padding-bottom:20px;margin-bottom:12px} | |
| .brand{font-size:30px;letter-spacing:-2px;font-weight:700}.brand span{color:var(--co-accent)!important} | |
| .eyebrow{font-size:11px;letter-spacing:2px;color:var(--co-muted)!important;margin-bottom:10px} | |
| .mast-row{display:flex;justify-content:space-between;align-items:center;gap:20px} | |
| .muted{color:var(--co-muted)!important;font-size:12px;line-height:1.7} | |
| .live{color:var(--co-accent)!important;font-size:11px;border:1px solid var(--co-border);padding:7px 10px;border-radius:4px} | |
| .theme-control{display:flex;gap:8px;align-items:center;margin-top:12px;font-size:12px} | |
| .theme-control select{background:var(--co-panel);color:var(--co-text);border:1px solid var(--co-border);padding:7px 28px 7px 10px;border-radius:4px;font:inherit;cursor:pointer} | |
| #terminal,#evidence{background:var(--co-panel)!important;border:1px solid var(--co-border)!important;border-radius:8px!important;padding:18px!important} | |
| #conversation{border:none!important;background:transparent!important} | |
| #conversation .message{font-family:inherit!important;font-size:14px!important;line-height:1.8!important;background:transparent!important} | |
| #conversation .message *,#conversation .prose *{color:var(--co-text)!important} | |
| #conversation pre,#conversation code,#route-log code{background:var(--co-input)!important;color:var(--co-text)!important} | |
| #conversation .icon-button{background:var(--co-hover)!important;color:var(--co-text)!important} | |
| #conversation .icon-button[aria-label="ζΈ η©Ίε―Ήθ―"],#conversation .icon-button[aria-label="Clear conversation"]{display:none!important} | |
| #prompt textarea{background:var(--co-input)!important;border:1px solid var(--co-border)!important;border-radius:5px!important;color:var(--co-text)!important;font-family:inherit!important;padding:15px!important;box-shadow:none!important} | |
| #prompt textarea::placeholder{color:var(--co-muted)!important;opacity:1!important} | |
| #prompt textarea:focus{border-color:var(--co-accent)!important;outline:2px solid var(--co-accent)!important;outline-offset:2px} | |
| button{font-family:inherit!important;border-radius:4px!important} | |
| #run,#run *{background:var(--co-button)!important;color:var(--co-on-button)!important;border:0!important;font-weight:700} | |
| #stop,#clear{background:var(--co-hover)!important;color:var(--co-text)!important;border:1px solid var(--co-border)!important} | |
| button:disabled{opacity:.55!important} | |
| #route-log{min-height:28px}#route-log p,#route-log code{font-family:inherit!important;font-size:12px!important;color:var(--co-accent)!important} | |
| .graph-toolbar{display:flex;justify-content:space-between;align-items:center;margin:12px 0;font-size:11px;letter-spacing:1px;gap:10px} | |
| .graph-toolbar button{border:1px solid var(--co-border);background:var(--co-hover);padding:5px 9px;cursor:pointer} | |
| .graph-canvas{background:radial-gradient(var(--co-border) 1px,transparent 1px);background-size:18px 18px;border:1px solid var(--co-border);border-radius:5px;overflow:hidden} | |
| .graph-canvas svg{display:block;width:100%;height:auto;min-height:260px} | |
| .graph-canvas text{fill:var(--co-text)!important;stroke:var(--co-panel)!important;stroke-width:3px;paint-order:stroke; font-size:11px} | |
| .graph-canvas line{stroke:var(--co-accent)!important}.graph-canvas line[stroke-dasharray]{stroke:var(--co-border)!important} | |
| .graph-node{cursor:pointer;outline:none}.graph-node circle{fill:var(--co-panel)!important;stroke:var(--co-source)!important} | |
| .graph-node[data-detail="query"] circle{stroke:var(--co-accent)!important} | |
| .graph-node:hover circle,.graph-node:focus circle,.graph-node.selected circle{stroke:var(--co-entity)!important;stroke-width:4px} | |
| .graph-detail{border-top:1px solid var(--co-border);margin-top:14px;padding-top:14px;font-size:12px;line-height:1.8;overflow-wrap:anywhere} | |
| .graph-detail p{font-size:12px!important;line-height:1.8!important}.graph-detail a{color:var(--co-accent)!important;text-decoration:underline} | |
| .graph-detail h4{font-size:13px!important;margin:0 0 8px} | |
| .legend{font-size:11px;line-height:1.8;margin:10px 0;color:var(--co-muted)!important} | |
| .empty-graph{padding:45px 16px;text-align:center;color:var(--co-muted)!important;font-size:12px;line-height:2} | |
| .bottom-line{border-top:1px solid var(--co-border);padding-top:14px;margin-top:15px;color:var(--co-muted)!important;font-size:11px;letter-spacing:.5px} | |
| @media(max-width:800px){.gradio-container{padding:12px!important}.brand{font-size:25px}.mast-row{align-items:flex-start;flex-wrap:wrap}.live{font-size:10px}#terminal{padding:10px!important}} | |
| ''' | |
| HEADER = '''<div id="masthead"><div class="eyebrow">COF / ROS RESEARCH ENVIRONMENT</div><div class="mast-row"><div><div class="brand"><span>β―</span> COFOS<span>_</span></div><div class="muted">Your chemistry research terminal. Ask, trace, investigate.</div></div><div class="live">β ZERO GPU / ONLINE</div></div><label class="theme-control">Appearance <select aria-label="Appearance"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select></label></div>''' | |
| WELCOME = '''<div style="padding:30px 10px;color:#b1bc9f;text-align:left"><pre style="color:#a8c67b;font-size:13px"> ββ COFOS / research session ββββββββββββββ | |
| β Model reasoning. Traceable evidence. β | |
| βββββββββββββββββββββββββββββββββββββββββ</pre><p>Ask a chemistry question or describe what you want to find.</p><p style="color:#77866a;font-size:12px">Try: What ROS does TT-T-COF generate?<br>Try: Find sources about H2O2 photocatalysis.</p></div>''' | |
| # This exact classifier runs in the browser before dispatch. Python mirrors it | |
| # for API clients and checks the hint; no model call is spent on routing. | |
| ROUTE_JS = r'''(question, history, route) => { | |
| const q = question.trim().toLowerCase(); | |
| const lookup = /^(find|search|look up|list|show|retrieve)\b/.test(q) && /\b(sources?|papers?|evidence|references?|documents?|studies)\b/.test(q); | |
| const chemistry = /cof|ros|h2o2|oxygen|photocatal|superoxide|singlet|radical|redox|mechanism|peroxide|evidence|papers?|sources?|ζ΄»ζ§ζ°§|ε ε¬ε|θΏζ°§εζ°’|ζ‘ζΆ/.test(q); | |
| const followup = history.length > 0 && /\b(it|its|they|their|those|this|that|more|why|how)\b/.test(q); | |
| route = lookup ? 'retrieval' : ((chemistry || followup) ? 'rag' : 'model'); | |
| return [question, history, route]; | |
| }''' | |
| def route_query(question, history): | |
| q = question.strip().lower() | |
| lookup = re.search(r'^(find|search|look up|list|show|retrieve)\b', q) and re.search(r'\b(sources?|papers?|evidence|references?|documents?|studies)\b', q) | |
| chemistry = re.search(r'cof|ros|h2o2|oxygen|photocatal|superoxide|singlet|radical|redox|mechanism|peroxide|evidence|papers?|sources?|ζ΄»ζ§ζ°§|ε ε¬ε|θΏζ°§εζ°’|ζ‘ζΆ', q) | |
| followup = bool(history) and re.search(r'\b(it|its|they|their|those|this|that|more|why|how)\b', q) | |
| return 'retrieval' if lookup else ('rag' if chemistry or followup else 'model') | |
| THEME_JS = ''' | |
| const picker = element.querySelector('select'); | |
| const media = window.matchMedia('(prefers-color-scheme: dark)'); | |
| let mode='system'; | |
| try {mode=localStorage.getItem('cofos-appearance')||'system';} catch (_) {} | |
| if (!['system','light','dark'].includes(mode)) mode='system'; | |
| picker.value=mode; | |
| function apply(){document.documentElement.dataset.cofosTheme=mode==='system'?(media.matches?'dark':'light'):mode;} | |
| picker.addEventListener('change',()=>{mode=picker.value;try{localStorage.setItem('cofos-appearance',mode);}catch(_){}apply();}); | |
| media.addEventListener('change',()=>{if(mode==='system')apply();}); | |
| apply(); | |
| ''' | |
| GRAPH_JS = ''' | |
| function selectNode(node) { | |
| const root = node.closest('.evidence-graph'); | |
| root.querySelectorAll('.graph-node').forEach(n => n.classList.remove('selected')); | |
| node.classList.add('selected'); | |
| const key = node.dataset.detail; | |
| root.querySelectorAll('.graph-detail').forEach(d => { d.hidden = d.dataset.key !== key; }); | |
| } | |
| element.addEventListener('click', e => { | |
| const node = e.target.closest('.graph-node'); if (node) selectNode(node); | |
| const btn = e.target.closest('[data-zoom]'); | |
| if (btn) { const svg=btn.closest('.evidence-graph').querySelector('svg'); | |
| const old=Number(svg.dataset.scale||1); const scale=btn.dataset.zoom==='reset'?1:Math.min(2.5,Math.max(.6,old*Number(btn.dataset.zoom))); | |
| svg.dataset.scale=scale; svg.querySelector('.graph-world').setAttribute('transform',`translate(260 195) scale(${scale}) translate(-260 -195)`); } | |
| }); | |
| element.addEventListener('keydown',e=>{if((e.key==='Enter'||e.key===' ')&&e.target.matches('.graph-node')){e.preventDefault();selectNode(e.target);}}); | |
| ''' | |
| def esc(value): | |
| return html.escape(str(value), quote=True) | |
| def graph_html(items, question='', route='idle'): | |
| if not items: | |
| msg = 'Evidence will appear here as your investigation unfolds.' if route == 'idle' else ('Model-only response. No dataset evidence was retrieved.' if route == 'model' else 'No matching evidence. Try a material name or different keywords.') | |
| return f'<div class="evidence-graph"><div class="eyebrow">EVIDENCE EXPLORER</div><div class="empty-graph">β<br>{msg}</div><div class="legend">Materials β relations β products<br>Click a node to inspect its source.</div></div>' | |
| nodes = [{'label':'QUERY','kind':'query','detail':'query'}] | |
| edges=[] | |
| details = [f'<div class="graph-detail" data-key="query"><h4>Current investigation</h4><p>{esc(question)}</p><p>Dashed edges mean retrieval relevance, not experimental support. Solid labelled edges come from dataset KG triples.</p></div>'] | |
| entity_ids={} | |
| for i,item in enumerate(items,1): | |
| key=f'source-{i}'; idx=len(nodes) | |
| nodes.append({'label':f'[{i}] '+item['kind'],'kind':'source','detail':key}) | |
| edges.append((0,idx,'retrieved',True)) | |
| url=f'https://huggingface.co/datasets/{DATASET}/blob/{REVISION}/{item["file"]}' | |
| details.append(f'<div class="graph-detail" data-key="{key}" hidden><h4>[{i}] {esc(item["kind"])} / row {item["row"]}</h4><p>Level: {esc(item.get("evidence_level","unspecified"))} Β· Support: {esc(item.get("support_status","unspecified"))}</p><p>{esc(item["text"][:2200])}</p><a href="{esc(url)}" target="_blank" rel="noopener noreferrer">Open dataset source β</a></div>') | |
| if item['kind']=='KG': | |
| pair=[] | |
| for field in ('head','tail'): | |
| entity=item.get(field,'') | |
| if entity not in entity_ids: | |
| entity_ids[entity]=len(nodes) | |
| label=entity.split(':',1)[-1].replace('_',' ') | |
| nodes.append({'label':label[:25]+('β¦' if len(label)>25 else ''),'kind':'entity','detail':key}) | |
| pair.append(entity_ids[entity]) | |
| edges.append((idx,pair[0],'describes',True)) | |
| edges.append((pair[0],pair[1],item.get('relation','related').replace('_',' '),False)) | |
| n=len(nodes)-1 | |
| positions=[(260,195)] + [(260+178*math.cos(-math.pi/2+2*math.pi*j/n),195+133*math.sin(-math.pi/2+2*math.pi*j/n)) for j in range(n)] | |
| svg=['<svg viewBox="0 0 520 390" role="group" aria-label="Interactive evidence knowledge graph"><g class="graph-world">'] | |
| for a,b,label,dashed in edges: | |
| x,y=positions[a];xx,yy=positions[b] | |
| dash='stroke-dasharray="4,5"' if dashed else '' | |
| svg.append(f'<line x1="{x}" y1="{y}" x2="{xx}" y2="{yy}" stroke="{"#526045" if dashed else "#b0c885"}" stroke-width="1.4" {dash}/>') | |
| if not dashed: | |
| svg.append(f'<text x="{(x+xx)/2}" y="{(y+yy)/2-8}" fill="#d1dba9" font-size="9" text-anchor="middle" stroke="#151812" stroke-width="3" paint-order="stroke">{esc(label)}</text>') | |
| for i,node in enumerate(nodes): | |
| x,y=positions[i];color={'query':'#c1d894','source':'#83acb0','entity':'#cda77c'}[node['kind']] | |
| svg.append(f'<g class="graph-node" data-detail="{node["detail"]}" role="button" tabindex="0" aria-label="Inspect {esc(node["label"])}"><circle cx="{x}" cy="{y}" r="{19 if i==0 else 12}" fill="#1b2416" stroke="{color}" stroke-width="2"/><text x="{x}" y="{y+32}" text-anchor="middle" fill="{color}" font-size="10">{esc(node["label"])}</text><title>{esc(node["label"])}</title></g>') | |
| svg.append('</g></svg>') | |
| return '<div class="evidence-graph"><div class="eyebrow">EVIDENCE EXPLORER</div><div class="graph-toolbar"><span>LIVE SUBGRAPH / '+str(len(items))+' SOURCES</span><span><button data-zoom="1.2" aria-label="Zoom in">+</button> <button data-zoom="0.833" aria-label="Zoom out">β</button> <button data-zoom="reset">Reset</button></span></div><div class="graph-canvas">'+''.join(svg)+'</div><div class="legend"><b>β</b> query Β· <span style="color:#83acb0">β</span> evidence Β· <span style="color:#cda77c">β</span> entity<br>Click a node to inspect evidence and support status.</div>'+''.join(details)+'</div>' | |