Spaces:
Runtime error
Runtime error
| (function () { | |
| 'use strict'; | |
| // ---------- state ---------- | |
| var state = { | |
| phase: 'intro', // intro | test | results | |
| probes: [], // the 35 drawn scenarios | |
| items: [], // 50-item interleaved sequence: {type:'scenario'|'word', ...} | |
| itemAnswers: [], // ratings for the 50 items, index-aligned with items | |
| answers: [], // derived scenario-only ratings (for compute/interpret/breakdown) | |
| currentQ: 0, | |
| cardVisible: true, | |
| theme: 'paper', | |
| poolSize: 0, | |
| nonce: null, | |
| profile: null, | |
| interp: null, | |
| wordGaps: null, | |
| barsReady: false | |
| }; | |
| var pool = null; | |
| var poolTries = 0; | |
| var drag = false; | |
| var timers = { t1: null, t2: null, tf: null, tb: null }; | |
| // ---------- dom ---------- | |
| var root = document.getElementById('app-root'); | |
| var el = { | |
| themeToggle: document.getElementById('theme-toggle'), | |
| phaseIntro: document.getElementById('phase-intro'), | |
| phaseTest: document.getElementById('phase-test'), | |
| phaseResults: document.getElementById('phase-results'), | |
| introGradient: document.getElementById('intro-gradient'), | |
| poolLine: document.getElementById('pool-line'), | |
| beginBtn: document.getElementById('begin-btn'), | |
| progressFill: document.getElementById('progress-fill'), | |
| progressText: document.getElementById('progress-text'), | |
| qIndex: document.getElementById('q-index'), | |
| wordPrompt: document.getElementById('word-prompt'), | |
| probeText: document.getElementById('probe-text'), | |
| valueDisplay: document.getElementById('value-display'), | |
| valueWord: document.getElementById('value-word'), | |
| rail: document.getElementById('rail'), | |
| railGradient: document.getElementById('rail-gradient'), | |
| railTicks: document.getElementById('rail-ticks'), | |
| railThumb: document.getElementById('rail-thumb'), | |
| card: document.getElementById('question-card'), | |
| prevBtn: document.getElementById('prev-btn'), | |
| nextBtn: document.getElementById('next-btn'), | |
| headlineCard: document.getElementById('headline-card'), | |
| headlineTitle: document.getElementById('headline-title'), | |
| headlineBody: document.getElementById('headline-body'), | |
| dimRows: document.getElementById('dim-rows'), | |
| insightList: document.getElementById('insight-list'), | |
| tierPanel: document.getElementById('tier-panel'), | |
| tierRead: document.getElementById('tier-read'), | |
| callbackPanel: document.getElementById('callback-panel'), | |
| callbackList: document.getElementById('callback-list'), | |
| wordgapPanel: document.getElementById('wordgap-panel'), | |
| wordgapList: document.getElementById('wordgap-list'), | |
| breakdownList: document.getElementById('breakdown-list'), | |
| restartBtn: document.getElementById('restart-btn') | |
| }; | |
| // ---------- constants ---------- | |
| var SCALE_MAP = { '-6':0,'-5':0,'-4':51,'-3':102,'-2':128,'-1':140,'0':153,'1':166,'2':179,'3':204,'4':230,'5':255,'6':255 }; | |
| var WORDS = ['Hits hard','Hits hard','Hits hard','Stings','Stings','Eases off','Doesn’t affect me','Lifts a little','Feels good','Feels good','Feels great','Feels great','Feels great']; | |
| var DIM_META = [ | |
| { key:'V', name:'Valence', lo:'negative bias', hi:'positive bias' }, | |
| { key:'A', name:'Arousal', lo:'calm / flat', hi:'reactive' }, | |
| { key:'D', name:'Dominance', lo:'powerless', hi:'in control' }, | |
| { key:'U', name:'Urgency', lo:'no pressure', hi:'everything urgent' }, | |
| { key:'G', name:'Gravity', lo:'feels heavy', hi:'feels light' }, | |
| { key:'W', name:'Self-Worth', lo:'self-critical', hi:'self-assured' }, | |
| { key:'I', name:'Intent', lo:'withdrawing', hi:'connecting' } | |
| ]; | |
| var TIER_META = [ | |
| { key:'N', name:'Every scene was neutral', desc:'Each one was written to be ambiguous — the meaning came from you. This is what you projected onto blank situations.' } | |
| ]; | |
| // ---------- palette / color ---------- | |
| function pal() { | |
| if (state.theme === 'dusk') return { | |
| clay:[220,114,83], sand:[150,139,118], green:[98,179,129], | |
| dims:{ V:'#6E9BD6', A:'#DB8154', D:'#A98AD2', U:'#DC6F5E', G:'#DFB35F', W:'#62BB8E', I:'#CDA552' }, | |
| rate:{ low:'#DC7253', high:'#62B381', mid:'#8E8369' }, | |
| ins:{ warn:'#D78B57', good:'#62B381', neutral:'#AC9F87' }, | |
| tier:{ N:'#B79BCB', NEG:'#DC7253', POS:'#62B381' } | |
| }; | |
| return { | |
| clay:[154,59,37], sand:[199,183,155], green:[47,107,69], | |
| dims:{ V:'#3F6FB0', A:'#C0623A', D:'#7A5BA6', U:'#B0453A', G:'#C2913E', W:'#3E8A6B', I:'#A8832F' }, | |
| rate:{ low:'#9A3B25', high:'#2F6B45', mid:'#A89B85' }, | |
| ins:{ warn:'#B26A3A', good:'#3E8A6B', neutral:'#9A8E79' }, | |
| tier:{ N:'#8A6B9E', NEG:'#B0453A', POS:'#3E8A6B' } | |
| }; | |
| } | |
| function colorFor(v) { | |
| var p = pal(); | |
| if (v === null || v === undefined) { var s = p.sand; return 'rgb(' + s[0] + ',' + s[1] + ',' + s[2] + ')'; } | |
| var a, b, t; | |
| if (v <= 0) { a = p.clay; b = p.sand; t = (v + 6) / 6; } else { a = p.sand; b = p.green; t = v / 6; } | |
| var c = a.map(function (x, i) { return Math.round(x + (b[i] - x) * t); }); | |
| return 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')'; | |
| } | |
| function gradientCss() { | |
| return 'linear-gradient(90deg,' + colorFor(-6) + ',' + colorFor(0) + ' 50%,' + colorFor(6) + ')'; | |
| } | |
| // ---------- theme ---------- | |
| function applyBodyBg(t) { | |
| try { | |
| var c = t === 'dusk' ? '#100E09' : '#ECE4D4'; | |
| document.body.style.background = c; | |
| document.documentElement.style.background = c; | |
| } catch (_) {} | |
| } | |
| function setTheme(t) { | |
| state.theme = t; | |
| root.setAttribute('data-theme', t); | |
| el.themeToggle.innerHTML = t === 'dusk' ? '☀' : '☾'; | |
| applyBodyBg(t); | |
| refreshGradients(); | |
| if (state.phase === 'test') renderTest(); | |
| if (state.phase === 'results') renderResults(); | |
| } | |
| function toggleTheme() { setTheme(state.theme === 'dusk' ? 'paper' : 'dusk'); } | |
| function refreshGradients() { | |
| var g = gradientCss(); | |
| el.introGradient.style.background = g; | |
| el.railGradient.style.background = g; | |
| el.progressFill.style.background = g; | |
| } | |
| // ---------- probe selection (ported from rtr_logic.js) ---------- | |
| function shuffle(arr) { | |
| for (var i = arr.length - 1; i > 0; i--) { | |
| var j = Math.floor(Math.random() * (i + 1)); | |
| var t = arr[i]; arr[i] = arr[j]; arr[j] = t; | |
| } | |
| return arr; | |
| } | |
| function selectProbes(p) { | |
| var cats = ['V','A','D','U','G','W','I'], tiers = ['N','NEG','POS'], groups = {}; | |
| for (var ci = 0; ci < cats.length; ci++) for (var ti = 0; ti < tiers.length; ti++) groups[cats[ci] + '_' + tiers[ti]] = []; | |
| for (var i = 0; i < p.length; i++) { var k = p[i].cat + '_' + p[i].tier; if (groups[k]) groups[k].push(p[i]); } | |
| for (var key in groups) shuffle(groups[key]); | |
| var selected = []; | |
| for (var c2 = 0; c2 < cats.length; c2++) { | |
| var c = cats[c2], catProbes = []; | |
| var nArr = groups[c + '_N'], negArr = groups[c + '_NEG'], posArr = groups[c + '_POS']; | |
| if (nArr.length) catProbes.push(nArr.pop()); | |
| if (negArr.length) catProbes.push(negArr.pop()); | |
| if (posArr.length) catProbes.push(posArr.pop()); | |
| var remaining = []; | |
| for (var j = 0; j < nArr.length; j++) remaining.push(nArr[j]); | |
| for (var j2 = 0; j2 < negArr.length; j2++) remaining.push(negArr[j2]); | |
| for (var j3 = 0; j3 < posArr.length; j3++) remaining.push(posArr[j3]); | |
| shuffle(remaining); | |
| var need = 5 - catProbes.length; | |
| for (var j4 = 0; j4 < need && j4 < remaining.length; j4++) catProbes.push(remaining[j4]); | |
| for (var j5 = 0; j5 < catProbes.length; j5++) selected.push(catProbes[j5]); | |
| } | |
| shuffle(selected); | |
| return selected; | |
| } | |
| // ---------- words fetch + 50-item sequence ---------- | |
| function fetchWords() { | |
| // fail-soft: any error or empty -> resolve to [] (just the 35 scenarios) | |
| return fetch('/api/words?n=15', { headers: { 'Accept': 'application/json' } }) | |
| .then(function (r) { return r.ok ? r.json() : null; }) | |
| .then(function (j) { | |
| var ws = (j && Array.isArray(j.words)) ? j.words : []; | |
| return ws.filter(function (w) { return w && w.word; }).slice(0, 15); | |
| }) | |
| .catch(function () { return []; }); | |
| } | |
| // Build a single 50-item sequence (35 scenarios + up to 15 words), | |
| // interleaved so it doesn't feel like two blocks. | |
| function buildSequence(scenarios, words) { | |
| var items = []; | |
| for (var i = 0; i < scenarios.length; i++) { | |
| items.push({ type: 'scenario', probe: scenarios[i] }); | |
| } | |
| for (var j = 0; j < words.length; j++) { | |
| items.push({ type: 'word', word: words[j] }); | |
| } | |
| shuffle(items); | |
| return items; | |
| } | |
| function scenarioProbes() { | |
| var out = []; | |
| for (var i = 0; i < state.items.length; i++) { | |
| if (state.items[i].type === 'scenario') out.push(state.items[i].probe); | |
| } | |
| return out; | |
| } | |
| function scenarioAnswers() { | |
| var out = []; | |
| for (var i = 0; i < state.items.length; i++) { | |
| if (state.items[i].type === 'scenario') out.push(state.itemAnswers[i]); | |
| } | |
| return out; | |
| } | |
| // ---------- pool load (retry, like original _loadPool) ---------- | |
| function loadPool() { | |
| var p = (typeof window !== 'undefined' && window.RTR_POOL) ? window.RTR_POOL : null; | |
| if (p && p.length) { | |
| pool = p; | |
| state.probes = selectProbes(pool); | |
| state.poolSize = pool.length; | |
| el.poolLine.textContent = '50 questions — ' + state.probes.length + ' situations drawn from ' + state.poolSize + ', plus words — every run differs'; | |
| } else { | |
| poolTries += 1; | |
| if (poolTries < 120) setTimeout(loadPool, 40); | |
| } | |
| } | |
| // ---------- flow ---------- | |
| function clearTimers() { | |
| clearTimeout(timers.t1); clearTimeout(timers.t2); | |
| clearTimeout(timers.tf); clearTimeout(timers.tb); | |
| } | |
| function showPhase(phase) { | |
| state.phase = phase; | |
| el.phaseIntro.hidden = phase !== 'intro'; | |
| el.phaseTest.hidden = phase !== 'test'; | |
| el.phaseResults.hidden = phase !== 'results'; | |
| } | |
| function begin() { | |
| if (!state.probes.length) return; | |
| el.beginBtn.disabled = true; | |
| fetchWords().then(function (words) { | |
| // fail-soft: words may be [] (then sequence is just the 35 scenarios) | |
| state.items = buildSequence(state.probes, words); | |
| state.itemAnswers = new Array(state.items.length).fill(null); | |
| state.nonce = newNonce(); | |
| state.currentQ = 0; | |
| state.cardVisible = true; | |
| el.card.classList.remove('rtr-hidden'); | |
| el.beginBtn.disabled = false; | |
| showPhase('test'); | |
| renderTest(); | |
| }); | |
| } | |
| function newNonce() { | |
| try { | |
| if (window.crypto && typeof window.crypto.randomUUID === 'function') return window.crypto.randomUUID(); | |
| } catch (_) {} | |
| return 'n-' + Date.now() + '-' + Math.random().toString(16).slice(2); | |
| } | |
| function setProvisional(value) { | |
| state.itemAnswers[state.currentQ] = value; | |
| renderTest(); | |
| } | |
| function valueFromEvent(e) { | |
| var r = el.rail.getBoundingClientRect(); | |
| var ratio = (e.clientX - r.left) / r.width; | |
| ratio = Math.max(0, Math.min(1, ratio)); | |
| return Math.round(ratio * 12) - 6; | |
| } | |
| function railDown(e) { | |
| drag = true; | |
| try { el.rail.setPointerCapture(e.pointerId); } catch (_) {} | |
| setProvisional(valueFromEvent(e)); | |
| } | |
| function railMove(e) { if (drag) setProvisional(valueFromEvent(e)); } | |
| function railUp() { | |
| if (!drag) return; | |
| drag = false; | |
| if (state.itemAnswers[state.currentQ] === null) return; | |
| scheduleAdvance(); | |
| } | |
| function railKey(e) { | |
| var cur = state.itemAnswers[state.currentQ]; | |
| if (cur === null) cur = 0; | |
| if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); setProvisional(Math.max(-6, cur - 1)); } | |
| else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); setProvisional(Math.min(6, cur + 1)); } | |
| else if (e.key === 'Enter') { if (state.itemAnswers[state.currentQ] !== null) next(); } | |
| } | |
| function scheduleAdvance() { | |
| clearTimers(); | |
| timers.t1 = setTimeout(function () { | |
| state.cardVisible = false; | |
| el.card.classList.add('rtr-hidden'); | |
| timers.t2 = setTimeout(function () { | |
| var nx = state.currentQ + 1; | |
| if (nx < state.items.length) { | |
| state.currentQ = nx; state.cardVisible = true; | |
| el.card.classList.remove('rtr-hidden'); | |
| renderTest(); | |
| } else { goToResults(); } | |
| }, 260); | |
| }, 300); | |
| } | |
| function fadeTo(idx) { | |
| clearTimers(); | |
| state.cardVisible = false; | |
| el.card.classList.add('rtr-hidden'); | |
| timers.tf = setTimeout(function () { | |
| state.currentQ = idx; state.cardVisible = true; | |
| el.card.classList.remove('rtr-hidden'); | |
| renderTest(); | |
| }, 260); | |
| } | |
| function next() { | |
| if (state.itemAnswers[state.currentQ] === null) return; | |
| var nx = state.currentQ + 1; | |
| if (nx < state.items.length) fadeTo(nx); | |
| else goToResults(); | |
| } | |
| function prev() { if (state.currentQ > 0) fadeTo(state.currentQ - 1); } | |
| function goToResults() { | |
| clearTimers(); | |
| // Scenario answers feed the 7-D profile EXACTLY as before; word answers do not. | |
| state.probes = scenarioProbes(); | |
| state.answers = scenarioAnswers(); | |
| state.profile = window.RTR_COMPUTE.computeProfile(state.probes, state.answers); | |
| state.interp = window.RTR_INTERPRET.interpret(state.profile, state.probes, state.answers); | |
| state.wordGaps = null; | |
| state.barsReady = false; | |
| showPhase('results'); | |
| renderResults(); | |
| try { window.scrollTo(0, 0); } catch (_) {} | |
| timers.tb = setTimeout(function () { state.barsReady = true; renderDimRows(); }, 220); | |
| submitRun(); | |
| } | |
| function submitRun() { | |
| var scenarios = []; | |
| var words = []; | |
| for (var i = 0; i < state.items.length; i++) { | |
| var it = state.items[i], rating = state.itemAnswers[i]; | |
| if (rating === null) continue; | |
| if (it.type === 'scenario') { | |
| scenarios.push({ id: it.probe.id, cat: it.probe.cat, tier: it.probe.tier, rating: rating }); | |
| } else { | |
| words.push({ id: it.word.id, word: it.word.word, source: it.word.source, rating: rating }); | |
| } | |
| } | |
| // fail-soft: if /api/submit errors, results stay; just no word-gap section. | |
| fetch('/api/submit', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ nonce: state.nonce, scenarios: scenarios, words: words }) | |
| }) | |
| .then(function (r) { return r.ok ? r.json() : null; }) | |
| .then(function (j) { | |
| var gaps = (j && Array.isArray(j.wordGaps)) ? j.wordGaps : null; | |
| if (state.phase !== 'results') return; | |
| state.wordGaps = gaps; | |
| renderWordGaps(); | |
| }) | |
| .catch(function () { /* fail-soft: leave word-gap section hidden */ }); | |
| } | |
| function restart() { | |
| clearTimers(); | |
| state.probes = selectProbes(pool || []); | |
| state.items = []; | |
| state.itemAnswers = []; | |
| state.answers = []; | |
| state.nonce = null; | |
| state.currentQ = 0; | |
| state.profile = null; | |
| state.interp = null; | |
| state.wordGaps = null; | |
| state.barsReady = false; | |
| state.cardVisible = true; | |
| el.card.classList.remove('rtr-hidden'); | |
| showPhase('intro'); | |
| try { window.scrollTo(0, 0); } catch (_) {} | |
| } | |
| // ---------- render: test ---------- | |
| function pad(n) { return (n < 10 ? '0' : '') + n; } | |
| function buildTicks() { | |
| el.railTicks.innerHTML = ''; | |
| for (var i = 0; i < 13; i++) { | |
| var d = document.createElement('div'); | |
| d.className = 'rtr-tick'; | |
| d.style.left = (i / 12 * 100) + '%'; | |
| el.railTicks.appendChild(d); | |
| } | |
| } | |
| function renderTest() { | |
| var items = state.items, idx = state.currentQ, total = items.length; | |
| var item = items[idx]; | |
| var cur = state.itemAnswers[idx]; | |
| var done = state.itemAnswers.filter(function (a) { return a !== null; }).length; | |
| var hasVal = cur !== null; | |
| var dusk = state.theme === 'dusk'; | |
| var isWord = item && item.type === 'word'; | |
| if (isWord) { | |
| el.qIndex.textContent = 'Word ' + pad(idx + 1) + ' / ' + total; | |
| el.wordPrompt.hidden = false; | |
| el.wordPrompt.textContent = 'How does this word feel?'; | |
| el.probeText.className = 'rtr-probe-text rtr-is-word'; | |
| el.probeText.textContent = item.word.word; | |
| } else { | |
| el.qIndex.textContent = 'Situation ' + pad(idx + 1) + ' / ' + total; | |
| el.wordPrompt.hidden = true; | |
| el.probeText.className = 'rtr-probe-text'; | |
| el.probeText.textContent = '“' + (item && item.probe ? item.probe.text : '') + '”'; | |
| } | |
| el.progressFill.style.width = (done / total * 100) + '%'; | |
| el.progressText.textContent = done + ' / ' + total; | |
| el.valueDisplay.textContent = hasVal ? ((cur > 0 ? '+' : '') + cur) : '–'; | |
| el.valueDisplay.style.color = colorFor(cur); | |
| el.valueWord.textContent = hasVal ? WORDS[cur + 6] : 'Drag or tap the line'; | |
| // thumb | |
| if (hasVal) { | |
| el.railThumb.className = 'rtr-rail-thumb has-value'; | |
| el.railThumb.style.left = ((((cur) + 6) / 12) * 100) + '%'; | |
| el.railThumb.style.borderColor = colorFor(cur); | |
| } else { | |
| el.railThumb.className = 'rtr-rail-thumb no-value'; | |
| el.railThumb.style.left = '50%'; | |
| el.railThumb.style.borderColor = ''; | |
| } | |
| // prev | |
| el.prevBtn.disabled = idx === 0; | |
| el.prevBtn.style.color = idx === 0 | |
| ? (dusk ? '#544B3B' : '#CFC4B0') | |
| : (dusk ? '#AC9F87' : '#8C806C'); | |
| // next | |
| var last = idx === total - 1; | |
| el.nextBtn.disabled = !hasVal; | |
| el.nextBtn.textContent = last ? 'See results →' : 'Next →'; | |
| if (hasVal) { | |
| el.nextBtn.style.background = dusk ? '#F2EAD9' : '#241F18'; | |
| el.nextBtn.style.borderColor = dusk ? '#F2EAD9' : '#241F18'; | |
| el.nextBtn.style.color = dusk ? '#1A160F' : '#F4EEE2'; | |
| } else { | |
| el.nextBtn.style.background = 'transparent'; | |
| el.nextBtn.style.borderColor = dusk ? '#322C20' : '#D8CDB8'; | |
| el.nextBtn.style.color = dusk ? '#544B3B' : '#C4B89F'; | |
| } | |
| } | |
| // ---------- render: results ---------- | |
| function insAccent(kind) { | |
| var p = pal(); | |
| return kind === 'warn' ? p.ins.warn : kind === 'good' ? p.ins.good : p.ins.neutral; | |
| } | |
| function renderDimRows() { | |
| var pf = state.profile, p = pal(), dims = (state.interp && state.interp.dimensions) || []; | |
| var byKey = {}; | |
| for (var i = 0; i < dims.length; i++) byKey[dims[i].key] = dims[i]; | |
| el.dimRows.innerHTML = ''; | |
| DIM_META.forEach(function (d, di) { | |
| var idim = byKey[d.key] || {}; | |
| var val = (typeof idim.score === 'number') ? idim.score : pf[d.key]; | |
| var mid = 128, color = p.dims[d.key]; | |
| var span = Math.abs(val - mid) / mid; | |
| var half = (state.barsReady ? span : 0) * 50; | |
| var right = val >= mid; | |
| var reading = idim.reading || getReading(val, d.lo, d.hi); | |
| var line = idim.line || ''; | |
| var row = document.createElement('div'); | |
| row.className = 'rtr-dim-row'; | |
| var head = document.createElement('div'); | |
| head.className = 'rtr-dim-head'; | |
| var nm = document.createElement('span'); | |
| nm.className = 'rtr-dim-name'; nm.textContent = idim.name || d.name; | |
| var rd = document.createElement('span'); | |
| rd.className = 'rtr-dim-reading'; rd.style.color = color; rd.textContent = reading; | |
| head.appendChild(nm); head.appendChild(rd); | |
| var track = document.createElement('div'); | |
| track.className = 'rtr-dim-track'; | |
| var center = document.createElement('div'); center.className = 'rtr-dim-center'; | |
| var fill = document.createElement('div'); | |
| fill.className = 'rtr-dim-fill'; | |
| fill.style.left = right ? '50%' : (50 - half) + '%'; | |
| fill.style.width = half + '%'; | |
| fill.style.background = color; | |
| fill.style.borderRadius = right ? '0 7px 7px 0' : '7px 0 0 7px'; | |
| fill.style.transitionDelay = (di * 0.07) + 's'; | |
| track.appendChild(center); track.appendChild(fill); | |
| var foot = document.createElement('div'); | |
| foot.className = 'rtr-dim-foot'; | |
| var lo = document.createElement('span'); lo.textContent = d.lo; | |
| var hi = document.createElement('span'); hi.textContent = d.hi; | |
| foot.appendChild(lo); foot.appendChild(hi); | |
| row.appendChild(head); row.appendChild(track); row.appendChild(foot); | |
| if (line) { | |
| var ln = document.createElement('p'); | |
| ln.className = 'rtr-dim-line'; ln.textContent = line; | |
| row.appendChild(ln); | |
| } | |
| el.dimRows.appendChild(row); | |
| }); | |
| } | |
| function getReading(val, lo, hi) { | |
| if (val < 80) return lo; | |
| if (val < 118) return 'slightly ' + lo; | |
| if (val > 180) return hi; | |
| if (val > 138) return 'slightly ' + hi; | |
| return 'balanced'; | |
| } | |
| function renderHeadline() { | |
| var h = state.interp && state.interp.headline; | |
| if (h && (h.title || h.body)) { | |
| el.headlineCard.hidden = false; | |
| el.headlineTitle.textContent = h.title || ''; | |
| el.headlineBody.textContent = h.body || ''; | |
| el.headlineCard.style.borderLeftColor = 'var(--accent,#A8754E)'; | |
| } else { | |
| el.headlineCard.hidden = true; | |
| } | |
| } | |
| function renderInsights() { | |
| var list = (state.interp && state.interp.insights) || []; | |
| el.insightList.innerHTML = ''; | |
| if (!list.length) { el.insightList.parentElement.hidden = true; return; } | |
| el.insightList.parentElement.hidden = false; | |
| list.forEach(function (ins) { | |
| var d = document.createElement('div'); | |
| d.className = 'rtr-insight'; | |
| d.style.borderLeftColor = insAccent(ins.kind); | |
| var t = document.createElement('div'); | |
| t.className = 'rtr-insight-title'; t.textContent = ins.title || ''; | |
| var b = document.createElement('div'); | |
| b.className = 'rtr-insight-body'; b.textContent = ins.body || ''; | |
| d.appendChild(t); d.appendChild(b); | |
| el.insightList.appendChild(d); | |
| }); | |
| } | |
| function renderTierRead() { | |
| var tr = state.interp && state.interp.tierRead; | |
| if (!tr) { el.tierPanel.hidden = true; return; } | |
| el.tierPanel.hidden = false; | |
| el.tierRead.innerHTML = ''; | |
| var lines = [ | |
| { label: 'Floor', text: tr.floorLine }, | |
| { label: 'Ceiling', text: tr.ceilingLine }, | |
| { label: 'Baseline', text: tr.baselineLine } | |
| ]; | |
| lines.forEach(function (l) { | |
| if (!l.text) return; | |
| var row = document.createElement('div'); | |
| row.className = 'rtr-tier-line'; | |
| var lab = document.createElement('div'); | |
| lab.className = 'rtr-tier-label'; lab.textContent = l.label; | |
| var txt = document.createElement('div'); | |
| txt.className = 'rtr-tier-text'; txt.textContent = l.text; | |
| row.appendChild(lab); row.appendChild(txt); | |
| el.tierRead.appendChild(row); | |
| }); | |
| if (tr.summary) { | |
| var s = document.createElement('div'); | |
| s.className = 'rtr-tier-summary'; s.textContent = tr.summary; | |
| el.tierRead.appendChild(s); | |
| } | |
| } | |
| function renderCallbacks() { | |
| var list = (state.interp && state.interp.callbacks) || []; | |
| if (!list.length) { el.callbackPanel.hidden = true; return; } | |
| el.callbackPanel.hidden = false; | |
| el.callbackList.innerHTML = ''; | |
| list.forEach(function (cb) { | |
| var d = document.createElement('div'); | |
| d.className = 'rtr-callback'; | |
| var head = document.createElement('div'); | |
| head.className = 'rtr-callback-head'; | |
| var txt = document.createElement('span'); | |
| txt.className = 'rtr-callback-text'; | |
| txt.textContent = '“' + (cb.text || '') + '”'; | |
| var rate = document.createElement('span'); | |
| rate.className = 'rtr-callback-rating'; | |
| rate.textContent = cb.ratingStr || ''; | |
| head.appendChild(txt); head.appendChild(rate); | |
| d.appendChild(head); | |
| if (cb.note) { | |
| var note = document.createElement('div'); | |
| note.className = 'rtr-callback-note'; note.textContent = cb.note; | |
| d.appendChild(note); | |
| } | |
| el.callbackList.appendChild(d); | |
| }); | |
| } | |
| function renderBreakdown() { | |
| var P = state.probes, A = state.answers, p = pal(); | |
| el.breakdownList.innerHTML = ''; | |
| TIER_META.forEach(function (td) { | |
| var grp = document.createElement('div'); | |
| grp.className = 'rtr-grp'; | |
| var name = document.createElement('div'); | |
| name.className = 'rtr-grp-name'; name.style.color = p.tier[td.key]; name.textContent = td.name; | |
| var desc = document.createElement('p'); | |
| desc.className = 'rtr-grp-desc'; desc.textContent = td.desc; | |
| grp.appendChild(name); grp.appendChild(desc); | |
| for (var pi = 0; pi < P.length; pi++) { | |
| if (P[pi].tier !== td.key) continue; | |
| var raw = A[pi]; | |
| var rs = (raw > 0 ? '+' : '') + raw; | |
| var rc = raw <= -3 ? p.rate.low : raw >= 3 ? p.rate.high : p.rate.mid; | |
| var item = document.createElement('div'); | |
| item.className = 'rtr-grp-item'; | |
| var it = document.createElement('span'); | |
| it.className = 'rtr-grp-item-text'; it.textContent = '“' + P[pi].text + '”'; | |
| var ir = document.createElement('span'); | |
| ir.className = 'rtr-grp-item-rating'; ir.style.color = rc; ir.textContent = rs; | |
| item.appendChild(it); item.appendChild(ir); | |
| grp.appendChild(item); | |
| } | |
| el.breakdownList.appendChild(grp); | |
| }); | |
| } | |
| function renderWordGaps() { | |
| var list = state.wordGaps; | |
| if (!list || !list.length) { el.wordgapPanel.hidden = true; return; } | |
| el.wordgapPanel.hidden = false; | |
| el.wordgapList.innerHTML = ''; | |
| var p = pal(); | |
| list.forEach(function (wg) { | |
| var word = wg.word || ''; | |
| var unknown = wg.source === 'unknown'; | |
| var d = document.createElement('div'); | |
| d.className = 'rtr-insight'; | |
| var title = document.createElement('div'); | |
| title.className = 'rtr-insight-title'; | |
| title.textContent = '‘' + word + '’'; | |
| var body = document.createElement('div'); | |
| body.className = 'rtr-insight-body'; | |
| if (unknown) { | |
| d.style.borderLeftColor = p.ins.neutral; | |
| body.textContent = wg.note || ('the engine didn’t know this; your rating helps teach it.'); | |
| } else { | |
| var gap = (typeof wg.gap === 'number') ? wg.gap : null; | |
| d.style.borderLeftColor = gap === null ? p.ins.neutral : (gap < 0 ? p.ins.warn : p.ins.good); | |
| body.textContent = wg.note || ('you read it ' + (gap === null ? 'differently' : (gap < 0 ? 'colder' : 'warmer')) + ' than the language does'); | |
| } | |
| d.appendChild(title); d.appendChild(body); | |
| el.wordgapList.appendChild(d); | |
| }); | |
| } | |
| function renderResults() { | |
| renderHeadline(); | |
| renderDimRows(); | |
| renderInsights(); | |
| renderTierRead(); | |
| renderCallbacks(); | |
| renderWordGaps(); | |
| renderBreakdown(); | |
| } | |
| // ---------- wire up ---------- | |
| function init() { | |
| setTheme('paper'); | |
| refreshGradients(); | |
| buildTicks(); | |
| el.themeToggle.addEventListener('click', toggleTheme); | |
| el.beginBtn.addEventListener('click', begin); | |
| el.restartBtn.addEventListener('click', restart); | |
| el.prevBtn.addEventListener('click', prev); | |
| el.nextBtn.addEventListener('click', next); | |
| el.rail.addEventListener('pointerdown', railDown); | |
| el.rail.addEventListener('pointermove', railMove); | |
| el.rail.addEventListener('pointerup', railUp); | |
| el.rail.addEventListener('keydown', railKey); | |
| loadPool(); | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); | |