Files
APAW/agent-evolution/real-fit.html
Deploy Bot 897b95072b fix(dashboard): deduplicate modal model list via set + normalize API names
- load(): normalize ollama-cloud/* names to short form, deduplicate with Set
- Prevents double entries when cache adds short names alongside API full names
2026-05-28 12:50:49 +01:00

474 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Fit Matrix — Agent × Model Performance</title>
<style>
:root{--bg:#0a0f1a;--bg2:#0f1525;--bg3:#141c2e;--bdr:#1e2d45;--txt:#e8f1ff;--txt2:#8ba3c0;--cyan:#00d4ff;--green:#00ff94;--red:#ff4757;--orange:#ff9f43;--purple:#a855f7;}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--txt);min-height:100vh;padding:24px}
h1{font-size:1.6rem;background:linear-gradient(90deg,var(--cyan),var(--green));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
.sub{color:var(--txt2);font-size:.85rem;margin-bottom:20px}
table{width:100%;border-collapse:collapse;font-size:.82rem}
th,td{padding:8px 10px;border:1px solid var(--bdr);text-align:center}
th{background:var(--bg2);color:var(--txt2);font-size:.72rem;text-transform:uppercase;letter-spacing:.5px;position:sticky;top:0}
td:first-child{text-align:left;font-weight:700;white-space:nowrap}
td.score{font-weight:700;font-family:monospace}
.hm-cur{box-shadow:inset 0 0 0 2px var(--cyan)}
.high{background:rgba(0,255,148,.18);color:var(--green)}
.good{background:rgba(0,212,255,.14);color:var(--cyan)}
.med{background:rgba(168,85,247,.15);color:var(--purple)}
.low{background:rgba(255,71,87,.1);color:var(--red)}
.na{background:transparent;color:var(--txt2);font-size:.9rem;cursor:pointer}
.na:hover{background:rgba(0,212,255,.08)}
.legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;font-size:.78rem;color:var(--txt2)}
.legend span{display:flex;align-items:center;gap:4px}
.dot{width:14px;height:14px;border-radius:3px}
.meta{font-size:.72rem;color:var(--txt2);margin-top:12px}
a{color:var(--cyan);text-decoration:none}
.btn-research{font-size:.9rem;background:none;border:none;cursor:pointer;margin-left:4px;opacity:.7}
.btn-research:hover{opacity:1}
.modal{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:1000;padding:16px}
.modal.hidden{display:none}
.modal-panel{background:var(--bg2);border:1px solid var(--cyan);border-radius:12px;max-width:90vw;width:480px;max-height:90vh;overflow-y:auto;padding:20px;position:relative}
.modal-panel.wide{width:640px}
.modal-title{font-size:1.1rem;margin-bottom:12px}
.modal-list{text-align:left;margin:12px 0;max-height:40vh;overflow-y:auto}
.modal-list label{display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;border-bottom:1px solid var(--bdr)}
.modal-list input{margin:0}
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
.btn{padding:6px 14px;border-radius:6px;border:1px solid var(--bdr);background:var(--bg3);color:var(--txt);cursor:pointer;font-size:.85rem}
.btn.primary{background:linear-gradient(90deg,var(--green),var(--cyan));color:#000;border:none;font-weight:700}
.progress{margin-top:12px}
.progress-bar{height:8px;background:var(--bg3);border-radius:4px;overflow:hidden}
.progress-fill{height:100%;width:0%;background:linear-gradient(90deg,var(--green),var(--cyan));transition:width .3s}
.progress-text{font-size:.8rem;color:var(--txt2);margin-top:6px;text-align:center}
.result-table{width:100%;margin-top:12px;font-size:.82rem;border-collapse:collapse}
.result-table th,.result-table td{padding:6px;border:1px solid var(--bdr)}
.result-table .best{background:rgba(0,255,148,.25);color:var(--green);font-weight:700}
.result-table tbody tr{cursor:pointer}
.result-table tbody tr:hover{background:rgba(0,212,255,.06)}
.detail-row{margin-bottom:12px}
.detail-label{font-size:.72rem;color:var(--txt2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.detail-val{font-size:.85rem;white-space:pre-wrap;word-break:break-word}
.detail-pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
.pill{font-size:.72rem;padding:2px 8px;border-radius:4px;background:var(--bg3);border:1px solid var(--bdr);color:var(--txt2)}
.score-big{font-size:2rem;font-weight:700;margin:4px 0}
.toggle{color:var(--cyan);cursor:pointer;font-size:.78rem}
.toggle:hover{text-decoration:underline}
.dim-bar{display:flex;align-items:center;gap:8px;margin:4px 0}
.dim-bar>span:first-child{width:120px;font-size:.75rem;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dim-track{flex:1;height:8px;background:var(--bg3);border-radius:4px;overflow:hidden}
.dim-fill{height:100%;border-radius:4px}
.dim-num{width:30px;text-align:right;font-size:.78rem;font-weight:700}
.v-pass{color:var(--green)}
.v-marginal{color:var(--orange)}
.v-fail{color:var(--red)}
.commentary{font-size:.85rem;padding:10px 12px;background:rgba(0,212,255,.08);border-left:3px solid var(--cyan);border-radius:0 6px 6px 0;color:var(--txt);white-space:pre-wrap;word-break:break-word}
</style>
</head>
<body>
<h1>Real-Fit Matrix</h1>
<div class="sub">Real agent × model evaluation scores via live Ollama API (28 calls, 4 models, 7 agents)</div>
<div id="matrix"></div>
<div class="legend">
<span><span class="dot high"></span> 90+ Excellent</span>
<span><span class="dot good"></span> 7589 Good</span>
<span><span class="dot med"></span> 5074 Average</span>
<span><span class="dot low"></span> &lt;50 Weak</span>
<span style="margin-left:auto">● = assigned model</span>
</div>
<div class="meta">Data source: <a href="data/real-fit-report.json" target="_blank">real-fit-report.json</a> | Updated: <span id="updated"></span></div>
<div id="researchAgentModal" class="modal hidden">
<div class="modal-panel">
<div class="modal-title" id="agentModalTitle">Research models</div>
<div class="modal-list" id="agentModalList"></div>
<div class="progress hidden" id="agentProgress">
<div class="progress-bar"><div class="progress-fill" id="agentProgressFill"></div></div>
<div class="progress-text" id="agentProgressText"></div>
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal('researchAgentModal')">Close</button>
<button class="btn" id="evolveAgentBtn" onclick="startEvolveAgent()">Run Role-Fit Test</button>
<button class="btn primary" id="agentStartBtn" onclick="startAgentResearch()">Start Research</button>
</div>
<div id="agentResults"></div>
</div>
</div>
<div id="researchCellModal" class="modal hidden">
<div class="modal-panel">
<div class="modal-title" id="cellModalTitle">Evaluate cell</div>
<div class="modal-list" id="cellModalList"></div>
<div class="progress hidden" id="cellProgress">
<div class="progress-bar"><div class="progress-fill" id="cellProgressFill"></div></div>
<div class="progress-text" id="cellProgressText"></div>
</div>
<div class="modal-actions">
<button class="btn" onclick="closeModal('researchCellModal')">Close</button>
<button class="btn primary" id="cellStartBtn" onclick="startCellResearch()">Evaluate</button>
</div>
<div id="cellResults"></div>
</div>
</div>
<div id="detailModal" class="modal hidden">
<div class="modal-panel wide">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="font-size:1rem;font-weight:700" id="detailTitle"></span>
<button class="btn" onclick="closeModal('detailModal')">Close</button>
</div>
<div id="detailContent"></div>
</div>
</div>
<script>
let reportData, evoData, allModels=[], allAvailableModels=[];
const API_BASE='http://localhost:3004';
const $=id=>document.getElementById(id);
const MODEL_BENCHMARKS={
"qwen3.5-122b":91,"qwen3-coder-480b":88,"deepseek-v4-pro":89,"deepseek-v4-pro-max":89,
"deepseek-v4-flash":86,"kimi-k2.6":91,"kimi-k2.5":90,
"minimax-m2.5":82,"minimax-m2.7":80,"glm-5.1":90,
"glm-5":90,"nemotron-3-super":78,"nemotron-3-nano":68,
"gemma4-27b":85,"devstral-2":80,"devstral-small-2":75
};
function clsFor(s){if(s>=90)return'score high';if(s>=75)return'score good';if(s>=50)return'score med';return'score low';}
function scoreColor(s){if(s>=90)return'var(--green)';if(s>=75)return'var(--cyan)';if(s>=50)return'var(--purple)';return'var(--red)';}
function closeModal(id){$(id).classList.add('hidden');}
async function load(){
let mRes;
try{ mRes=await fetch(`${API_BASE}/api/models`); allAvailableModels=[...new Set((await mRes.json()).models.map(modelShort))].sort(); }
catch(e){ allAvailableModels=Object.keys(MODEL_BENCHMARKS); }
try{
const rRes=await fetch(`${API_BASE}/api/real-fit-report`);
reportData=await rRes.json();
}catch(e){
const rRes=await fetch('data/real-fit-report.json');
reportData=await rRes.json();
}
const eRes=await fetch('data/evolution.json');
evoData=await eRes.json();
reportData.generated = reportData.generated || new Date().toISOString();
$('updated').textContent=new Date(reportData.generated).toLocaleString('ru-RU');
const agents=Object.values(reportData.agents).filter(a=>Object.values(a.evaluations).some(s=>s>0));
const modelScores={};
agents.forEach(a=>{for(const[m,s] of Object.entries(a.evaluations)){modelScores[m]=(modelScores[m]||0)+s;}});
allModels=Object.keys(modelScores).filter(m=>modelScores[m]>0).sort();
mergeCachedResults();
renderTable();
}
function currentModel(agentName){
const info=reportData.agents[agentName]?.info||[];
return (info[2]||'').split('/').pop();
}
function modelShort(full){return full.replace('ollama-cloud/','');}
function openAgentModal(agent){
$('agentModalTitle').textContent='Research models for '+agent;
const cur=currentModel(agent);
let html='';
allAvailableModels.forEach(full=>{
const m=modelShort(full);
const checked=m===cur||cur.replace(':','-')===m||m.replace('-',':')===cur?'checked':'';
const ifs=MODEL_BENCHMARKS[m]||'—';
html+=`<label><input type="checkbox" value="${m}" ${checked}> <span>${m}</span> <span style="color:var(--txt2);margin-left:auto">IF ${ifs}</span></label>`;
});
$('agentModalList').innerHTML=html||'<p style="color:var(--txt2)">No model data</p>';
$('agentResults').innerHTML='';
$('agentProgress').classList.add('hidden');
$('agentStartBtn').disabled=false;
$('evolveAgentBtn').disabled=false;
$('researchAgentModal').classList.remove('hidden');
window.__activeAgent=agent;
}
function openCellModal(agent,model){
$('cellModalTitle').textContent='Evaluate '+agent+' × '+model;
$('cellModalList').innerHTML=`<label><input type="checkbox" value="${model}" checked> <span>${model}</span></label>`;
$('cellResults').innerHTML='';
$('cellProgress').classList.add('hidden');
$('cellStartBtn').disabled=false;
$('researchCellModal').classList.remove('hidden');
window.__activeAgent=agent; window.__activeModel=model;
}
async function openDetail(agent,model){
$('detailTitle').textContent=agent+' × '+model;
$('detailContent').innerHTML='<p style="color:var(--txt2)">Loading...</p>';
$('detailModal').classList.remove('hidden');
let data;
try{
const res=await fetch(`${API_BASE}/api/evaluation/${encodeURIComponent(agent)}/${encodeURIComponent(model)}`);
if(!res.ok) throw new Error(res.status);
data=await res.json();
}catch(e){
$('detailContent').innerHTML='<p style="color:var(--red);margin-top:12px">No detailed evaluation data available for this combination. Run research first.</p>';
return;
}
const s=data.total_score??data.score??0;
const verdict=(data.verdict||'').toUpperCase();
let vClass='';
if(verdict==='PASS') vClass='v-pass';
else if(verdict==='MARGINAL') vClass='v-marginal';
else if(verdict==='FAIL') vClass='v-fail';
const verdictHtml=verdict?`<span class="${vClass}" style="font-size:.85rem;font-weight:700;border:1px solid currentColor;padding:2px 8px;border-radius:4px;margin-left:8px">${verdict}</span>`:'';
let scoresHtml='';
if(data.scores){
scoresHtml='<div class="detail-row"><div class="detail-label">Score Breakdown</div>';
for(const [k,v] of Object.entries(data.scores)){
const num=typeof v==='number'?v:Number(v)||0;
scoresHtml+=`<div class="dim-bar"><span>${k}</span><div class="dim-track"><div class="dim-fill" style="width:${num}%;background:${scoreColor(num)}"></div></div><span class="dim-num">${Math.round(num)}</span></div>`;
}
scoresHtml+='</div>';
}
let commentaryHtml='';
if(data.explanation){
commentaryHtml=`<div class="detail-row"><div class="detail-label">Evaluator Commentary</div><div class="commentary">${data.explanation}</div></div>`;
}
let rubricHtml='';
if(data.rubric){
rubricHtml='<div class="detail-row"><div class="detail-label">Rubric Weights</div><div class="detail-pills">';
for(const [k,v] of Object.entries(data.rubric)){
rubricHtml+=`<span class="pill">${k}: ${v}</span>`;
}
rubricHtml+='</div></div>';
}
let kwHtml='';
if(data.expected_keywords?.length){ kwHtml='<div class="detail-pills">'+data.expected_keywords.map(k=>`<span class="pill">${k}</span>`).join('')+'</div>'; }
const resp=(data.response||'').toString();
const trunc=resp.length>500?resp.slice(0,500)+'...':resp;
const more=resp.length>500;
const rid='r'+Math.random().toString(36).slice(2);
window.__respCache=window.__respCache||{};
window.__respCache[rid]={full:resp,trunc:trunc};
let respHtml=`<div class="detail-val" id="${rid}">${trunc}</div>`;
if(more) respHtml+=`<span class="toggle" onclick="const c=window.__respCache['${rid}'];const el=$('${rid}');const isFull=el.dataset.f==='1';el.textContent=isFull?c.trunc:c.full;el.dataset.f=isFull?'0':'1';this.textContent=isFull?'Show more':'Show less'">Show more</span>`;
const lat=data.latency_ms;
const latTxt=typeof lat==='number'?(lat>=1000?(lat/1000).toFixed(1)+'s':lat+'ms'):'—';
$('detailContent').innerHTML=`
<div class="detail-row"><div class="detail-label">Agent × Model</div><div class="detail-val">${agent} × ${model}${verdictHtml}</div></div>
<div class="detail-row"><div class="detail-label">Total Score</div><div class="score-big" style="color:${scoreColor(s)}">${Math.round(s)}</div></div>
${scoresHtml}
<div class="detail-row"><div class="detail-label">Task</div><div class="detail-val">${data.user_prompt||'—'}</div></div>
<div class="detail-row"><div class="detail-label">System Role</div><div class="detail-val">${data.system_prompt||'—'}</div></div>
<div class="detail-row"><div class="detail-label">Model Response</div>${respHtml}</div>
${commentaryHtml}
${rubricHtml}
<div class="detail-row"><div class="detail-label">Evaluator</div><div class="detail-val">${data.evaluator||'—'}</div></div>
<div class="detail-row"><div class="detail-label">Latency</div><div class="detail-val">${latTxt}</div></div>
<div class="detail-row"><div class="detail-label">Tokens</div><div class="detail-val">Prompt: ${data.tokens_prompt??0} / Response: ${data.tokens_response??0}</div></div>
<div class="detail-row"><div class="detail-label">Expected Keywords</div>${kwHtml||'<div class="detail-val">—</div>'}</div>
<div class="detail-row"><div class="detail-label">Evaluated At</div><div class="detail-val">${data.evaluated_at?new Date(data.evaluated_at).toLocaleString('ru-RU'):'—'}</div></div>
`;
}
async function animateProgress(pid,label,ms){
const bar=$(pid+'Fill'),txt=$(pid+'Text'),wrap=$(pid);
wrap.classList.remove('hidden'); txt.textContent=label; bar.style.width='0%';
await new Promise(r=>setTimeout(r,50)); bar.style.transition=`width ${ms}ms linear`;
await new Promise(r=>setTimeout(r,50)); bar.style.width='100%';
await new Promise(r=>setTimeout(r,ms));
bar.style.transition='width .3s';
}
function setProgress(pid,percent,label){
const bar=$(pid+'Fill'),txt=$(pid+'Text'),wrap=$(pid);
wrap.classList.remove('hidden'); txt.textContent=label; bar.style.width=percent+'%';
}
function mergeCachedResults(){
try{
const store=JSON.parse(localStorage.getItem('__researchResults')||'{}');
for(const[agent,rec] of Object.entries(store)){
if(!reportData.agents[agent]){
reportData.agents[agent]={name:agent,evaluations:{},info:[],best_model:'',best_score:0};
}
for(const r of (rec.models||[])){
const shortM=modelShort(r.model);
// Only fill gaps — never override existing DB scores
const existing=reportData.agents[agent].evaluations[shortM];
if(existing===undefined||existing===0){
reportData.agents[agent].evaluations[shortM]=r.score;
}
if(r.score>reportData.agents[agent].best_score){
reportData.agents[agent].best_model=shortM;
reportData.agents[agent].best_score=r.score;
}
if(!allModels.includes(shortM)) allModels.push(shortM);
if(!allAvailableModels.includes(shortM)) allAvailableModels.push(shortM);
}
}
allModels.sort();
}catch(e){}
}
function renderTable(){
const agents=Object.values(reportData.agents).filter(a=>Object.values(a.evaluations).some(s=>s>0));
let html='<table><thead><tr><th>Agent</th>';
allModels.forEach(m=>html+=`<th>${m}</th>`);
html+='<th>Best</th><th>Score</th></tr></thead><tbody>';
agents.forEach(a=>{
html+=`<tr><td>${a.name} <button class="btn-research" onclick="openAgentModal('${a.name}')" title="Research models">🔬</button></td>`;
allModels.forEach(m=>{
const score=a.evaluations[m];
const isCur=a.info&&a.info[2]&&a.info[2].includes(m);
let cls='na',text='—',click=`onclick="openCellModal('${a.name}','${m}')"`;
if(score!==undefined&&score>0){cls=clsFor(score);text=Math.round(score);click=`onclick="openDetail('${a.name}','${m}')"`;}
const curCls=isCur?' hm-cur':'';
html+=`<td class="${cls}${curCls}" data-model="${m}" ${click}>${text}${isCur?' ●':''}</td>`;
});
html+=`<td>${a.best_model}</td><td style="font-weight:700">${Math.round(a.best_score)}</td></tr>`;
});
html+='</tbody></table>';
$('matrix').innerHTML=html;
}
function updateCell(agent,model,score){
const short=modelShort(model);
if(!reportData.agents[agent]){
reportData.agents[agent]={name:agent,evaluations:{},info:[],best_model:model,best_score:0};
}
reportData.agents[agent].evaluations[short]=score;
reportData.agents[agent].best_model=short;
reportData.agents[agent].best_score=score;
if(!allModels.includes(short)){
allModels.push(short);
allModels.sort();
}
if(!allAvailableModels.includes(short)) allAvailableModels.push(short);
renderTable();
}
async function pollJob(jobId,pid){
for(let i=0;i<60;i++){
await new Promise(r=>setTimeout(r,2000));
try{
const res=await fetch(`${API_BASE}/api/research/${jobId}`);
if(!res.ok) continue;
const job=await res.json();
if(job.status==='pending') setProgress(pid,25,'Waiting in queue...');
else if(job.status==='running') setProgress(pid,75,'Running evaluation...');
else if(job.status==='done'){ setProgress(pid,100,'Done!'); return job; }
else if(job.status==='error'){ setProgress(pid,100,'Error!'); return job; }
}catch(e){ console.warn('poll error',e); }
}
setProgress(pid,100,'Timeout'); return {status:'timeout'};
}
async function startAgentResearch(){
const agent=window.__activeAgent;
const models=[...$('agentModalList').querySelectorAll('input:checked')].map(i=>i.value);
if(!models.length)return;
$('agentStartBtn').disabled=true;
$('evolveAgentBtn').disabled=true;
setProgress('agentProgress',10,'Submitting job...');
let job;
try{
const res=await fetch(`${API_BASE}/api/research`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent,models})});
if(!res.ok) throw new Error('API error '+res.status);
job=await res.json();
job=await pollJob(job.job_id,'agentProgress');
}catch(e){
$('agentResults').innerHTML='<p style="color:var(--red);margin-top:12px">API unavailable — run real-fit-engine.py to evaluate '+agent+'</p>';
$('agentProgressText').textContent='Error: API unavailable';
$('agentStartBtn').disabled=false;
$('evolveAgentBtn').disabled=false;
return;
}
const results=job.models_scored||[];
let html='<table class="result-table"><thead><tr><th>Model</th><th>Score</th></tr></thead><tbody>';
let best=-1;
results.forEach(r=>{if(r.score>best)best=r.score;});
results.forEach(r=>{
const b=r.score>=best-0.1?'best':'';
html+=`<tr class="${b}" onclick="openDetail('${agent}','${r.model}')"><td>${r.model}</td><td>${Math.round(r.score)}</td></tr>`;
updateCell(agent,r.model,r.score);
});
html+='</tbody></table>';
$('agentResults').innerHTML=html;
$('agentProgressText').textContent='Done! Best score: '+Math.round(best);
const store=JSON.parse(localStorage.getItem('__researchResults')||'{}');
store[agent]={models:results,ts:Date.now()};
localStorage.setItem('__researchResults',JSON.stringify(store));
$('agentStartBtn').disabled=false;
$('evolveAgentBtn').disabled=false;
}
async function startEvolveAgent(){
const agent=window.__activeAgent;
const models=[...$('agentModalList').querySelectorAll('input:checked')].map(i=>i.value);
if(!models.length) return;
$('evolveAgentBtn').disabled=true;
$('agentStartBtn').disabled=true;
setProgress('agentProgress',10,'Submitting evolve-agent job...');
try{
const res=await fetch(`${API_BASE}/api/evolve-agent/start`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent,models})});
if(!res.ok) throw new Error('API error '+res.status);
setProgress('agentProgress',50,'Running role-fit test...');
const result=await res.json();
if(result.job_id){
await pollJob(result.job_id,'agentProgress');
}else{
await animateProgress('agentProgress','Processing...',2000);
}
setProgress('agentProgress',100,'Done!');
}catch(e){
console.error('evolve-agent error',e);
setProgress('agentProgress',100,'Error: '+e.message);
$('evolveAgentBtn').disabled=false;
$('agentStartBtn').disabled=false;
return;
}
await load();
closeModal('researchAgentModal');
$('evolveAgentBtn').disabled=false;
$('agentStartBtn').disabled=false;
}
async function startCellResearch(){
const agent=window.__activeAgent, model=window.__activeModel;
$('cellStartBtn').disabled=true;
setProgress('cellProgress',10,'Submitting...');
let job;
try{
const res=await fetch(`${API_BASE}/api/research/cell`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({agent,model})});
if(!res.ok) throw new Error('API error '+res.status);
job=await res.json();
job=await pollJob(job.job_id,'cellProgress');
}catch(e){
$('cellResults').innerHTML='<p style="color:var(--red);margin-top:12px">API unavailable — run real-fit-engine.py to evaluate '+agent+'</p>';
$('cellProgressText').textContent='Error: API unavailable';
$('cellStartBtn').disabled=false;
return;
}
const result=(job.models_scored||[])[0]||{model,score:0};
updateCell(agent,result.model,result.score);
$('cellResults').innerHTML='<table class="result-table"><tbody><tr onclick="openDetail(\''+agent+'\',\''+result.model+'\')"><td>'+result.model+'</td><td>'+Math.round(result.score)+'</td></tr></tbody></table>';
$('cellProgressText').textContent='Done! Score: '+Math.round(result.score);
const store=JSON.parse(localStorage.getItem('__researchResults')||'{}');
if(!store[agent]) store[agent]={models:[],ts:Date.now()};
store[agent].models=store[agent].models.filter(m=>m.model!==result.model);
store[agent].models.push(result);
localStorage.setItem('__researchResults',JSON.stringify(store));
}
load().catch(e=>$('matrix').innerHTML='Error: '+e);
</script>
</body>
</html>