Spaces:
Sleeping
Sleeping
File size: 14,329 Bytes
dcce5d5 40f5226 9fd5661 40f5226 dcce5d5 40f5226 dcce5d5 40f5226 dcce5d5 40f5226 dcce5d5 9fd5661 dcce5d5 | 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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | """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>'
|