Spaces:
Paused
Paused
File size: 7,073 Bytes
a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 a875dec b814c28 22a56cb b814c28 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | const get = id => document.getElementById(id);
const percent = p => `${(p * 100).toLocaleString('en', { maximumFractionDigits: 1 })}%`;
let models = [], record = null, revision = 0, active = null;
function panel(name) {
get('output').hidden = !name;
for (const id of ['loading', 'result', 'error-panel']) get(id).hidden = id !== name;
get('output').setAttribute('aria-busy', String(name === 'loading'));
}
function invalidate() { revision++; record = null; active?.abort(); panel(null); get('copy-status').textContent = ''; }
function error(message, cost = '') { get('error').textContent = message; get('error-cost').textContent = cost; panel('error-panel'); }
function pricing() {
const model = models.find(m => m.id === get('model').value);
get('model-pricing').textContent = model ? `$${model.rates.input.toFixed(2)} input / $${model.rates.output.toFixed(2)} output per million tokens.` : '';
}
async function loadModels() {
get('model').disabled = true; get('forecast-submit').disabled = true; get('reload-models').hidden = true;
try {
const response = await fetch('/api/models', { signal: AbortSignal.timeout(25000) });
const data = await response.json();
if (!response.ok || !Array.isArray(data.models) || !data.models.length) throw new Error('Models are unavailable. Please retry.');
models = data.models; get('model').replaceChildren();
for (const m of models) { const option = document.createElement('option'); option.value = m.id; option.textContent = `${m.name} · ${m.tier}`; get('model').append(option); }
get('model').disabled = false; get('forecast-submit').disabled = false; pricing();
} catch { get('model-pricing').textContent = 'Couldn’t load the model list.'; get('reload-models').hidden = false; }
}
get('reload-models').addEventListener('click', loadModels);
get('model').addEventListener('change', () => { invalidate(); pricing(); });
get('forget-key').addEventListener('click', () => { get('api-key').value = ''; invalidate(); get('api-key').focus(); });
window.addEventListener('pagehide', () => { get('api-key').value = ''; active?.abort(); });
get('resolution-date').min = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
for (const field of get('forecast-form').querySelectorAll('input, textarea, select')) field.addEventListener('input', invalidate);
const examples = {
space: ['Will SpaceX land an uncrewed Starship on Mars before 2030?', '2029-12-31'],
energy: ['Will a fusion power plant supply electricity to a public grid before 2035?', '2034-12-31'],
policy: ['Will the United States introduce a federal carbon tax before 2030?', '2029-12-31'],
};
for (const button of document.querySelectorAll('[data-example]')) button.addEventListener('click', () => {
const [question, date] = examples[button.dataset.example]; get('forecast-question').value = question; get('resolution-date').value = date; invalidate(); get('forecast-question').focus();
});
function list(id, values) {
const items = Array.isArray(values) ? values.slice(0, 8) : [];
get(id).replaceChildren(); get(`${id}-section`).hidden = !items.length;
for (const value of items) { const li = document.createElement('li'); li.textContent = typeof value === 'string' ? value : JSON.stringify(value); get(id).append(li); }
}
function explanation(answer) {
// Provider text stays text: never interpret its HTML or execute links/scripts.
const prose = answer.replace(/```(?:vaticinus-forecast|json)\s*[\s\S]*?```/gi, '').replace(/^#{1,6}\s+/gm, '').replace(/\*\*([^*]+)\*\*/g, '$1').trim();
get('explanation').replaceChildren();
for (const text of prose.split(/\n\s*\n/).filter(Boolean)) { const p = document.createElement('p'); p.textContent = text; get('explanation').append(p); }
}
get('cancel').addEventListener('click', () => active?.abort());
get('forecast-form').addEventListener('submit', async event => {
event.preventDefault();
if (active) return;
invalidate(); const current = revision, controller = new AbortController(); active = controller;
get('forecast-submit').disabled = true; get('cancel').hidden = false; panel('loading');
const started = Date.now();
const update = () => { get('loading-status').textContent = `Generating forecast… ${Math.floor((Date.now() - started) / 1000)}s`; };
update(); const timer = setInterval(update, 1000);
try {
const response = await fetch('/api/forecast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal,
body: JSON.stringify({ question: get('forecast-question').value, resolutionDate: get('resolution-date').value, evidence: get('evidence').value, apiKey: get('api-key').value.trim(), model: get('model').value }) });
const data = await response.json(); if (current !== revision) return;
if (!response.ok) throw new Error(data.error || 'The request failed. Please try again.');
const { run, cost } = data;
if (run.status !== 'issued' || typeof run.probability !== 'number' || !Number.isFinite(run.probability) || run.probability < 0 || run.probability > 1) {
error(data.message || 'The model didn’t return a valid forecast. Try a more specific question or another model.', cost.calls ? 'The provider may have charged for this attempt. Check your OpenRouter activity.' : 'No model call was made.'); return;
}
record = data; get('result-question').textContent = run.question.question;
get('probability').textContent = percent(run.probability); get('probability-no').textContent = percent(1 - run.probability);
const name = models.find(m => m.id === run.model)?.name || run.model;
get('result-meta').textContent = `${name} · Deadline ${run.question.resolution_date} · ${new Date(run.created_at).toLocaleDateString()}`;
explanation(run.answer); list('assumptions', run.spec?.assumptions); list('cruxes', run.spec?.kill_criteria); panel('result');
} catch (e) { if (current === revision) error(e.name === 'AbortError' ? 'Request cancelled.' : e.message, 'If processing started, OpenRouter may still charge for it.'); }
finally { clearInterval(timer); if (active === controller) { active = null; get('cancel').hidden = true; get('forecast-submit').disabled = !models.length; get('output').setAttribute('aria-busy', 'false'); } }
});
get('copy-result').addEventListener('click', async () => {
if (!record) return;
try { await navigator.clipboard.writeText(`${record.run.question.question}\nYes: ${percent(record.run.probability)} · No: ${percent(1 - record.run.probability)}\n${get('explanation').textContent}\nhttps://vaticinus.com`); get('copy-status').textContent = 'Copied.'; }
catch { get('copy-status').textContent = 'Clipboard unavailable. Use Download.'; }
});
get('download').addEventListener('click', () => {
if (!record) return;
const url = URL.createObjectURL(new Blob([JSON.stringify(record, null, 2) + '\n'], { type: 'application/json' }));
const a = document.createElement('a'); a.href = url; a.download = 'vaticinus-forecast.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
});
loadModels();
|