Download static/index.html from wop/inference: direct link, hf CLI and curl.
- Browser
- Download file 23.4 kB
-
https://huggingface.co/spaces/wop/inference/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/wop/inference/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/wop/inference/resolve/main/static/index.html
23.4 kB
| <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);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 = ''; | |
| // Reload model data | |
| 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> | |