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>'