0xarchit's picture
feat(inference): fix ui and shap
199cee1
const state = {
activeId: null,
fields: {},
specs: {}
};
const elements = {
tabs: document.querySelectorAll('.tab-btn'),
fieldsContainer: document.getElementById('fields-container'),
inferenceForm: document.getElementById('inference-form'),
resultDisplay: document.getElementById('result-display'),
infoTrigger: document.querySelector('.info-trigger'),
modal: document.getElementById('modal-overlay'),
modalClose: document.getElementById('modal-close'),
specsContent: document.getElementById('specs-content'),
modalTitle: document.getElementById('modal-title'),
btnAutofill: document.getElementById('btn-autofill')
};
async function autofill() {
elements.btnAutofill.disabled = true;
const btnText = elements.btnAutofill.querySelector('.btn-text');
const originalText = btnText.innerText;
btnText.innerText = 'Extracting...';
try {
const res = await fetch(`/api/sample/${state.activeId}`);
const data = await res.json();
Object.entries(data).forEach(([key, val]) => {
const input = elements.inferenceForm.querySelector(`[name="${key}"]`);
if (input) {
input.value = val;
input.style.borderColor = 'var(--secondary)';
input.style.boxShadow = '0 0 15px rgba(0, 210, 255, 0.3)';
setTimeout(() => {
input.style.borderColor = '';
input.style.boxShadow = '';
}, 1500);
}
});
} catch (e) {
console.error(e);
} finally {
elements.btnAutofill.disabled = false;
btnText.innerText = originalText;
}
}
elements.btnAutofill.onclick = autofill;
async function loadTab(id) {
if (state.activeId === id) return;
state.activeId = id;
elements.tabs.forEach(btn => btn.classList.toggle('active', btn.dataset.id === id));
elements.fieldsContainer.style.opacity = '0';
elements.fieldsContainer.style.transform = 'translateY(10px)';
try {
const [fields, info] = await Promise.all([
fetch(`/api/fields/${id}`).then(r => r.json()),
fetch(`/api/info/${id}`).then(r => r.json())
]);
state.fields[id] = fields;
state.specs[id] = info;
renderFields(fields);
elements.resultDisplay.innerHTML = `
<div class="empty-state">
<div class="neural-loader">
<div class="node"></div><div class="node"></div><div class="node"></div>
</div>
<p>System Initialized. Awaiting Input Data.</p>
</div>
`;
} catch (e) {
elements.fieldsContainer.innerHTML = `<p class="error">System Error: ${e.message}</p>`;
} finally {
setTimeout(() => {
elements.fieldsContainer.style.opacity = '1';
elements.fieldsContainer.style.transform = 'translateY(0)';
elements.fieldsContainer.style.transition = 'all 0.5s cubic-bezier(0.2, 0.8, 0.2, 1)';
}, 50);
}
}
function renderFields(fields) {
elements.fieldsContainer.innerHTML = fields.map((f, i) => `
<div class="field-group" style="animation: slide-reveal 0.5s ease forwards; animation-delay: ${i * 0.03}s; opacity: 0;">
<label>${f.name}</label>
${f.type === 'select' ? `
<select name="${f.name}">
<option value="" disabled selected>Select option...</option>
${f.options.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>
` : `
<input type="number" name="${f.name}" placeholder="Value..." step="any">
`}
</div>
`).join('');
}
elements.inferenceForm.onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(elements.inferenceForm);
const payload = { features: {} };
let hasEmpty = false;
formData.forEach((v, k) => {
if (v === "") hasEmpty = true;
payload.features[k] = v;
});
if (hasEmpty) {
alert("Please complete all parameters before execution.");
return;
}
const btn = document.querySelector('.btn-primary');
const btnText = btn.querySelector('.btn-text');
const originalText = btnText.innerText;
btn.disabled = true;
btnText.innerText = 'Calculating...';
elements.resultDisplay.scrollIntoView({ behavior: 'smooth', block: 'center' });
try {
const res = await fetch(`/api/run/${state.activeId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Inference engine failure');
renderResult(data);
} catch (e) {
elements.resultDisplay.innerHTML = `
<div class="empty-state">
<p style="color: var(--primary)">Execution Error: ${e.message}</p>
</div>
`;
} finally {
btn.disabled = false;
btnText.innerText = originalText;
}
};
function renderResult(data) {
const scores = Object.entries(data.scores).sort((a, b) => b[1] - a[1]);
const shap = Object.entries(data.shap || {});
const maxShap = Math.max(...shap.map(s => Math.abs(s[1])), 0.001);
elements.resultDisplay.innerHTML = `
<div class="result-view">
<div class="prediction-box">
<span style="font-size: 0.65rem; color: var(--text-dim); letter-spacing: 4px; font-weight: 700; text-transform: uppercase;">Prediction Output</span>
<div class="label-val">${data.result}</div>
</div>
<div class="score-chart">
<span style="font-size: 0.65rem; color: var(--text-dim); letter-spacing: 4px; font-weight: 700; text-transform: uppercase; margin-bottom: 10px;">Probability Vectors</span>
${scores.map(([label, score]) => `
<div class="score-row">
<span class="score-label" title="${label}">${label}</span>
<div class="bar-container">
<div class="bar-fill" style="width: 0%"></div>
</div>
<span class="score-label" style="text-align: right; width: 50px; font-weight: 700; color: var(--text)">${(score * 100).toFixed(1)}%</span>
</div>
`).join('')}
</div>
<div class="shap-chart">
<span style="font-size: 0.65rem; color: var(--text-dim); letter-spacing: 4px; font-weight: 700; text-transform: uppercase; margin-bottom: 10px;">SHAP Decision Drivers</span>
${shap.map(([feat, val]) => `
<div class="shap-row">
<span class="score-label" style="font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis;" title="${feat}">${feat}</span>
<div class="shap-viz">
<div class="shap-axis"></div>
<div class="shap-bar ${val >= 0 ? 'positive' : 'negative'}" style="width: 0%"></div>
</div>
<span class="score-label" style="text-align: right; font-weight: 700; color: ${val >= 0 ? '#10b981' : '#ef4444'}">${val >= 0 ? '+' : ''}${val.toFixed(2)}</span>
</div>
`).join('')}
</div>
</div>
`;
setTimeout(() => {
const fills = elements.resultDisplay.querySelectorAll('.bar-fill');
scores.forEach((s, i) => fills[i].style.width = `${s[1] * 100}%`);
const shapBars = elements.resultDisplay.querySelectorAll('.shap-bar');
shap.forEach((s, i) => {
const width = (Math.abs(s[1]) / maxShap) * 50; // Max 50% from center
shapBars[i].style.width = `${width}%`;
});
}, 100);
}
elements.infoTrigger.onclick = () => {
const info = state.specs[state.activeId];
if (!info) return;
elements.modalTitle.innerText = `${info.title} Architecture`;
elements.specsContent.innerHTML = `
<div class="spec-grid">
<div class="spec-item"><h4>Volume</h4><p>${info.dataset.rows.toLocaleString()}</p></div>
<div class="spec-item"><h4>Dimensions</h4><p>${info.dataset.cols}</p></div>
<div class="spec-item"><h4>Model Type</h4><p>${info.model.type || 'Ensemble'}</p></div>
<div class="spec-item"><h4>Target</h4><p>${info.dataset.target}</p></div>
</div>
<div class="metrics-table-wrapper" style="margin-bottom: 30px;">
<table style="width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 0.8rem;">
<thead style="text-align: left; color: var(--text-dim); border-bottom: 1px solid var(--border);">
<tr>${info.metrics.length ? Object.keys(info.metrics[0]).map(k => `<th style="padding: 10px;">${k}</th>`).join('') : ''}</tr>
</thead>
<tbody>
${info.metrics.map(m => `
<tr style="border-bottom: 1px solid var(--border);">
${Object.values(m).map(v => `<td style="padding: 10px; color: var(--text);">${typeof v === 'number' ? v.toFixed(4) : v}</td>`).join('')}
</tr>
`).join('')}
</tbody>
</table>
</div>
<a href="${info.url}" target="_blank" class="source-link">
<i class="fa-solid fa-arrow-up-right-from-square"></i> Explore Kaggle Dataset
</a>
`;
elements.modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
};
elements.modalClose.onclick = () => {
elements.modal.classList.add('hidden');
document.body.style.overflow = 'auto';
};
window.onclick = (e) => {
if (e.target === elements.modal) {
elements.modal.classList.add('hidden');
document.body.style.overflow = 'auto';
}
};
elements.tabs.forEach(btn => btn.onclick = () => {
loadTab(btn.dataset.id);
btn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
});
const initialId = elements.tabs[0]?.dataset.id;
if (initialId) loadTab(initialId);