"""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 = '''
COF / ROS RESEARCH ENVIRONMENT
❯ COFOS_
Your chemistry research terminal. Ask, trace, investigate.
● ZERO GPU / ONLINE
''' WELCOME = '''
  ┌─ COFOS / research session ─────────────┐
  │  Model reasoning. Traceable evidence. │
  └───────────────────────────────────────┘

Ask a chemistry question or describe what you want to find.

Try: What ROS does TT-T-COF generate?
Try: Find sources about H2O2 photocatalysis.

''' # 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'
EVIDENCE EXPLORER
◌
{msg}
Materials → relations → products
Click a node to inspect its source.
' nodes = [{'label':'QUERY','kind':'query','detail':'query'}] edges=[] details = [f'

Current investigation

{esc(question)}

Dashed edges mean retrieval relevance, not experimental support. Solid labelled edges come from dataset KG triples.

'] 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'') 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=[''] 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'') if not dashed: svg.append(f'{esc(label)}') for i,node in enumerate(nodes): x,y=positions[i];color={'query':'#c1d894','source':'#83acb0','entity':'#cda77c'}[node['kind']] svg.append(f'{esc(node["label"])}{esc(node["label"])}') svg.append('') return '
EVIDENCE EXPLORER
LIVE SUBGRAPH / '+str(len(items))+' SOURCES
'+''.join(svg)+'
● query · ● evidence · ● entity
Click a node to inspect evidence and support status.
'+''.join(details)+'
'