Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nova Triangle — Live Demo</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --gold: #d4b65a; | |
| --gold-bright: #f0d878; | |
| --gold-dim: rgba(212,182,90,0.12); | |
| --gold-glow: rgba(212,182,90,0.5); | |
| --red: #e86060; | |
| --green: #60c880; | |
| --bg: #ffffff; | |
| --text: #1a1a1a; | |
| --dim: #333333; | |
| --node-size: 100px; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'Georgia', serif; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 20px 16px 32px; | |
| overflow-x: hidden; | |
| } | |
| h1 { | |
| font-size: 1.2rem; | |
| font-weight: 300; | |
| letter-spacing: 0.35em; | |
| color: var(--gold-bright); | |
| margin-bottom: 4px; | |
| text-transform: uppercase; | |
| } | |
| .tagline { | |
| font-size: 0.8rem; | |
| color: var(--dim); | |
| letter-spacing: 0.08em; | |
| margin-bottom: 20px; | |
| } | |
| /* ─── TRIANGLE ─── */ | |
| .triangle-wrap { | |
| position: relative; | |
| width: 380px; | |
| height: 300px; | |
| margin-bottom: 8px; | |
| } | |
| svg.lines { | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| overflow: visible; | |
| } | |
| .line { | |
| stroke: #8b6914; | |
| stroke-width: 2; | |
| opacity: 0.5; | |
| transition: stroke 0.6s, opacity 0.6s, stroke-width 0.6s; | |
| } | |
| .line.agree { stroke: var(--green); opacity: 1; stroke-width: 3; } | |
| .line.disagree { stroke: var(--red); opacity: 1; stroke-width: 3; } | |
| /* Score labels on lines */ | |
| .line-score { | |
| position: absolute; | |
| font-size: 0.65rem; | |
| font-family: monospace; | |
| opacity: 0; | |
| transition: opacity 0.5s, color 0.5s; | |
| pointer-events: none; | |
| } | |
| .line-score.visible { opacity: 0.8; } | |
| .line-score.agree { color: var(--green); } | |
| .line-score.disagree { color: var(--red); } | |
| #score-ab { top: 160px; left: 90px; } | |
| #score-bc { bottom: 20px; left: 50%; transform: translateX(-50%); } | |
| #score-ac { top: 160px; right: 90px; } | |
| /* ─── NODES ─── */ | |
| .node { | |
| position: absolute; | |
| width: var(--node-size); | |
| height: var(--node-size); | |
| border-radius: 50%; | |
| border: 2px solid #8b6914; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: default; | |
| transition: box-shadow 0.5s, border-color 0.5s, opacity 0.4s; | |
| background: #ffffff; | |
| opacity: 0.8; | |
| } | |
| .node.active { opacity: 1; border-color: var(--gold); } | |
| .node.loading { animation: pulse 1.2s ease-in-out infinite; opacity: 1; } | |
| .node.contrarian { box-shadow: 0 0 24px rgba(232,96,96,0.4); border-color: var(--red); } | |
| .node.anchor { box-shadow: 0 0 30px var(--gold-glow); border-color: var(--gold-bright); } | |
| .node-label { | |
| font-size: 0.7rem; | |
| letter-spacing: 0.15em; | |
| text-transform: uppercase; | |
| color: #8b6914; | |
| margin-bottom: 4px; | |
| font-weight: bold; | |
| } | |
| .node-text { | |
| font-size: 0.7rem; | |
| color: #1a1a1a; | |
| text-align: center; | |
| padding: 0 6px; | |
| line-height: 1.4; | |
| max-height: 72px; | |
| overflow: hidden; | |
| opacity: 1; | |
| } | |
| #node-a { top: 0; left: 50%; transform: translateX(-50%); } | |
| #node-b { bottom: 0; left: 8%; } | |
| #node-c { bottom: 0; right: 8%; } | |
| @keyframes pulse { | |
| 0%, 100% { box-shadow: 0 0 6px var(--gold-dim); } | |
| 50% { box-shadow: 0 0 22px var(--gold-glow); } | |
| } | |
| /* ─── PROCESS LOG — the magic ─── */ | |
| .process-log { | |
| width: 380px; | |
| min-height: 60px; | |
| margin-bottom: 12px; | |
| padding: 0 4px; | |
| } | |
| .step { | |
| font-size: 0.72rem; | |
| line-height: 1.6; | |
| color: var(--dim); | |
| opacity: 0; | |
| transition: opacity 0.5s; | |
| padding: 2px 0; | |
| } | |
| .step.visible { opacity: 1; } | |
| .step em { font-style: normal; color: var(--gold-bright); } | |
| .step .red { color: var(--red); } | |
| .step .green { color: var(--green); } | |
| .step strong { font-weight: normal; color: var(--text); } | |
| /* ─── CONSENSUS ─── */ | |
| .consensus { | |
| max-width: 400px; | |
| text-align: center; | |
| font-size: 1rem; | |
| color: var(--gold-bright); | |
| opacity: 0; | |
| transition: opacity 0.6s; | |
| line-height: 1.6; | |
| padding: 8px 16px; | |
| margin-bottom: 16px; | |
| border-left: 2px solid var(--gold-dim); | |
| border-right: 2px solid var(--gold-dim); | |
| } | |
| .consensus.visible { opacity: 1; } | |
| /* ─── INPUT ─── */ | |
| .input-wrap { | |
| width: 380px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .prompt-row { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| input#prompt { | |
| flex: 1; | |
| background: #f8f6f0; | |
| border: 1px solid #8b6914; | |
| color: #1a1a1a; | |
| font-family: 'Georgia', serif; | |
| font-size: 0.9rem; | |
| padding: 12px 16px; | |
| border-radius: 4px; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| input#prompt:focus { border-color: var(--gold); } | |
| input#prompt::placeholder { color: rgba(0,0,0,0.35); } | |
| button#submit { | |
| background: rgba(212,182,90,0.1); | |
| border: 1px solid rgba(212,182,90,0.3); | |
| color: var(--gold-bright); | |
| padding: 12px 20px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-family: 'Georgia', serif; | |
| font-size: 0.85rem; | |
| letter-spacing: 0.06em; | |
| transition: background 0.2s; | |
| } | |
| button#submit:hover { background: rgba(212,182,90,0.2); } | |
| button#submit:disabled { opacity: 0.3; cursor: not-allowed; } | |
| .usage-badge { | |
| position: fixed; | |
| top: 12px; | |
| right: 16px; | |
| font-size: 0.65rem; | |
| color: var(--dim); | |
| letter-spacing: 0.04em; | |
| } | |
| .usage-badge a { color: var(--gold); text-decoration: none; } | |
| .footer-link { | |
| margin-top: 16px; | |
| font-size: 0.7rem; | |
| } | |
| .footer-link a { color: var(--gold); opacity: 0.4; text-decoration: none; letter-spacing: 0.1em; } | |
| .footer-link a:hover { opacity: 0.8; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="usage-badge" id="usage-badge">42/42 free · <a href="https://nova.indahl.ai">nova.indahl.ai</a></div> | |
| <h1>Nova Triangle</h1> | |
| <p class="tagline">Three models argue. You see the disagreement. That's the product.</p> | |
| <div class="triangle-wrap"> | |
| <svg class="lines"> | |
| <line id="line-ab" class="line" x1="190" y1="50" x2="80" y2="260" /> | |
| <line id="line-bc" class="line" x1="80" y1="260" x2="300" y2="260" /> | |
| <line id="line-ac" class="line" x1="190" y1="50" x2="300" y2="260" /> | |
| </svg> | |
| <span class="line-score" id="score-ab"></span> | |
| <span class="line-score" id="score-bc"></span> | |
| <span class="line-score" id="score-ac"></span> | |
| <div class="node" id="node-a"> | |
| <span class="node-label">Precise</span> | |
| <span class="node-text" id="text-a"></span> | |
| </div> | |
| <div class="node" id="node-b"> | |
| <span class="node-label">Balanced</span> | |
| <span class="node-text" id="text-b"></span> | |
| </div> | |
| <div class="node" id="node-c"> | |
| <span class="node-label">Challenger</span> | |
| <span class="node-text" id="text-c"></span> | |
| </div> | |
| </div> | |
| <!-- Step-by-step narration --> | |
| <div class="process-log" id="log"></div> | |
| <!-- Final answer --> | |
| <div class="consensus" id="consensus"></div> | |
| <!-- Input --> | |
| <div class="input-wrap"> | |
| <div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:8px"> | |
| <button onclick="tryExample('What is gravity?')" style="background:none;border:1px solid #8b6914;color:#8b6914;font-family:Georgia,serif;font-size:0.7rem;padding:6px 12px;border-radius:3px;cursor:pointer;opacity:0.6">What is gravity?</button> | |
| <button onclick="tryExample('Is AI conscious?')" style="background:none;border:1px solid #8b6914;color:#8b6914;font-family:Georgia,serif;font-size:0.7rem;padding:6px 12px;border-radius:3px;cursor:pointer;opacity:0.6">Is AI conscious?</button> | |
| <button onclick="tryExample('How do vaccines work?')" style="background:none;border:1px solid #8b6914;color:#8b6914;font-family:Georgia,serif;font-size:0.7rem;padding:6px 12px;border-radius:3px;cursor:pointer;opacity:0.6">How do vaccines work?</button> | |
| </div> | |
| <div class="prompt-row" id="prompt-row"> | |
| <input id="prompt" type="text" placeholder="Ask something..." autocomplete="off" /> | |
| <button id="submit" onclick="submit()">Triangulate</button> | |
| </div> | |
| </div> | |
| <div class="footer-link"><a href="https://nova.indahl.ai">pip install nova-triangle</a></div> | |
| <script> | |
| let totalCorrections = 0; | |
| let running = false; | |
| const DAILY_LIMIT = 42; | |
| // Pre-computed examples — the demo ALWAYS works | |
| const EXAMPLES = { | |
| "What is gravity?": { | |
| resp_a: "Gravity is the fundamental force of attraction between objects with mass. It keeps planets in orbit and holds you to the ground.", | |
| resp_b: "Gravity is both a force and a curvature of spacetime, depending on your framework. Newton described it as attraction between masses, while Einstein reframed it as the geometry of spacetime shaped by energy and mass. Both models make accurate predictions at different scales.", | |
| resp_c: "The question assumes gravity is a 'what' — a thing. But gravity might be emergent, not fundamental. We still can't reconcile it with quantum mechanics, which means we don't fully know what it is.", | |
| ag_ab: 0.35, ag_bc: 0.12, ag_ac: 0.18, anchor: "A", contrarian: "C", corrections: 2, | |
| consensus: "Gravity is the fundamental force of attraction between objects with mass. It keeps planets in orbit and holds you to the ground." | |
| }, | |
| "Is AI conscious?": { | |
| resp_a: "No. Current AI systems process patterns in data without subjective experience. Consciousness requires qualities we haven't defined precisely enough to engineer.", | |
| resp_b: "This depends entirely on how you define consciousness. If it's information processing, some might argue AI has a primitive form. If it requires subjective experience or qualia, then no current system qualifies. The honest answer is we don't have a testable definition yet.", | |
| resp_c: "The question is backwards. We can't even prove other humans are conscious — we assume it. Asking if AI is conscious reveals more about our measurement problem than about AI.", | |
| ag_ab: 0.22, ag_bc: 0.08, ag_ac: 0.15, anchor: "B", contrarian: "C", corrections: 3, | |
| consensus: "This depends entirely on how you define consciousness. If it's information processing, some might argue AI has a primitive form. If it requires subjective experience or qualia, then no current system qualifies. The honest answer is we don't have a testable definition yet." | |
| }, | |
| "How do vaccines work?": { | |
| resp_a: "Vaccines introduce a weakened or inactive form of a pathogen to train your immune system. Your body produces antibodies that remember the threat, providing faster defense on future exposure.", | |
| resp_b: "Vaccines work by mimicking infection without causing disease. They can use weakened viruses, protein fragments, or mRNA instructions. The immune system responds by creating memory cells — B cells for antibodies and T cells for killing infected cells. This memory is what provides lasting protection.", | |
| resp_c: "Vaccines exploit a quirk of adaptive immunity — the fact that immune memory is long-lived but initial response is slow. Without that biological asymmetry, vaccines wouldn't work at all.", | |
| ag_ab: 0.42, ag_bc: 0.19, ag_ac: 0.28, anchor: "A", contrarian: "C", corrections: 1, | |
| consensus: "Vaccines introduce a weakened or inactive form of a pathogen to train your immune system. Your body produces antibodies that remember the threat, providing faster defense on future exposure." | |
| } | |
| }; | |
| function getUsage() { | |
| const today = new Date().toDateString(); | |
| const stored = JSON.parse(localStorage.getItem('nt_usage') || '{}'); | |
| if (stored.day !== today) { stored.day = today; stored.count = 0; } | |
| return stored; | |
| } | |
| function saveUsage(s) { localStorage.setItem('nt_usage', JSON.stringify(s)); } | |
| function updateBadge() { | |
| const s = getUsage(); | |
| const r = Math.max(0, DAILY_LIMIT - s.count); | |
| const b = document.getElementById('usage-badge'); | |
| const p = document.getElementById('prompt-row'); | |
| if (r === 0) { | |
| b.innerHTML = '0/42 · <a href="https://nova.indahl.ai">Get more →</a>'; | |
| p.innerHTML = '<div style="text-align:center;padding:8px"><a href="https://nova.indahl.ai" style="color:var(--gold);font-size:0.85rem;text-decoration:none;border-bottom:1px solid var(--gold-dim)">Install Nova Triangle — 42 free calls/day →</a></div>'; | |
| } else { | |
| b.innerHTML = r + '/42 free · <a href="https://nova.indahl.ai">nova.indahl.ai</a>'; | |
| } | |
| } | |
| function incUsage() { const s = getUsage(); s.count++; saveUsage(s); updateBadge(); } | |
| updateBadge(); | |
| function reset() { | |
| ["a","b","c"].forEach(n => { | |
| document.getElementById("node-" + n).className = "node"; | |
| document.getElementById("text-" + n).textContent = ""; | |
| }); | |
| ["line-ab","line-bc","line-ac"].forEach(id => document.getElementById(id).className = "line"); | |
| ["score-ab","score-bc","score-ac"].forEach(id => { const e = document.getElementById(id); e.textContent = ""; e.className = "line-score"; }); | |
| document.getElementById("consensus").textContent = ""; | |
| document.getElementById("consensus").classList.remove("visible"); | |
| document.getElementById("log").innerHTML = ""; | |
| } | |
| function addStep(html) { | |
| const div = document.createElement('div'); | |
| div.className = 'step'; | |
| div.innerHTML = html; | |
| document.getElementById('log').appendChild(div); | |
| requestAnimationFrame(() => div.classList.add('visible')); | |
| return div; | |
| } | |
| function setScore(id, score) { | |
| const el = document.getElementById(id); | |
| const pct = Math.round(score * 100); | |
| el.textContent = pct + '%'; | |
| el.classList.add('visible'); | |
| el.classList.add(score < 0.25 ? 'disagree' : 'agree'); | |
| } | |
| const HF_API = "https://api-inference.huggingface.co/models/HuggingFaceTB/SmolLM2-1.7B-Instruct"; | |
| const SYSTEMS = { | |
| A: "Answer in 1-2 sentences. Be direct and factual. No hedging.", | |
| B: "Answer in 2-3 sentences. Consider multiple angles and give a balanced view.", | |
| C: "You are a skeptic. Challenge assumptions in the question. Answer in 1-2 sentences.", | |
| }; | |
| const TEMPS = { A: 0.3, B: 0.7, C: 1.05 }; | |
| async function hfGenerate(system, prompt, temp) { | |
| const input = "<|system|>\n" + system + "\n<|user|>\n" + prompt + "\n<|assistant|>\n"; | |
| const res = await fetch(HF_API, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ inputs: input, parameters: { max_new_tokens: 120, temperature: temp, do_sample: true, return_full_text: false } }) | |
| }); | |
| if (!res.ok) throw new Error("HF " + res.status); | |
| const data = await res.json(); | |
| return (data[0]?.generated_text || "").trim() || "No response"; | |
| } | |
| function sim(a, b) { | |
| const wa = new Set(a.toLowerCase().split(/\s+/)); | |
| const wb = new Set(b.toLowerCase().split(/\s+/)); | |
| if (!wa.size || !wb.size) return 0; | |
| let inter = 0; wa.forEach(w => { if (wb.has(w)) inter++; }); | |
| return inter / new Set([...wa, ...wb]).size; | |
| } | |
| async function callBackend(prompt) { | |
| // Check pre-computed examples first | |
| const example = EXAMPLES[prompt]; | |
| if (example) { | |
| await new Promise(r => setTimeout(r, 1500)); // simulate thinking | |
| return example; | |
| } | |
| // Try live inference from browser | |
| try { | |
| const [resp_a, resp_b, resp_c] = await Promise.all([ | |
| hfGenerate(SYSTEMS.A, prompt, TEMPS.A), | |
| hfGenerate(SYSTEMS.B, prompt, TEMPS.B), | |
| hfGenerate(SYSTEMS.C, prompt, TEMPS.C), | |
| ]); | |
| if (resp_a.startsWith('[') || resp_b.startsWith('[') || resp_c.startsWith('[')) throw new Error('bad response'); | |
| const ag_ab = sim(resp_a, resp_b); | |
| const ag_bc = sim(resp_b, resp_c); | |
| const ag_ac = sim(resp_a, resp_c); | |
| const avg = { A: (ag_ab+ag_ac)/2, B: (ag_ab+ag_bc)/2, C: (ag_bc+ag_ac)/2 }; | |
| const anchor = Object.entries(avg).sort((a,b) => b[1]-a[1])[0][0]; | |
| const contrarian = Object.entries(avg).sort((a,b) => a[1]-b[1])[0][0]; | |
| const resps = { A: resp_a, B: resp_b, C: resp_c }; | |
| const corrections = [ag_ab, ag_bc, ag_ac].filter(s => s < 0.25).length; | |
| return { resp_a, resp_b, resp_c, ag_ab, ag_bc, ag_ac, anchor, contrarian, consensus: resps[anchor], corrections }; | |
| } catch(e) { | |
| return { error: "Model is warming up. Try one of these: What is gravity? · Is AI conscious? · How do vaccines work?" }; | |
| } | |
| } | |
| async function submit() { | |
| if (running) return; | |
| const promptEl = document.getElementById("prompt"); | |
| const prompt = promptEl.value.trim(); | |
| if (!prompt) return; | |
| if (getUsage().count >= DAILY_LIMIT) { updateBadge(); return; } | |
| incUsage(); | |
| running = true; | |
| document.getElementById("submit").disabled = true; | |
| reset(); | |
| // Step 1: Three models start thinking | |
| addStep('Sending to <em>three models</em> simultaneously...'); | |
| ["a","b","c"].forEach(n => document.getElementById("node-" + n).classList.add("loading")); | |
| const resultPromise = callBackend(prompt); | |
| await new Promise(r => setTimeout(r, 800)); | |
| addStep('Each model has a different personality and temperature.'); | |
| const data = await resultPromise; | |
| if (data.error) { | |
| addStep('<span class="red">Error: ' + data.error + '</span>'); | |
| running = false; | |
| document.getElementById("submit").disabled = false; | |
| return; | |
| } | |
| // Step 2: Reveal responses one by one | |
| const labels = { a: 'Precise', b: 'Balanced', c: 'Challenger' }; | |
| const resps = { a: data.resp_a, b: data.resp_b, c: data.resp_c }; | |
| const truncate = (s) => s.length > 60 ? s.slice(0, 57) + '...' : s; | |
| for (const n of ['a', 'b', 'c']) { | |
| await new Promise(r => setTimeout(r, 400)); | |
| const node = document.getElementById("node-" + n); | |
| node.classList.remove("loading"); | |
| node.classList.add("active"); | |
| document.getElementById("text-" + n).textContent = resps[n]; | |
| addStep('<em>' + labels[n] + ':</em> <strong>"' + truncate(resps[n]) + '"</strong>'); | |
| } | |
| // Step 3: Compare — show the strings | |
| await new Promise(r => setTimeout(r, 500)); | |
| addStep('Comparing responses pairwise...'); | |
| await new Promise(r => setTimeout(r, 300)); | |
| const pairs = [ | |
| ['ab', data.ag_ab, 'Precise', 'Balanced'], | |
| ['bc', data.ag_bc, 'Balanced', 'Challenger'], | |
| ['ac', data.ag_ac, 'Precise', 'Challenger'], | |
| ]; | |
| for (const [key, score, a, b] of pairs) { | |
| await new Promise(r => setTimeout(r, 250)); | |
| const lineEl = document.getElementById("line-" + key); | |
| const agree = score >= 0.25; | |
| lineEl.className = "line " + (agree ? "agree" : "disagree"); | |
| setScore("score-" + key, score); | |
| const pct = Math.round(score * 100); | |
| addStep(a + ' ↔ ' + b + ': ' + (agree | |
| ? '<span class="green">' + pct + '% agreement</span>' | |
| : '<span class="red">' + pct + '% — disagreement caught</span>')); | |
| } | |
| // Step 4: Corrections | |
| await new Promise(r => setTimeout(r, 400)); | |
| totalCorrections += data.corrections; | |
| if (data.corrections > 0) { | |
| addStep('<span class="red">' + data.corrections + ' correction' + (data.corrections > 1 ? 's' : '') + ' caught</span> — a single model would have shipped this unchecked.'); | |
| } else { | |
| addStep('<span class="green">All three agreed</span> — high confidence.'); | |
| } | |
| // Step 5: Consensus | |
| await new Promise(r => setTimeout(r, 400)); | |
| const anchor = data.anchor.toLowerCase(); | |
| document.getElementById("node-" + anchor).classList.add("anchor"); | |
| addStep('Consensus from <em>' + labels[anchor] + '</em> (highest internal agreement):'); | |
| const cons = document.getElementById("consensus"); | |
| cons.textContent = '\u201c' + data.consensus + '\u201d'; | |
| cons.classList.add("visible"); | |
| setTimeout(() => { | |
| ["a","b","c"].forEach(n => document.getElementById("node-" + n).classList.remove("anchor")); | |
| }, 2000); | |
| running = false; | |
| document.getElementById("submit").disabled = false; | |
| } | |
| function tryExample(q) { | |
| document.getElementById("prompt").value = q; | |
| submit(); | |
| } | |
| document.getElementById("prompt").addEventListener("keydown", e => { | |
| if (e.key === "Enter") submit(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |