File size: 31,385 Bytes
06617ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GFP Mutation Explorer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.9/babel.min.js"></script>
<style>
body { margin: 0; padding: 0; }
* { -webkit-font-smoothing: antialiased; }
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
var useState = React.useState, useCallback = React.useCallback, useEffect = React.useEffect, useRef = React.useRef;

var SEQ = "SKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTGKLPVPWPTLVTTLSYGVQCFSRYPDHMKQHDFFKSAMPEGYVQERTIFFKDDGNYKTRAEVKFEGDTLVNRIELKGIDFKEDGNILGHKLEYNYNSHNVYIMADKQKNGIKVNFKIRHNIEDGSVQLADHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITHGMDELYK";
var AAs = ["A","C","D","E","F","G","H","I","K","L","M","N","P","Q","R","S","T","V","W","Y"];
var AA3 = {A:"Ala",C:"Cys",D:"Asp",E:"Glu",F:"Phe",G:"Gly",H:"His",I:"Ile",K:"Lys",L:"Leu",M:"Met",N:"Asn",P:"Pro",Q:"Gln",R:"Arg",S:"Ser",T:"Thr",V:"Val",W:"Trp",Y:"Tyr"};
var AANAME = {A:"Alanine",C:"Cysteine",D:"Aspartate",E:"Glutamate",F:"Phenylalanine",G:"Glycine",H:"Histidine",I:"Isoleucine",K:"Lysine",L:"Leucine",M:"Methionine",N:"Asparagine",P:"Proline",Q:"Glutamine",R:"Arginine",S:"Serine",T:"Threonine",V:"Valine",W:"Tryptophan",Y:"Tyrosine"};
var CHROMO = new Set([63,64,65]);
var CA_Z=[-16.0,-18.7,-17.3,-15.6,-18.4,-17.4,-14.4,-15.3,-16.0,-14.3,-14.3,-13.3,-10.6,-10.1,-6.8,-6.3,-2.8,-2.2,1.3,4.6,8.3,11.0,10.4,7.7,3.9,2.2,-1.1,-1.7,-5.0,-5.2,-7.8,-8.1,-9.7,-9.1,-10.4,-9.7,-10.7,-7.4,-5.4,-5.1,-4.6,-4.3,-3.9,-2.9,-1.6,-1.6,0.0,-2.3,0.4,3.1,6.3,6.3,9.0,9.4,10.3,7.3,9.0,8.7,5.4,3.9,4.8,2.1,-0.3,-0.5,-0.0,-2.9,-5.5,-4.5,-8.2,-7.8,-5.3,-6.1,-6.2,-6.6,-9.9,-8.3,-7.2,-10.6,-10.6,-7.6,-9.5,-9.3,-9.0,-12.3,-14.3,-13.5,-15.5,-17.7,-17.2,-13.5,-10.2,-7.4,-3.8,-1.1,2.4,6.0,8.7,12.4,15.0,18.5,20.8,17.8,14.6,11.3,7.7,4.8,1.3,-2.3,-4.8,-8.5,-10.0,-13.8,-16.3,-18.5,-21.2,-20.0,-16.3,-13.2,-11.7,-8.2,-7.7,-4.2,-2.4,1.4,4.2,8.0,10.5,14.3,14.9,18.1,18.8,21.8,19.9,18.4,14.8,14.8,17.7,15.8,18.5,16.0,15.3,11.7,12.0,10.1,11.3,9.4,8.4,7.0,5.0,4.4,1.1,0.3,-2.8,-2.5,-5.5,-5.1,-5.6,-8.3,-6.2,-3.1,0.7,2.5,6.3,8.5,11.7,11.4,14.1,14.3,16.6,19.1,20.1,23.8,23.1,23.0,19.8,18.0,16.4,13.7,11.9,8.1,6.0,2.2,-0.7,-4.1,-7.3,-11.0,-12.3,-15.9,-18.1,-18.5,-16.4,-13.4,-11.4,-8.0,-5.4,-2.7,0.9,0.9,2.4,1.5,3.5,3.6,4.8,4.8,5.8,5.9,5.1,4.0,1.0,2.4,-0.5,-0.3,1.6,1.3,3.8,2.1,2.8,1.1,1.0,0.6,0.3,-0.5,-0.9,-1.5,-1.5,0.5,0.9,3.9,5.1,2.2,-0.2,1.0,1.5,-2.2,-3.1,-1.8,-2.0];
var ssArr = new Array(SEQ.length).fill("C");
[[3,6],[55,62],[67,69],[74,79],[81,84],[154,156]].forEach(function(p){for(var i=p[0];i<=p[1]&&i<SEQ.length;i++)ssArr[i]="H";});
[[9,20],[23,34],[39,46],[90,98],[103,113],[116,126],[146,153],[158,168],[174,185],[197,206],[215,225]].forEach(function(p){for(var i=p[0];i<=p[1]&&i<SEQ.length;i++)ssArr[i]="E";});
var HYDRO=new Set(["A","V","I","L","M","F","W","P"]),POLAR=new Set(["S","T","Y","H","C","N","Q"]),POSC=new Set(["K","R"]),NEGC=new Set(["D","E"]);
function sameGrp(a,b){return(HYDRO.has(a)&&HYDRO.has(b))||(POLAR.has(a)&&POLAR.has(b))||(POSC.has(a)&&POSC.has(b))||(NEGC.has(a)&&NEGC.has(b));}
function hash(a,b){var h=((a*2654435761)^(b*2246822519))>>>0;h=((h>>16)^h)*0x45d9f3b;h=((h>>16)^h)*0x45d9f3b;return((h>>16)^h)&0xffff;}
// ── Real scores from METL model (injected by backend) ──
var oScores = /*__ORACLE_SCORES__*/[];
var dScores = /*__DESIGN_SCORES__*/[];

var allM=[];for(var p2=0;p2<SEQ.length;p2++)for(var a2=0;a2<20;a2++){var sc=oScores[p2][a2];if(sc!==null)allM.push({pos:p2,aaIdx:a2,score:sc});}
allM.sort(function(a,b){return b.score-a.score;});
var trainData=[],trainSet=new Set(),sampPos=new Set();
for(var i0=0;i0<allM.length&&trainData.length<8;i0++){var mm=allM[i0];if(!sampPos.has(mm.pos)){trainData.push(mm);sampPos.add(mm.pos);trainSet.add(mm.pos+"-"+mm.aaIdx);}}
var botM=allM.slice().reverse();for(var i1=0;i1<botM.length&&trainData.length<18;i1++){var m2=botM[i1];if(!sampPos.has(m2.pos)){trainData.push(m2);sampPos.add(m2.pos);trainSet.add(m2.pos+"-"+m2.aaIdx);}}
for(var p3=0;p3<SEQ.length;p3+=4){if(trainData.length>=64)break;if(!sampPos.has(p3)){var a3=hash(p3,999)%20;var s3=oScores[p3][a3];if(s3!==null){trainData.push({pos:p3,aaIdx:a3,score:s3});sampPos.add(p3);trainSet.add(p3+"-"+a3);}}}
trainData.sort(function(a,b){return a.pos-b.pos;});
var dRnk=[];for(var p4=0;p4<SEQ.length;p4++)for(var a4=0;a4<20;a4++){var ds=dScores[p4][a4];if(ds!==null&&!trainSet.has(p4+"-"+a4))dRnk.push({pos:p4,aaIdx:a4,ds:ds,os:oScores[p4][a4]});}
dRnk.sort(function(a,b){return b.ds-a.ds;});
var aiPicks=dRnk.slice(0,5).map(function(m){return{pos:m.pos,aaIdx:m.aaIdx,score:m.os};});

var CA2D=[[97.4,144],[110.4,143.5],[125.6,135.2],[115.4,122.3],[119.6,110.4],[137.3,109.6],[134.1,99.2],[119.3,89],[129.4,74],[144.7,68.6],[158.1,81],[175.7,77.3],[181.8,88.9],[198.7,95.7],[202.8,104.4],[219,112.7],[222.3,119.4],[238,128.5],[240.2,135.1],[247.8,138.3],[244.3,137.5],[253.1,146.8],[270,140],[267.3,126.8],[265.5,125],[254.4,112.7],[246.5,107.9],[231.7,97.3],[225.7,90.7],[208.5,84.5],[201.1,73.8],[184,67.8],[167.3,69.4],[151.4,78.2],[134,78.1],[122.5,92.4],[107.8,82.3],[109.7,73],[120,85],[133,72],[150.9,75.8],[165.3,64.3],[182.6,70.7],[198.6,62.8],[210.7,75],[228.7,71.2],[242.4,80.6],[256.8,81.6],[266.8,89.9],[265.2,77],[255.6,78],[242.9,91.2],[246.5,104.6],[228.4,107.4],[215.7,94.4],[205.8,88.6],[189.6,93.7],[194.3,111.4],[203.2,110.2],[187.6,103.1],[179,119],[189.4,126.8],[188.8,112.5],[171.4,106],[161.2,121.1],[170.7,127.6],[162.2,116.5],[144.9,121.2],[141,122],[136.9,104.1],[123.1,107.2],[106.3,100.5],[93,113.1],[74.9,109.1],[68,114.3],[59,128.3],[75.3,135.5],[84.4,134.6],[84.8,153.1],[96.2,152],[109.5,142.8],[122.6,156.1],[135.8,143.7],[130.8,136.1],[125.6,151.3],[142.3,158.5],[149.5,144.3],[158.8,140.4],[166.9,156.9],[166.8,154],[165.3,162],[176.5,167.7],[174.8,163.9],[182.1,174.8],[182.5,166.8],[186.8,168.8],[184.1,156.3],[185.5,161.4],[188.2,148.7],[182.9,152.4],[197.7,153.2],[208.4,148.3],[214.9,156.2],[207.3,161.1],[208.1,154.1],[207.8,165.7],[205.8,159.8],[200.7,163],[194.3,150.7],[191.7,151.1],[179.5,139.6],[179.6,137.7],[173.4,125.3],[187.5,120.2],[187.7,107.5],[175.2,95.2],[178.4,97.4],[176.3,108.2],[191.1,115.9],[192.2,123.6],[208.4,132.7],[211.2,138.7],[223.1,150],[223,147.3],[232,155.8],[229,153.3],[232.8,166.1],[234.4,166.7],[233.5,148.5],[241,141.5],[249.1,125.3],[238.1,122],[222.7,126],[211.9,113.4],[217.2,116.3],[235.3,118.4],[236.5,106.5],[227.3,93.2],[214.5,90.4],[200.4,88.7],[191.4,73.1],[189.7,67.6],[171.4,67.8],[162.6,81.1],[146.2,87.9],[130.8,91.8],[128.2,109.6],[116.5,122.1],[124.5,135.8],[115.5,151.4],[115.8,159.7],[110.2,176.9],[111.9,187.2],[121.7,202.5],[116.1,212.2],[128,226.1],[142.2,214.6],[133.5,205.1],[135.1,189.9],[132.1,179.9],[134.8,181.5],[137.1,165.4],[136.3,163.7],[137.4,148.9],[144.1,141.9],[154,126.8],[155.9,113.9],[171.7,104.4],[172.1,89.8],[186,89.1],[192,72.3],[188.7,75.7],[170.5,77.8],[170.3,96.3],[166.7,105.8],[176.5,119.1],[165.7,131.5],[169.8,143.3],[161.6,157.2],[162.6,155.1],[160.3,169.9],[160.6,169.4],[158.3,181.3],[154.7,174.2],[155.2,184.1],[156.5,179.3],[173.7,178.8],[167.4,180.7],[159,192.9],[141.9,186.2],[126.4,188],[124.2,176.8],[109.2,172],[107.5,180.4],[97.6,170.1],[85.5,173.7],[89,168.4],[91.3,150.2],[102.5,137.8],[111,122.4],[109.2,107.3],[125.5,99],[130,82.3],[147.2,76.3],[158.2,62.5],[176.4,60.5],[184.5,44.4],[200.2,36.5],[206.3,26.5],[214,10.7],[225.5,10],[232.4,27.2],[248,30.3],[247.9,48.7],[239.7,60.2],[223.3,61.4],[205.4,62.8],[190.7,55.8],[176.4,67.2],[159.1,61.7],[146,74.9],[128.4,78.1],[121.8,95.1],[104.5,100.9],[95.8,117.1],[81.3,122.9],[73.3,139.4],[71,150.6],[54.8,157],[46.2,164.4],[57.7,155.7],[50.8,139.7],[33.4,145.3],[32.9,150],[35.4,132.2],[18.5,129.1],[10,145.5]];

function sColor(s){if(s===null)return"transparent";if(s<-2.5)return"#7f1d1d";if(s<-1.5)return"#b91c1c";if(s<-0.8)return"#ef4444";if(s<-0.3)return"#fca5a5";if(s<0.2)return"#fef3f2";if(s<0.7)return"#dbeafe";if(s<1.2)return"#60a5fa";if(s<1.8)return"#2563eb";return"#1e3a8a";}
function sLabel(s){if(s>1)return"Highly beneficial";if(s>0.3)return"Mildly beneficial";if(s>-0.3)return"Near neutral";if(s>-1.5)return"Deleterious";return"Highly deleterious";}
function ssClr(i){if(CHROMO.has(i))return"#22c55e";if(ssArr[i]==="E")return"#3b82f6";if(ssArr[i]==="H")return"#a855f7";return"#94a3b8";}

var CELL=14,PP=50;

function StructView(props){
  var hl=props.hl,onH=props.onH,selP=props.selP||[];var sS=new Set(selP);
  function spline(pts,n){if(pts.length<2)return pts;var r=[];for(var i=0;i<pts.length-1;i++){var p0=pts[Math.max(0,i-1)],p1=pts[i],p2=pts[i+1],p3=pts[Math.min(pts.length-1,i+2)];for(var t=0;t<n;t++){var u=t/n,u2=u*u,u3=u2*u;r.push([.5*((2*p1[0])+(-p0[0]+p2[0])*u+(2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*u2+(-p0[0]+3*p1[0]-3*p2[0]+p3[0])*u3),.5*((2*p1[1])+(-p0[1]+p2[1])*u+(2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*u2+(-p0[1]+3*p1[1]-3*p2[1]+p3[1])*u3)])}}r.push(pts[pts.length-1]);return r}
  var segs=[],si=0;while(si<SEQ.length){var ss=ssArr[si],sj=si;while(sj<SEQ.length&&ssArr[sj]===ss)sj++;segs.push({t:ss,s:si,e:sj-1});si=sj}
  var els=[];
  segs.forEach(function(seg,idx){
    var raw=[];for(var k=seg.s;k<=seg.e&&k<CA2D.length;k++)raw.push(CA2D[k]);if(raw.length<2)return;
    var sm=spline(raw,5);var d="M"+sm.map(function(p){return p[0].toFixed(1)+","+p[1].toFixed(1)}).join("L");
    if(seg.t==="E"){
      els.push(<path key={"eo"+idx} d={d} fill="none" stroke="#166534" strokeWidth={6} opacity={0.7} strokeLinecap="round" strokeLinejoin="round"/>);
      els.push(<path key={"ei"+idx} d={d} fill="none" stroke="#4ade80" strokeWidth={3} opacity={0.85} strokeLinecap="round" strokeLinejoin="round"/>);
      var L=sm[sm.length-1],P=sm[Math.max(0,sm.length-4)];var dx=L[0]-P[0],dy=L[1]-P[1],dl=Math.sqrt(dx*dx+dy*dy);if(dl>0){dx/=dl;dy/=dl}
      var tp=[L[0]+dx*3,L[1]+dy*3];els.push(<polygon key={"ea"+idx} points={(L[0]-dy*4.5)+","+(L[1]+dx*4.5)+" "+tp[0]+","+tp[1]+" "+(L[0]+dy*4.5)+","+(L[1]-dx*4.5)} fill="#4ade80" opacity={0.85}/>);
    }else if(seg.t==="H"){
      els.push(<path key={"ho"+idx} d={d} fill="none" stroke="#155e75" strokeWidth={5} opacity={0.7} strokeLinecap="round" strokeLinejoin="round"/>);
      els.push(<path key={"hi"+idx} d={d} fill="none" stroke="#22d3ee" strokeWidth={2.5} opacity={0.85} strokeLinecap="round" strokeLinejoin="round"/>);
    }else{
      els.push(<path key={"c"+idx} d={d} fill="none" stroke="#4ade80" strokeWidth={1} opacity={0.2} strokeLinecap="round"/>)}
  });
  var dots=[];
  for(var j=0;j<CA2D.length;j++){var isHL=hl===j,isSel=sS.has(j),isChr=CHROMO.has(j);
    if(isHL||isSel||isChr){dots.push(<circle key={"d"+j} cx={CA2D[j][0]} cy={CA2D[j][1]} r={isHL?5:isSel?4:3} fill={isHL?"#ec4899":isSel?"#f97316":"#22c55e"} stroke="white" strokeWidth={1}/>)}
    dots.push(<circle key={"h"+j} cx={CA2D[j][0]} cy={CA2D[j][1]} r={5} fill="transparent" style={{cursor:"pointer"}} onMouseEnter={function(idx){return function(){onH(idx)}}(j)} onMouseLeave={function(){onH(null)}}/>)}
  return(
    <div style={{borderRadius:8,overflow:"hidden",border:"1px solid #1e293b",background:"#0f172a",padding:8,textAlign:"center"}}>
      <div style={{fontSize:10,fontWeight:700,color:"#94a3b8",marginBottom:2}}>GFP Structure</div>
      <svg viewBox="-5 -5 300 256" style={{width:"100%",maxWidth:260}}>{els}{dots}</svg>
      <div style={{display:"flex",justifyContent:"center",gap:10,fontSize:8,marginTop:3}}>
        <span style={{color:"#4ade80"}}>━ strand</span>
        <span style={{color:"#22d3ee"}}>━ helix</span>
        <span style={{color:"#22c55e"}}>● chr</span>
      </div>
      <div style={{fontSize:10,fontWeight:600,color:"#ec4899",marginTop:2,height:16,overflow:"hidden"}}>{hl!==null&&hl>=0&&hl<SEQ.length?("Pos "+hl+": "+SEQ[hl]+" ("+AA3[SEQ[hl]]+")"):" "}</div>
    </div>);
}





function SeqStrip(props){
  var hl=props.hl,onClick=props.onClick;var ref=useRef(null);
  useEffect(function(){if(hl!==null&&ref.current){var el=ref.current.querySelector("[data-p='"+hl+"']");if(el)el.scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"});}},[hl]);
  return(
    <div ref={ref} style={{overflowX:"auto",padding:"4px 0",background:"#f8fafc",borderRadius:8,border:"1px solid #e2e8f0",marginBottom:8}}>
      <div style={{display:"flex",paddingLeft:4,minWidth:SEQ.length*14}}>
        {SEQ.split("").map(function(aa,i){var isH=hl===i;return <div key={i} data-p={i} onClick={function(){onClick(i);}} style={{width:14,height:18,display:"flex",alignItems:"center",justifyContent:"center",fontFamily:"monospace",fontSize:9,fontWeight:isH?800:400,color:isH?"white":CHROMO.has(i)?"#16a34a":"#475569",background:isH?"#2563eb":ssArr[i]==="E"?"#eff6ff":ssArr[i]==="H"?"#faf5ff":"transparent",borderRadius:isH?3:0,cursor:"pointer"}}>{aa}</div>;})}
      </div>
      <div style={{display:"flex",paddingLeft:4,minWidth:SEQ.length*14}}>
        {SEQ.split("").map(function(_,i){return <div key={i} style={{width:14,textAlign:"center",fontSize:6,color:i%10===0?"#475569":i%5===0?"#94a3b8":"transparent"}}>{i%5===0?String(i):""}</div>;})}
      </div>
    </div>);
}

function Grid(props){
  var ps=props.ps,pe=props.pe,scores=props.scores,hover=props.hover,setHover=props.setHover,sel=props.sel,toggle=props.toggle,showAll=props.showAll!==false,showDots=props.showDots===true;
  var cols=pe-ps,rows=[];
  for(var ai=0;ai<20;ai++){var cells=[];for(var c=0;c<cols;c++){(function(pos,aaIdx){
    var s=scores[pos][aaIdx],isWT=SEQ[pos]===AAs[aaIdx],isSel=sel.some(function(m){return m.pos===pos&&m.aaIdx===aaIdx;}),isHov=hover&&hover.pos===pos&&hover.aaIdx===aaIdx,isTr=trainSet.has(pos+"-"+aaIdx);
    var bg=isWT?"transparent":(showAll||isTr)?sColor(s):"#f1f5f9";
    var brd=isSel?"2px solid #f59e0b":isWT?"1px solid #94a3b8":(isHov&&!isWT)?"1px solid #2563eb":"0.5px solid rgba(0,0,0,0.05)";
    cells.push(<div key={c} onMouseEnter={function(){setHover({pos:pos,aaIdx:aaIdx});}} onMouseLeave={function(){setHover(null);}} onClick={function(){if(!isWT&&s!==null)toggle(pos,aaIdx);}} style={{width:CELL,height:CELL,boxSizing:"border-box",background:bg,border:brd,cursor:isWT?"default":"pointer",borderRadius:isSel?2:0,transform:"none",zIndex:isHov?10:isSel?5:1,position:"relative",display:"flex",alignItems:"center",justifyContent:"center"}}>
      {isSel&&<span style={{fontSize:6,color:"#92400e"}}>{"✦"}</span>}
      {showDots&&isTr&&!isSel&&<span style={{fontSize:4,color:"rgba(0,0,0,0.5)"}}>{"●"}</span>}
    </div>);})(ps+c,ai);}rows.push(<div key={ai} style={{display:"flex",height:CELL}}>{cells}</div>);}
  return(
    <div style={{overflowX:"auto",overflowY:"hidden",maxWidth:"100%"}}>
      <div style={{display:"inline-block"}}>
        <div style={{marginLeft:24,display:"flex",marginBottom:1}}>{Array.from({length:cols},function(_,c){var pos=ps+c;return <div key={c} style={{width:CELL,textAlign:"center",fontSize:6,fontWeight:700,color:CHROMO.has(pos)?"#16a34a":"#aaa",fontFamily:"monospace"}}>{SEQ[pos]}</div>;})}</div>
        <div style={{display:"flex"}}><div style={{width:24,flexShrink:0}}>{AAs.map(function(aa){return <div key={aa} style={{height:CELL,display:"flex",alignItems:"center",justifyContent:"flex-end",paddingRight:3,fontSize:8,fontWeight:700,fontFamily:"monospace",color:"#374151"}}>{aa}</div>;})}</div><div>{rows}</div></div>
        <div style={{marginLeft:24,display:"flex"}}>{Array.from({length:cols},function(_,c){var pos=ps+c;return <div key={c} style={{width:CELL,textAlign:"center",fontSize:6,color:pos%10===0?"#333":pos%5===0?"#999":"transparent",fontWeight:pos%10===0?700:400,marginTop:1}}>{pos%5===0?String(pos):""}</div>;})}</div>
      </div>
    </div>);
}

function PageNav(props){
  var pg=props.pg,set=props.set,tot=props.tot;var btns=[];
  for(var i=0;i<tot;i++){(function(idx){btns.push(<button key={idx} onClick={function(){set(idx);}} style={{padding:"3px 8px",borderRadius:4,border:pg===idx?"2px solid #2563eb":"1px solid #ddd",background:pg===idx?"#eff6ff":"white",fontWeight:pg===idx?700:400,cursor:"pointer",fontFamily:"inherit",fontSize:10}}>{idx*PP+"-"+Math.min((idx+1)*PP-1,SEQ.length-1)}</button>);})(i);}
  return <div style={{display:"flex",gap:3,marginBottom:6,flexWrap:"wrap"}}>{btns}</div>;
}

function Tip(props){
  var h=props.h,scores=props.scores,extra=props.extra;var pos=h?h.pos:0,ai=h?h.aaIdx:0,isWT=h?(SEQ[pos]===AAs[ai]):true,s=h?scores[pos][ai]:0;
  return(
    <div style={{background:"white",border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 10px",boxShadow:"0 2px 10px rgba(0,0,0,0.1)",fontSize:11,height:30,overflow:"hidden",opacity:h?1:0,transition:"opacity 0.1s"}}>
      <b>{"Pos "+pos}</b>{h&&CHROMO.has(pos)?" 🟒":""}
      {" Β· "+(ssArr[pos]==="E"?"Ξ²":ssArr[pos]==="H"?"Ξ±":"coil")+" Β· "}
      {isWT?<span>{SEQ[pos]+" (wild type)"}</span>:(
        <span><span style={{color:"#dc2626"}}>{SEQ[pos]}</span>{"β†’"}<span style={{color:"#2563eb",fontWeight:700}}>{AAs[ai]}</span>{" = "}<b style={{color:s>0?"#2563eb":"#dc2626"}}>{s!==null?s.toFixed(2):"?"}</b>{" "+sLabel(s)}</span>
      )}
      {extra&&<span style={{color:extra.color,marginLeft:4}}>{extra.text}</span>}
    </div>);
}

function Legend(){return(
  <div style={{display:"flex",gap:8,fontSize:9,color:"#64748b",flexWrap:"wrap",marginTop:6,marginBottom:6}}>
    <span><span style={{display:"inline-block",width:30,height:6,borderRadius:2,background:"linear-gradient(90deg,#b91c1c,#fef3f2,#2563eb)",verticalAlign:"middle"}}/> Del→Ben</span>
    <span><span style={{display:"inline-block",width:8,height:8,border:"1px solid #94a3b8",verticalAlign:"middle"}}/> WT</span>
    <span><span style={{display:"inline-block",width:8,height:8,border:"2px solid #f59e0b",borderRadius:2,verticalAlign:"middle"}}/> Sel</span>
    <span><span style={{display:"inline-block",width:8,height:3,background:"#22c55e",borderRadius:1,verticalAlign:"middle"}}/> Chrom</span>
  </div>);}

function App(){
  var s1=useState("learn"),tab=s1[0],setTab=s1[1];
  var s2=useState(0),pg=s2[0],setPg=s2[1];
  var s3=useState(null),hov=s3[0],setHov=s3[1];
  var s4=useState([]),sel=s4[0],setSel=s4[1];
  var s5=useState(false),pred=s5[0],setPred=s5[1];
  var s6=useState(false),pding=s6[0],setPding=s6[1];
  var s7=useState([]),hPicks=s7[0],setHP=s7[1];
  var s8=useState("learn"),cStep=s8[0],setCS=s8[1];
  var s9=useState(-1),revI=s9[0],setRevI=s9[1];
  var s10=useState(null),sHL=s10[0],setSHL=s10[1];
  var s11=useState(false),aiRev=s11[0],setAiRev=s11[1];

  var totP=Math.ceil(SEQ.length/PP),ps=pg*PP,pe=Math.min(ps+PP,SEQ.length);
  useEffect(function(){setSHL(hov?hov.pos:null);},[hov]);
  useEffect(function(){if(aiRev&&revI>=0&&revI<aiPicks.length){var t=setTimeout(function(){setRevI(function(v){return v+1;});},700);return function(){clearTimeout(t);};}},[aiRev,revI]);

  var toggle=useCallback(function(pos,ai){
    var sL=tab==="compete"?setHP:setSel;var mx=tab==="compete"?5:10;var k=pos+"-"+ai;
    sL(function(prev){if(prev.find(function(m){return m.pos+"-"+m.aaIdx===k;}))return prev.filter(function(m){return m.pos+"-"+m.aaIdx!==k;});if(prev.length>=mx)return prev;return prev.concat([{pos:pos,aaIdx:ai,score:oScores[pos][ai]}]);});
    if(tab!=="compete")setPred(false);},[tab]);

  var doPredict=useCallback(function(){setPding(true);setTimeout(function(){setPding(false);setPred(true);},1200);},[]);
  var hScore=hPicks.reduce(function(s,m){return s+m.score;},0);
  var aiSc=aiPicks.reduce(function(s,m){return s+m.score;},0);
  function resetC(){setCS("learn");setHP([]);setAiRev(false);setRevI(-1);}
  function getEx(){if(!hov)return null;var k=hov.pos+"-"+hov.aaIdx;if(tab==="compete"&&cStep==="pick"){return trainSet.has(k)?{text:"βœ“ training",color:"#16a34a"}:{text:"unknown",color:"#94a3b8"};}if(tab==="oracle"&&trainSet.has(k))return{text:"βœ“ train ex",color:"#16a34a"};return null;}
  function sClick(p){setSHL(p);setPg(Math.floor(p/PP));}
  function stHov(p){setSHL(p);if(p!==null)setPg(Math.floor(p/PP));}
  var stSel=(tab==="compete"?hPicks:sel).map(function(m){return m.pos;});
  var curSel=tab==="compete"?hPicks:sel;

  return(
    <div style={{fontFamily:"Georgia,serif",background:"#faf9f7",minHeight:"100vh",color:"#1a1a1a",overflow:"hidden",maxWidth:"100%"}}>
      <div style={{background:"linear-gradient(135deg,#0f172a,#1e3a5f)",padding:"12px 14px 10px",color:"white"}}>
        <div style={{fontSize:16,fontWeight:700}}>GFP Mutation Explorer</div>
        <div style={{fontSize:10,opacity:0.6,marginBottom:8}}>Gitter Lab - Morgridge Institute</div>
        <div style={{display:"flex",gap:2,background:"rgba(255,255,255,0.1)",borderRadius:6,padding:2}}>
          {[["learn","πŸ“– Learn"],["compete","🧠 vs GFP-Design"],["oracle","πŸ”¬ Oracle"]].map(function(it){
            return <button key={it[0]} onClick={function(){setTab(it[0]);setPg(0);setSel([]);setPred(false);if(it[0]!=="compete")resetC();}} style={{padding:"5px 10px",borderRadius:5,border:"none",cursor:"pointer",fontSize:10,fontWeight:600,fontFamily:"inherit",background:tab===it[0]?"white":"transparent",color:tab===it[0]?"#0f172a":"rgba(255,255,255,0.8)",flex:1}}>{it[1]}</button>;})}
        </div>
      </div>

      <div style={{padding:"10px 12px 24px",overflow:"hidden"}}>
        <Tip h={hov} scores={oScores} extra={getEx()}/>

        {tab==="learn"&&(<div>
          <p style={{fontSize:12,color:"#64748b",margin:"0 0 8px"}}>A <b>mutation</b> swaps one amino acid for another. Hover the sequence, click the grid to pick mutations.</p>
          <SeqStrip hl={sHL} onClick={sClick}/>
          <StructView hl={sHL} onH={stHov} selP={stSel}/>
          <div style={{marginTop:8,fontWeight:700,fontSize:13,marginBottom:4}}>Mutation Grid</div>
          <PageNav pg={pg} set={setPg} tot={totP}/><Legend/>
          <Grid ps={ps} pe={pe} scores={oScores} hover={hov} setHover={setHov} sel={sel} toggle={toggle} showAll={true}/>
          {sel.length>0&&(<div style={{marginTop:8,background:"#f8fafc",borderRadius:8,padding:10}}>
            <div style={{fontWeight:700,fontSize:12,marginBottom:4}}>{"Selected ("+sel.length+")"}</div>
            {sel.map(function(m,i){return <div key={i} style={{display:"flex",justifyContent:"space-between",fontSize:11,padding:"2px 0"}}><span style={{fontFamily:"monospace",fontWeight:700}}>{SEQ[m.pos]+m.pos+AAs[m.aaIdx]}</span><span style={{display:"flex",gap:4,alignItems:"center"}}>{pred&&<span style={{color:m.score>0?"#2563eb":"#dc2626",fontWeight:600}}>{(m.score>0?"+":"")+m.score.toFixed(2)}</span>}<button onClick={function(){setSel(function(prev){return prev.filter(function(_,j){return j!==i;});});setPred(false);}} style={{background:"none",border:"none",cursor:"pointer",color:"#999",fontSize:12}}>{"Γ—"}</button></span></div>;})}
            {!pred&&<button onClick={doPredict} disabled={pding} style={{width:"100%",marginTop:6,padding:8,borderRadius:6,border:"none",background:pding?"#94a3b8":"#2563eb",color:"white",fontWeight:700,fontSize:12,cursor:"pointer",fontFamily:"inherit"}}>{pding?"Running...":"🧬 Predict"}</button>}
            {pred&&<div style={{marginTop:6,padding:6,background:"#f0fdf4",borderRadius:4,textAlign:"center",fontSize:11,fontWeight:700,color:"#16a34a"}}>{"Total: "+sel.reduce(function(s,m){return s+m.score;},0).toFixed(2)}</div>}
          </div>)}
        </div>)}

        {tab==="compete"&&(<div>
          <div style={{textAlign:"center",marginBottom:10}}>
            <div style={{fontSize:16,fontWeight:800}}>{"🧠 You vs πŸ€– GFP-Design"}</div>
            <div style={{fontSize:11,color:"#64748b"}}>Both see 64 examples. Oracle (70k) judges.</div>
          </div>
          <div style={{display:"flex",justifyContent:"center",gap:3,marginBottom:10}}>
            {[["learn","1. Study"],["pick","2. Pick"],["reveal","3. Result"]].map(function(it){return <div key={it[0]} style={{padding:"5px 12px",borderRadius:12,fontSize:10,fontWeight:600,background:cStep===it[0]?"#0f172a":"#f1f5f9",color:cStep===it[0]?"white":"#999"}}>{it[1]}</div>;})}
          </div>
          <SeqStrip hl={sHL} onClick={sClick}/>

          {cStep==="learn"&&(<div>
            <div style={{background:"white",borderRadius:8,border:"1px solid #e2e8f0",overflow:"hidden",marginBottom:10}}>
              <div style={{padding:"8px 10px",background:"#f8fafc",borderBottom:"1px solid #e2e8f0",fontWeight:700,fontSize:11}}>64 Training Examples</div>
              <div style={{maxHeight:280,overflowY:"auto"}}>
                <table style={{width:"100%",borderCollapse:"collapse",fontSize:10}}>
                  <thead><tr style={{position:"sticky",top:0,background:"#f8fafc"}}>{["Pos","WT","β†’","Mut","SS","Score"].map(function(h){return <th key={h} style={{padding:"4px 6px",textAlign:"left",borderBottom:"1px solid #e2e8f0",fontSize:9,color:"#64748b"}}>{h}</th>;})}</tr></thead>
                  <tbody>{trainData.slice(0,64).map(function(m,i){return(
                    <tr key={i} style={{background:i%2===0?"white":"#fafbfc"}} onMouseEnter={function(){setSHL(m.pos);}} onMouseLeave={function(){setSHL(null);}}>
                      <td style={{padding:"3px 6px",fontFamily:"monospace",fontWeight:700}}>{m.pos}</td>
                      <td style={{padding:"3px 6px",fontFamily:"monospace"}}>{SEQ[m.pos]}</td>
                      <td style={{padding:"3px 6px",color:"#ccc"}}>{"β†’"}</td>
                      <td style={{padding:"3px 6px",fontFamily:"monospace",fontWeight:700,color:"#2563eb"}}>{AAs[m.aaIdx]}</td>
                      <td style={{padding:"3px 6px",fontSize:9}}>{ssArr[m.pos]==="E"?"Ξ²":ssArr[m.pos]==="H"?"Ξ±":"β€”"}</td>
                      <td style={{padding:"3px 6px",fontWeight:700,fontFamily:"monospace",color:m.score>0?"#2563eb":m.score>-1?"#d97706":"#dc2626"}}>{(m.score>0?"+":"")+m.score.toFixed(2)}</td>
                    </tr>);})}</tbody>
                </table>
              </div>
            </div>
            <button onClick={function(){setCS("pick");}} style={{width:"100%",padding:10,borderRadius:6,border:"none",background:"#0f172a",color:"white",fontWeight:700,fontSize:12,cursor:"pointer",fontFamily:"inherit"}}>{"Ready β†’ Pick Mutations"}</button>
          </div>)}

          {cStep==="pick"&&(<div>
            <div style={{background:"#fefce8",border:"1px solid #fde68a",borderRadius:6,padding:"6px 10px",marginBottom:8,fontSize:11}}><b>Pick 5.</b>{" Colored=training. Gray=unknown. "}<b>{hPicks.length+"/5"}</b></div>
            <PageNav pg={pg} set={setPg} tot={totP}/><Legend/>
            <div style={{display:"flex",gap:12,alignItems:"flex-start",flexWrap:"wrap"}}>
              <div style={{flex:"1 1 auto"}}><Grid ps={ps} pe={pe} scores={oScores} hover={hov} setHover={setHov} sel={hPicks} toggle={toggle} showAll={false}/></div>
              <StructView hl={sHL} onH={stHov} selP={hPicks.map(function(m){return m.pos})}/>
            </div>
            {hPicks.length>0&&<div style={{marginTop:6,display:"flex",gap:4,flexWrap:"wrap"}}>{hPicks.map(function(m,i){return <span key={i} style={{padding:"3px 8px",background:"#fef3c7",borderRadius:4,fontFamily:"monospace",fontWeight:700,fontSize:10}}>{SEQ[m.pos]+m.pos+AAs[m.aaIdx]}<button onClick={function(){setHP(function(prev){return prev.filter(function(_,j){return j!==i;});});}} style={{background:"none",border:"none",cursor:"pointer",color:"#999",fontSize:10,marginLeft:2}}>{"Γ—"}</button></span>;})}</div>}
            {hPicks.length===5&&<button onClick={function(){setCS("reveal");setAiRev(true);setRevI(0);}} style={{width:"100%",marginTop:10,padding:10,borderRadius:6,border:"none",background:"#dc2626",color:"white",fontWeight:700,fontSize:13,cursor:"pointer",fontFamily:"inherit"}}>{"πŸ”“ Lock In & Reveal"}</button>}
          </div>)}

          {cStep==="reveal"&&(<div>
            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10,marginBottom:12}}>
              <div style={{background:"white",borderRadius:8,border:"2px solid #f59e0b",overflow:"hidden"}}>
                <div style={{background:"#fefce8",padding:"6px 10px",fontWeight:700,fontSize:12}}>{"🧠 You"}</div>
                <div style={{padding:8}}>{hPicks.map(function(m,i){return <div key={i} style={{display:"flex",justifyContent:"space-between",padding:"3px 0",fontSize:11,fontFamily:"monospace"}}><b>{SEQ[m.pos]+m.pos+AAs[m.aaIdx]}</b><span style={{color:m.score>0?"#2563eb":"#dc2626",fontWeight:700}}>{(m.score>0?"+":"")+m.score.toFixed(2)}</span></div>;})}<div style={{marginTop:6,padding:6,background:"#fefce8",borderRadius:4,fontWeight:800,fontSize:14,textAlign:"center"}}>{"= "+hScore.toFixed(2)}</div></div>
              </div>
              <div style={{background:"white",borderRadius:8,border:"2px solid #2563eb",overflow:"hidden"}}>
                <div style={{background:"#eff6ff",padding:"6px 10px",fontWeight:700,fontSize:12}}>{"πŸ€– AI"}</div>
                <div style={{padding:8}}>{aiPicks.map(function(m,i){var rv=i<revI;return <div key={i} style={{display:"flex",justifyContent:"space-between",padding:"3px 0",fontSize:11,fontFamily:"monospace",opacity:rv?1:0.3,transition:"opacity 0.5s"}}><b>{rv?SEQ[m.pos]+m.pos+AAs[m.aaIdx]:"???"}</b><span style={{color:"#2563eb",fontWeight:700}}>{rv?(m.score>0?"+":"")+m.score.toFixed(2):"??"}</span></div>;})}<div style={{marginTop:6,padding:6,background:"#eff6ff",borderRadius:4,fontWeight:800,fontSize:14,textAlign:"center"}}>{"= "+(revI>=5?aiSc.toFixed(2):aiPicks.slice(0,Math.max(0,revI)).reduce(function(s,m){return s+m.score;},0).toFixed(2))}</div></div>
              </div>
            </div>
            {revI>=5&&(<div style={{textAlign:"center",padding:14,background:aiSc>hScore?"#eff6ff":"#fefce8",borderRadius:10,border:"2px solid "+(aiSc>hScore?"#2563eb":"#f59e0b")}}>
              <div style={{fontSize:24}}>{aiSc>hScore?"πŸ€–":"πŸŽ‰"}</div>
              <div style={{fontSize:16,fontWeight:800}}>{aiSc>hScore?"GFP-Design wins!":"You win!"}</div>
              <div style={{fontSize:11,color:"#64748b",margin:"4px 0"}}>{aiSc>hScore?"AI: "+(aiSc-hScore).toFixed(2)+" pts ahead. ML finds patterns from 64 examples.":"Your scientific intuition beat the model!"}</div>
              <button onClick={resetC} style={{marginTop:8,padding:"6px 20px",borderRadius:6,border:"none",background:"#0f172a",color:"white",fontWeight:700,fontSize:11,cursor:"pointer",fontFamily:"inherit"}}>Try Again</button>
            </div>)}
          </div>)}
        </div>)}

        {tab==="oracle"&&(<div>
          <div style={{fontWeight:800,fontSize:14,marginBottom:2}}>Oracle Landscape</div>
          <p style={{fontSize:11,color:"#64748b",margin:"0 0 8px"}}>{"● = training example (64 / ~4500)"}</p>
          <SeqStrip hl={sHL} onClick={sClick}/>
          <StructView hl={sHL} onH={stHov} selP={stSel}/>
          <PageNav pg={pg} set={setPg} tot={totP}/><Legend/>
          <Grid ps={ps} pe={pe} scores={oScores} hover={hov} setHover={setHov} sel={sel} toggle={toggle} showAll={true} showDots={true}/>
        </div>)}
      </div>
    </div>);
}

ReactDOM.render(React.createElement(App), document.getElementById("root"));

</script>
</body>
</html>