| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Human Essence · Inference</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| *{margin:0;padding:0;box-sizing:border-box} |
| :root{--bg-primary:#08080d;--bg-secondary:#0e0e16;--bg-tertiary:#16161f;--bg-card:#111119;--text-primary:#e0e0e8;--text-secondary:#6b6b80;--text-muted:#44445a;--accent:#6366f1;--border:#1e1e2e;--border-subtle:#181828} |
| body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg-primary);color:var(--text-primary);min-height:100vh} |
| .container{max-width:1200px;margin:0 auto;padding:16px;display:flex;flex-direction:column;gap:12px} |
| .header{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;background:var(--bg-secondary);border-radius:12px;border:1px solid var(--border);flex-shrink:0;flex-wrap:wrap;gap:10px} |
| .header-left{display:flex;align-items:center;gap:10px} |
| .header-icon{width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#6366f1,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:.9rem} |
| .header h1{font-size:1.2rem;font-weight:600;background:linear-gradient(135deg,#c7c8ff,#8b5cf6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.02em} |
| .header-controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap} |
| .btn{padding:7px 14px;border:none;border-radius:7px;cursor:pointer;font-size:.8rem;font-weight:500;transition:all .2s ease;font-family:inherit;display:flex;align-items:center;gap:5px;white-space:nowrap} |
| .btn-primary{background:linear-gradient(135deg,#6366f1,#7c3aed);color:#fff} |
| .btn-primary:hover{background:linear-gradient(135deg,#5558e3,#6d28d9);transform:translateY(-1px)} |
| .btn-secondary{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border)} |
| .btn-secondary:hover{background:#1e1e2e} |
| .btn-success{background:linear-gradient(135deg,#059669,#10b981);color:#fff} |
| .btn-success:hover{background:linear-gradient(135deg,#047857,#059669);transform:translateY(-1px)} |
| .btn-danger{background:rgba(127,29,29,.5);color:#fca5a5;border:1px solid rgba(153,27,27,.5)} |
| .btn-danger:hover{background:rgba(153,27,27,.6)} |
| .editor-section{flex:1;display:flex;flex-direction:column;min-height:160px;gap:8px} |
| .editor-wrapper{flex:1;position:relative;border-radius:14px;border:1px solid var(--border);background:var(--bg-card);overflow:hidden;transition:border-color .4s ease,box-shadow .4s ease} |
| .editor-wrapper:focus-within{border-color:rgba(99,102,241,.25)} |
| .editor-stats{position:absolute;bottom:0;left:0;right:0;padding:6px 20px;display:flex;justify-content:space-between;align-items:center;font-size:.72rem;color:var(--text-muted);background:linear-gradient(to top,var(--bg-card) 70%,transparent);z-index:3;pointer-events:none;user-select:none} |
| #editorOverlay{position:absolute;top:0;left:0;right:0;bottom:0;padding:22px 22px 36px;font-size:1.05rem;line-height:1.75;font-family:inherit;color:var(--text-primary);white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;overflow-y:auto;pointer-events:none;user-select:none;z-index:1;letter-spacing:.01em} |
| #editorOverlay .word{padding:1px 2px;margin:0 -2px;border-radius:3px;transition:background-color .2s ease} |
| #editorOverlay .word.has-emotion{border-bottom:1.5px solid currentColor} |
| .placeholder-text{color:var(--text-muted)!important;font-style:italic;font-weight:300} |
| #editor{width:100%;height:100%;background:transparent;color:transparent;caret-color:#9194f8;border:none;padding:22px 22px 36px;font-size:1.05rem;line-height:1.75;font-family:inherit;resize:none;outline:none;position:absolute;top:0;left:0;z-index:2;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;letter-spacing:.01em} |
| #editor::selection{background:rgba(99,102,241,.3)} |
| #editor::-webkit-scrollbar,#editorOverlay::-webkit-scrollbar{width:5px} |
| #editor::-webkit-scrollbar-track,#editorOverlay::-webkit-scrollbar-track{background:transparent} |
| #editor::-webkit-scrollbar-thumb,#editorOverlay::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} |
| .loading-bar{display:none;align-items:center;gap:8px;padding:8px 12px;background:var(--bg-tertiary);border-radius:7px;font-size:.8rem;color:var(--text-secondary);border:1px solid var(--border-subtle)} |
| .loading-bar.active{display:flex} |
| .spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite} |
| @keyframes spin{to{transform:rotate(360deg)}} |
| .timeline-section{background:var(--bg-secondary);border-radius:12px;border:1px solid var(--border);padding:14px 18px;flex-shrink:0;display:none} |
| .timeline-section.active{display:block} |
| .timeline-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-wrap:wrap;gap:10px} |
| .timeline-title{font-size:.9rem;font-weight:600;display:flex;align-items:center;gap:7px;user-select:none} |
| .timeline-title-icon{font-size:.85rem;opacity:.6} |
| .timeline-canvas-container{position:relative;background:var(--bg-tertiary);border-radius:8px;overflow:hidden;border:1px solid var(--border-subtle)} |
| #timelineCanvas{display:block;width:100%} |
| .legend-bar{display:flex;flex-wrap:wrap;gap:10px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border-subtle)} |
| .legend-item{display:flex;align-items:center;gap:6px;font-size:.78rem;color:var(--text-secondary);font-weight:500} |
| .legend-color{width:12px;height:12px;border-radius:4px} |
| .tooltip{display:none;position:fixed;background:rgba(14,14,22,.95);color:#e8e8f0;padding:6px 11px;border-radius:7px;font-size:.73rem;pointer-events:none;z-index:100;border:1px solid #2a2a3a;box-shadow:0 6px 20px rgba(0,0,0,.5);font-family:'Inter',sans-serif;max-width:260px;line-height:1.4} |
| .model-badge{font-size:.7rem;padding:3px 8px;border-radius:12px;background:rgba(99,102,241,.1);color:#a5a8ff;border:1px solid rgba(99,102,241,.15)} |
| .stats-grid{display:none;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px} |
| .stats-grid.active{display:grid} |
| .stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:10px 12px;text-align:center} |
| .stat-value{font-size:1.3rem;font-weight:700;color:#c7c8ff} |
| .stat-label{font-size:.68rem;color:var(--text-secondary);margin-top:2px;text-transform:uppercase;letter-spacing:.04em} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <div class="header-left"> |
| <div class="header-icon">✦</div> |
| <h1>Human Essence · Inference</h1> |
| </div> |
| <div class="header-controls"> |
| <span class="model-badge" id="modelStatus">Loading…</span> |
| <button class="btn btn-success" onclick="trainModel()" id="trainBtn">■ Train Model</button> |
| <button class="btn btn-secondary" onclick="clearAll()">✕ Clear</button> |
| </div> |
| </div> |
|
|
| <div class="stats-grid" id="statsGrid"></div> |
|
|
| <div class="editor-section"> |
| <div class="editor-wrapper" id="editorWrapper"> |
| <div id="editorOverlay"><span class="placeholder-text">Type or paste text here…</span></div> |
| <textarea id="editor"></textarea> |
| </div> |
| <div class="loading-bar" id="loadingBar"> |
| <div class="spinner"></div> |
| <span>Running inference…</span> |
| </div> |
| </div> |
|
|
| <div class="timeline-section" id="timelineSection"> |
| <div class="timeline-header"> |
| <div class="timeline-title"> |
| <span class="timeline-title-icon">◆</span> |
| Emotional Flow |
| </div> |
| </div> |
| <div class="timeline-canvas-container"> |
| <canvas id="timelineCanvas"></canvas> |
| </div> |
| <div class="legend-bar" id="legendBar"></div> |
| </div> |
| </div> |
|
|
| <div class="tooltip" id="tooltip"></div> |
|
|
| <script> |
| var modelData = null; |
| var loaded = false; |
| |
| var palette = [ |
| '#6366f1','#8b5cf6','#ec4899','#f43f5e','#f97316', |
| '#eab308','#22c55e','#06b6d4','#3b82f6','#a855f7', |
| '#14b8a6','#f472b6','#fb923c','#a3e635','#818cf8', |
| '#c084fc','#f9a8d4','#fdba74','#bef264','#67e8f9', |
| '#7dd3fc','#c4b5fd','#f0abfc','#d4d4d8' |
| ]; |
| var labelColor = {}; |
| |
| function colorFor(label) { |
| if (!labelColor[label]) { |
| labelColor[label] = palette[Object.keys(labelColor).length % palette.length]; |
| } |
| return labelColor[label]; |
| } |
| |
| function hexToRgb(hex) { |
| var m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); |
| if (m) return {r:parseInt(m[1],16),g:parseInt(m[2],16),b:parseInt(m[3],16)}; |
| return {r:0,g:0,b:0}; |
| } |
| |
| function tokenize(text) { |
| return text.match(/\S+/g) || []; |
| } |
| |
| function tokenFeatures(tokens, i) { |
| var token = tokens[i]; |
| var lower = token.toLowerCase(); |
| var clean = lower.replace(/[^a-z0-9']+/g, ''); |
| var prev = i > 0 ? tokens[i-1].toLowerCase() : '<START>'; |
| var next = i + 1 < tokens.length ? tokens[i+1].toLowerCase() : '<END>'; |
| var n = Math.max(tokens.length - 1, 1); |
| return { |
| 'word': lower, |
| 'clean_word': clean || '<PUNCT>', |
| 'prev_word': prev, |
| 'next_word': next, |
| 'prefix2': clean.slice(0,2), |
| 'prefix3': clean.slice(0,3), |
| 'suffix2': clean.slice(-2), |
| 'suffix3': clean.slice(-3), |
| 'word_len': token.length, |
| 'position_norm': i / n, |
| 'position_sin': Math.sin((i / n) * Math.PI), |
| 'is_first': i === 0 ? 1 : 0, |
| 'is_last': i === tokens.length - 1 ? 1 : 0, |
| 'is_title': /^[A-Z]/.test(token) && /[a-z]/.test(token) ? 1 : 0, |
| 'is_upper': token === token.toUpperCase() && /[A-Z]/.test(token) ? 1 : 0, |
| 'has_digit': /\d/.test(token) ? 1 : 0, |
| 'has_punct': /[^a-zA-Z0-9]/.test(token) ? 1 : 0, |
| 'ends_period': token.endsWith('.') ? 1 : 0, |
| 'ends_question': token.endsWith('?') ? 1 : 0, |
| 'ends_exclaim': token.endsWith('!') ? 1 : 0, |
| 'contains_ai': /ai/.test(clean) ? 1 : 0 |
| }; |
| } |
| |
| function predict(text) { |
| if (!loaded) return null; |
| var tokens = tokenize(text); |
| if (tokens.length === 0) return {tokens:[], predictions:[], labels:[]}; |
| |
| var vocab = modelData.vocab; |
| var nFeatures = modelData.n_features; |
| var nLabels = modelData.labels.length; |
| var coefs = modelData.coefs; |
| var intercepts = modelData.intercepts; |
| |
| var X = []; |
| for (var i = 0; i < tokens.length; i++) { |
| var feats = tokenFeatures(tokens, i); |
| var vec = new Float64Array(nFeatures); |
| var keys = Object.keys(feats); |
| for (var ki = 0; ki < keys.length; ki++) { |
| var key = keys[ki]; |
| var val = feats[key]; |
| var vocabKey = (typeof val === 'string') ? (key + '=' + val) : key; |
| var col = vocab[vocabKey]; |
| if (col !== undefined) vec[col] = typeof val === 'number' ? val : 1.0; |
| } |
| X.push(vec); |
| } |
| |
| var predictions = new Array(tokens.length); |
| for (i = 0; i < tokens.length; i++) { |
| var preds = new Float64Array(nLabels); |
| for (var j = 0; j < nLabels; j++) { |
| var val = intercepts[j]; |
| var row = X[i]; |
| var c = coefs[j]; |
| for (var k = 0; k < nFeatures; k++) { |
| if (row[k] !== 0) val += row[k] * c[k]; |
| } |
| preds[j] = Math.max(0, Math.min(1, val)); |
| } |
| predictions[i] = preds; |
| } |
| return {tokens:tokens, predictions:predictions, labels:modelData.labels}; |
| } |
| |
| function toFlow(result, threshold) { |
| if (threshold === undefined) threshold = 0.15; |
| var flow = []; |
| var nTokens = result.tokens.length; |
| for (var j = 0; j < result.labels.length; j++) { |
| var label = result.labels[j]; |
| var values = []; |
| for (var i = 0; i < nTokens; i++) values.push(result.predictions[i][j]); |
| var active = values.map(function(v) { return v >= threshold; }); |
| if (!active.some(function(a) { return a; })) continue; |
| var curves = []; |
| i = 0; |
| while (i < nTokens) { |
| if (!active[i]) { i++; continue; } |
| var start = i; |
| while (i + 1 < nTokens && active[i + 1]) i++; |
| var end = i; |
| var segment = values.slice(start, end + 1); |
| var peak = Math.max.apply(null, segment); |
| curves.push({start_word:start,end_word:end,start_intensity:Math.round(values[start]*10000)/10000,peak_intensity:Math.round(peak*10000)/10000,end_intensity:Math.round(values[end]*10000)/10000}); |
| i++; |
| } |
| if (curves.length) flow.push({label:label,curves:curves}); |
| } |
| return flow; |
| } |
| |
| function updateOverlay(result, flow) { |
| var overlay = document.getElementById('editorOverlay'); |
| var editor = document.getElementById('editor'); |
| var text = editor.value; |
| var tokens = result.tokens; |
| var nTokens = tokens.length; |
| var emotionData = {}; |
| if (flow.length > 0) { |
| for (var i = 0; i < nTokens; i++) { |
| var active = []; |
| for (var fi = 0; fi < flow.length; fi++) { |
| var emo = flow[fi]; |
| var lbl = emo.label; |
| var col = colorFor(lbl); |
| for (var ci = 0; ci < emo.curves.length; ci++) { |
| var curve = emo.curves[ci]; |
| if (i >= curve.start_word && i <= curve.end_word) { |
| var intensity = curve.peak_intensity; |
| if (curve.start_word === curve.end_word) {active.push({label:lbl,color:col,intensity:intensity});} |
| else if (i === curve.start_word) {active.push({label:lbl,color:col,intensity:curve.start_intensity});} |
| else if (i === curve.end_word) {active.push({label:lbl,color:col,intensity:curve.end_intensity});} |
| else {active.push({label:lbl,color:col,intensity:intensity});} |
| } |
| } |
| } |
| if (active.length > 0) emotionData[i] = active; |
| } |
| } |
| var words = tokens; |
| var html = ''; |
| var lastIdx = 0; |
| for (i = 0; i < words.length; i++) { |
| var w = words[i]; |
| var idx = text.indexOf(w, lastIdx); |
| if (idx > lastIdx) { |
| var gap = text.slice(lastIdx, idx).replace(/ /g,' ').replace(/\n/g,'<br>').replace(/\t/g,' '); |
| html += gap; |
| } |
| var emos = emotionData[i]; |
| var style = ''; |
| var cls = 'word'; |
| if (emos && emos.length) { |
| var r=0,g=0,b=0,total=0; |
| for (var ei = 0; ei < emos.length; ei++) { |
| var rgb = hexToRgb(emos[ei].color); |
| r += rgb.r * emos[ei].intensity; |
| g += rgb.g * emos[ei].intensity; |
| b += rgb.b * emos[ei].intensity; |
| total += emos[ei].intensity; |
| } |
| if (total > 0) { |
| r = Math.round(r/total); g = Math.round(g/total); b = Math.round(b/total); |
| var maxI = 0; |
| for (ei = 0; ei < emos.length; ei++) {if (emos[ei].intensity > maxI) maxI = emos[ei].intensity;} |
| var bgA = Math.max(0.06, maxI * 0.25); |
| style += 'background:rgba('+r+','+g+','+b+','+bgA+');'; |
| var blend = Math.min(0.45, maxI * 0.4); |
| style += 'color:rgb('+Math.round(224*(1-blend)+r*blend)+','+Math.round(224*(1-blend)+g*blend)+','+Math.round(232*(1-blend)+b*blend)+');'; |
| style += 'border-bottom-color:rgba('+r+','+g+','+b+','+Math.min(0.5,maxI*0.4)+');'; |
| cls += ' has-emotion'; |
| } |
| } |
| var escaped = w.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
| html += '<span class="'+cls+'" style="'+style+'">'+escaped+'</span>'; |
| lastIdx = idx + w.length; |
| } |
| var trailing = text.slice(lastIdx).replace(/ /g,' ').replace(/\n/g,'<br>').replace(/\t/g,' '); |
| html += trailing; |
| overlay.innerHTML = html; |
| } |
| |
| var canvas = document.getElementById('timelineCanvas'); |
| var ctx = canvas.getContext('2d'); |
| var tooltip = document.getElementById('tooltip'); |
| |
| function resizeCanvas() { |
| var c = canvas.parentElement; |
| canvas.width = c.clientWidth; |
| canvas.height = 175; |
| } |
| |
| function wordToX(wi, total) { |
| var pad = 35, w = canvas.width - pad*2; |
| return pad + (wi/total)*w; |
| } |
| |
| function intensityToY(i) { |
| var pad = 22, h = canvas.height - pad*2; |
| return pad + h - i*h; |
| } |
| |
| function drawTimeline(result, flow) { |
| resizeCanvas(); |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| var total = result.tokens.length || 1; |
| for (var i = 0; i <= 1; i += 0.25) { |
| var y = intensityToY(i); |
| ctx.strokeStyle = i === 0 ? '#1e1e2e' : '#141420'; |
| ctx.lineWidth = 1; |
| ctx.beginPath(); ctx.moveTo(35, y); ctx.lineTo(canvas.width-10, y); ctx.stroke(); |
| ctx.fillStyle = '#2e2e42'; |
| ctx.font = '9px Inter, sans-serif'; |
| ctx.textAlign = 'right'; |
| ctx.fillText((i*100).toFixed(0)+'%', 31, y+3); |
| } |
| var step = Math.max(1, Math.floor(total/20)); |
| for (i = 0; i <= total; i += step) { |
| var x = wordToX(i, total); |
| ctx.strokeStyle = '#111120'; |
| ctx.beginPath(); ctx.moveTo(x,22); ctx.lineTo(x,canvas.height-22); ctx.stroke(); |
| } |
| for (var fi = 0; fi < flow.length; fi++) { |
| var emo = flow[fi]; |
| var color = colorFor(emo.label); |
| var rgb = hexToRgb(color); |
| var sorted = emo.curves.slice().sort(function(a,b) { return a.start_word - b.start_word; }); |
| if (!sorted.length) continue; |
| ctx.beginPath(); |
| var fX = wordToX(sorted[0].start_word, total); |
| var fY = intensityToY(sorted[0].peak_intensity); |
| ctx.moveTo(fX, canvas.height-22); |
| ctx.lineTo(fX, fY); |
| for (var si = 1; si < sorted.length; si++) { |
| var sx = wordToX(sorted[si].start_word, total); |
| var sy = intensityToY(sorted[si].peak_intensity); |
| var ppx = wordToX(sorted[si-1].start_word, total); |
| var ppy = intensityToY(sorted[si-1].peak_intensity); |
| ctx.bezierCurveTo((ppx+sx)/2, ppy, (ppx+sx)/2, sy, sx, sy); |
| } |
| var lX = wordToX(sorted[sorted.length-1].start_word, total); |
| ctx.lineTo(lX, canvas.height-22); |
| ctx.closePath(); |
| var grad = ctx.createLinearGradient(0,22,0,canvas.height-22); |
| grad.addColorStop(0, 'rgba('+rgb.r+','+rgb.g+','+rgb.b+',0.1)'); |
| grad.addColorStop(1, 'rgba('+rgb.r+','+rgb.g+','+rgb.b+',0)'); |
| ctx.fillStyle = grad; ctx.fill(); |
| ctx.beginPath(); |
| for (si = 0; si < sorted.length; si++) { |
| sx = wordToX(sorted[si].start_word, total); |
| sy = intensityToY(sorted[si].peak_intensity); |
| if (si === 0) ctx.moveTo(sx, sy); |
| else { ppx = wordToX(sorted[si-1].start_word, total); ppy = intensityToY(sorted[si-1].peak_intensity); ctx.bezierCurveTo((ppx+sx)/2, ppy, (ppx+sx)/2, sy, sx, sy); } |
| } |
| ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke(); |
| for (si = 0; si < sorted.length; si++) { |
| sx = wordToX(sorted[si].start_word, total); |
| sy = intensityToY(sorted[si].peak_intensity); |
| ctx.beginPath(); ctx.arc(sx, sy, 4, 0, Math.PI*2); |
| ctx.fillStyle = color; ctx.fill(); |
| ctx.beginPath(); ctx.arc(sx, sy, 2, 0, Math.PI*2); |
| ctx.fillStyle = '#fff'; ctx.fill(); |
| } |
| } |
| } |
| |
| function updateLegend(flow) { |
| var bar = document.getElementById('legendBar'); |
| var html = ''; |
| for (var i = 0; i < flow.length; i++) { |
| var emo = flow[i]; |
| html += '<div class="legend-item"><span class="legend-color" style="background:'+colorFor(emo.label)+'"></span>'+emo.label+'</div>'; |
| } |
| bar.innerHTML = html; |
| } |
| |
| function runInference() { |
| var text = document.getElementById('editor').value.trim(); |
| if (!text) { document.getElementById('timelineSection').classList.remove('active'); return; } |
| if (!loaded) return; |
| var loadingBar = document.getElementById('loadingBar'); |
| loadingBar.classList.add('active'); |
| setTimeout(function() { |
| try { |
| var result = predict(text); |
| if (!result) return; |
| var flow = toFlow(result, 0.15); |
| updateOverlay(result, flow); |
| drawTimeline(result, flow); |
| updateLegend(flow); |
| document.getElementById('timelineSection').classList.add('active'); |
| } finally { loadingBar.classList.remove('active'); } |
| }, 10); |
| } |
| |
| function clearAll() { |
| document.getElementById('editor').value = ''; |
| document.getElementById('editorOverlay').innerHTML = '<span class="placeholder-text">Type or paste text here…</span>'; |
| document.getElementById('timelineSection').classList.remove('active'); |
| } |
| |
| function debounce(fn, ms) { |
| var timer = null; |
| return function() { |
| var args = arguments, self = this; |
| if (timer) clearTimeout(timer); |
| timer = setTimeout(function() { fn.apply(self, args); }, ms); |
| }; |
| } |
| |
| function showStats(info) { |
| var grid = document.getElementById('statsGrid'); |
| grid.className = 'stats-grid active'; |
| var items = [ |
| {label:'Rows', value:info.total_rows}, |
| {label:'Usable', value:info.usable_rows}, |
| {label:'Words', value:info.word_examples}, |
| {label:'Curves', value:info.curve_points}, |
| {label:'Emotions', value:info.labels ? info.labels.length : 0}, |
| {label:'MAE', value:info.train_mae ? info.train_mae.toFixed(4) : '-'}, |
| {label:'RMSE', value:info.train_rmse ? info.train_rmse.toFixed(4) : '-'} |
| ]; |
| grid.innerHTML = items.map(function(item) { |
| return '<div class="stat-card"><div class="stat-value">'+item.value+'</div><div class="stat-label">'+item.label+'</div></div>'; |
| }).join(''); |
| } |
| |
| function loadModel() { |
| document.getElementById('modelStatus').textContent = 'Loading…'; |
| fetch('/api/model_data') |
| .then(function(res) { |
| if (!res.ok) throw new Error('No model yet'); |
| return res.json(); |
| }) |
| .then(function(data) { |
| modelData = data; |
| loaded = true; |
| document.getElementById('modelStatus').textContent = '\u2713 '+modelData.labels.length+' emotions'; |
| var ta = document.getElementById('editor'); |
| ta.addEventListener('input', debounce(runInference, 400)); |
| ta.focus(); |
| if (ta.value.trim()) runInference(); |
| }) |
| .catch(function() { |
| document.getElementById('modelStatus').textContent = 'Need training'; |
| document.getElementById('modelStatus').style.color = '#fbbf24'; |
| var ta = document.getElementById('editor'); |
| ta.addEventListener('input', debounce(runInference, 400)); |
| }); |
| fetch('/api/stats') |
| .then(function(res) { return res.json(); }) |
| .then(function(stats) { |
| if (stats.trained) showStats(stats); |
| }) |
| .catch(function() {}); |
| } |
| |
| function trainModel() { |
| var btn = document.getElementById('trainBtn'); |
| btn.disabled = true; |
| btn.textContent = '\u25A0 Training…'; |
| document.getElementById('modelStatus').textContent = 'Training…'; |
| fetch('/api/train', {method:'POST'}) |
| .then(function(res) { |
| if (!res.ok) throw new Error('Training failed'); |
| return res.json(); |
| }) |
| .then(function(data) { |
| showStats(data.training_info); |
| document.getElementById('modelStatus').textContent = '\u2713 '+data.training_info.labels.length+' emotions'; |
| document.getElementById('modelStatus').style.color = ''; |
| |
| return fetch('/api/model_data'); |
| }) |
| .then(function(res) { return res.json(); }) |
| .then(function(data) { |
| modelData = data; |
| loaded = true; |
| btn.textContent = '\u25A0 Train Model'; |
| btn.disabled = false; |
| var ta = document.getElementById('editor'); |
| if (ta.value.trim()) runInference(); |
| }) |
| .catch(function(e) { |
| document.getElementById('modelStatus').textContent = '\u2715 Train error'; |
| document.getElementById('modelStatus').style.color = '#fca5a5'; |
| btn.textContent = '\u25A0 Train Model'; |
| btn.disabled = false; |
| console.error(e); |
| }); |
| } |
| |
| document.getElementById('editor').addEventListener('scroll', function() { |
| var overlay = document.getElementById('editorOverlay'); |
| overlay.scrollTop = this.scrollTop; |
| overlay.scrollLeft = this.scrollLeft; |
| }); |
| |
| window.addEventListener('resize', function() { |
| if (loaded && document.getElementById('timelineSection').classList.contains('active')) { |
| var text = document.getElementById('editor').value.trim(); |
| if (text) { var result = predict(text); if (result) drawTimeline(result, toFlow(result, 0.15)); } |
| } |
| }); |
| |
| loadModel(); |
| </script> |
| </body> |
| </html> |
|
|