quill/oracle Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files main
181 lines · 7.6 KB · ~2.2k tok Raw History
1
/**
2
 * Every page, server-rendered, script-free. The oracle speaks in a serif and
3
 * keeps its bookkeeping in a sans — the voice and the ledger should not look
4
 * like the same thing.
5
 */
6
7
import { calibrate, verdictOnItself, type Calibration } from "./oracle.ts";
8
import type { Filter, Row } from "./db.ts";
9
10
export const escape = (s: string): string =>
11
  s.replace(
12
    /[&<>"']/g,
13
    (c) =>
14
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c] ??
15
      c,
16
  );
17
18
const STYLE = `
19
  :root {
20
    color-scheme: light dark;
21
    --ink: light-dark(#1b1a17, #ece7dd);
22
    --dim: light-dark(#6b665c, #9a9387);
23
    --rule: light-dark(#1b1a1722, #ece7dd22);
24
    --ground: light-dark(#faf7f1, #16151a);
25
    font: 17px/1.6 system-ui, sans-serif;
26
  }
27
  * { box-sizing: border-box; }
28
  body { max-width: 44rem; margin: 0 auto; padding: 2rem 1rem 4rem; background: var(--ground); color: var(--ink); }
29
  a { color: inherit; }
30
  h1 { font: 600 1.5rem/1.2 ui-serif, Georgia, serif; letter-spacing: .02em; margin: 0; }
31
  h1 a { text-decoration: none; }
32
  .sub { color: var(--dim); margin: .25rem 0 2rem; font-size: .95rem; }
33
  .stance { font: 400 1.6rem/1.35 ui-serif, Georgia, serif; margin: .5rem 0 1rem; }
34
  .auspice { font: italic 400 1.05rem/1.5 ui-serif, Georgia, serif; color: var(--dim); }
35
  .label { font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--dim); }
36
  form.ask { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 2rem; }
37
  input[type=text] { flex: 1 1 14rem; min-height: 48px; padding: .7rem .9rem; font: inherit;
38
    color: var(--ink); background: transparent; border: 1px solid var(--rule); border-radius: .6rem; }
39
  button { min-height: 48px; padding: .7rem 1.1rem; font: inherit; cursor: pointer;
40
    border: 1px solid var(--rule); border-radius: .6rem; background: var(--ink); color: var(--ground); }
41
  button.quiet { background: transparent; color: var(--ink); }
42
  .meter { height: 6px; border-radius: 999px; background: var(--rule); overflow: hidden; margin: .4rem 0 1rem; }
43
  .meter div { height: 100%; background: var(--ink); }
44
  .card { border-top: 1px solid var(--rule); padding: 1.1rem 0; }
45
  .card p { margin: .3rem 0; }
46
  .q { font-weight: 600; }
47
  .row { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
48
  .tag { font-size: .78rem; padding: .15rem .5rem; border: 1px solid var(--rule); border-radius: 999px; color: var(--dim); }
49
  .tag.right { color: light-dark(#166534, #86efac); }
50
  .tag.wrong { color: light-dark(#9f1239, #fda4af); }
51
  footer { margin-top: 3rem; color: var(--dim); font-size: .85rem; border-top: 1px solid var(--rule); padding-top: 1rem; }
52
  nav { display: flex; gap: .75rem; flex-wrap: wrap; margin-bottom: 1.5rem; font-size: .9rem; }
53
  ul { padding: 0; margin: 0; list-style: none; }
54
`;
55
56
interface Chrome {
57
  env: string;
58
  pull: string | undefined;
59
  commit: string;
60
  ssh: string | undefined;
61
}
62
63
function where(chrome: Chrome): string {
64
  return chrome.env === "preview"
65
    ? `preview of pull #${chrome.pull ?? "?"}`
66
    : chrome.env;
67
}
68
69
function layout(chrome: Chrome, title: string, body: string): string {
70
  return `<!doctype html>
71
<meta charset="utf-8">
72
<meta name="viewport" content="width=device-width, initial-scale=1">
73
<title>${escape(title)}</title>
74
<style>${STYLE}</style>
75
<h1><a href="/">the oracle</a></h1>
76
<p class="sub">Ask once. It answers with conviction, never twice differently, and keeps score of how often it was right.</p>
77
${body}
78
<footer>
79
  <p><span class="tag">${escape(where(chrome))}</span>${chrome.commit ? ` <span class="tag">@ ${escape(chrome.commit)}</span>` : ""}</p>
80
  ${chrome.ssh ? `<p>Same ledger in a terminal: <code>${escape(chrome.ssh)}</code></p>` : ""}
81
  <p>Verdicts are derived from the question itself, not from chance — the joke and the honesty are the same mechanism.</p>
82
</footer>
83
`;
84
}
85
86
function meter(confidence: number): string {
87
  return `<div class="meter"><div style="width:${Math.max(0, Math.min(100, confidence))}%"></div></div>`;
88
}
89
90
function outcomeTag(row: Row): string {
91
  if (row.outcome === "right") return `<span class="tag right">judged right</span>`;
92
  if (row.outcome === "wrong") return `<span class="tag wrong">judged wrong</span>`;
93
  return `<span class="tag">unjudged</span>`;
94
}
95
96
function card(row: Row): string {
97
  return `<li class="card">
98
  <p class="q"><a href="/v/${row.id}">${escape(row.question)}</a></p>
99
  <p>${escape(row.stance)}</p>
100
  <p class="row"><span class="tag">${row.confidence}% sure</span>${outcomeTag(row)}${
101
    row.asked_count > 1 ? `<span class="tag">asked ${row.asked_count}×</span>` : ""
102
  }</p>
103
</li>`;
104
}
105
106
function calibrationBlock(c: Calibration): string {
107
  const line = escape(verdictOnItself(c));
108
  if (c.accuracy === undefined) return `<p class="label">its record</p><p>${line}</p>`;
109
  return `<p class="label">its record</p>
110
<p>${line}</p>
111
${meter(c.accuracy)}
112
<p class="row"><span class="tag">${c.right} right of ${c.judged}</span><span class="tag">claimed ${c.claimed}%</span><span class="tag">${
113
    (c.gap ?? 0) > 0 ? `overconfident by ${c.gap}` : (c.gap ?? 0) < 0 ? `modest by ${-(c.gap ?? 0)}` : "square"
114
  }</span></p>`;
115
}
116
117
export function homePage(
118
  chrome: Chrome,
119
  rows: readonly Row[],
120
  scores: readonly { confidence: number; outcome: string }[],
121
): string {
122
  const recent =
123
    rows.length === 0
124
      ? `<p class="auspice">No one has asked it anything yet. Be the first; it has been waiting.</p>`
125
      : `<p class="label">lately</p><ul>${rows.map(card).join("")}</ul>`;
126
  return layout(
127
    chrome,
128
    "the oracle",
129
    `<form class="ask" method="post" action="/ask">
130
  <input type="text" name="question" placeholder="Should I ship it on Friday?" required maxlength="280" autofocus>
131
  <button>Ask</button>
132
</form>
133
${calibrationBlock(calibrate(scores))}
134
${recent}
135
<nav><a href="/ledger">the whole ledger →</a></nav>`,
136
  );
137
}
138
139
export function verdictPage(chrome: Chrome, row: Row): string {
140
  return layout(
141
    chrome,
142
    `${row.question} — the oracle`,
143
    `<p class="label">you asked</p>
144
<p class="q">${escape(row.question)}</p>
145
<p class="stance">${escape(row.stance)}</p>
146
${meter(row.confidence)}
147
<p class="row"><span class="tag">${row.confidence}% sure</span>${outcomeTag(row)}${
148
      row.asked_count > 1 ? `<span class="tag">asked ${row.asked_count}×</span>` : ""
149
    }<span class="tag">${escape(row.asked_at)}</span></p>
150
<p class="label">the sign it read</p>
151
<p class="auspice">${escape(row.auspice)}</p>
152
<p class="label">how did it turn out?</p>
153
<form class="ask" method="post" action="/v/${row.id}/judge">
154
  <button name="outcome" value="right">It was right</button>
155
  <button class="quiet" name="outcome" value="wrong">It was wrong</button>
156
  ${row.outcome === "unjudged" ? "" : `<button class="quiet" name="outcome" value="unjudged">Take that back</button>`}
157
</form>
158
<nav><a href="/">ask another</a> <a href="/ledger">the ledger</a></nav>`,
159
  );
160
}
161
162
export function ledgerPage(chrome: Chrome, rows: readonly Row[], filter: Filter): string {
163
  const tab = (f: Filter, label: string): string =>
164
    f === filter ? `<strong>${label}</strong>` : `<a href="/ledger?show=${f}">${label}</a>`;
165
  return layout(
166
    chrome,
167
    "the ledger — the oracle",
168
    `<nav>${tab("all", "all")} ${tab("unjudged", "unjudged")} ${tab("right", "right")} ${tab("wrong", "wrong")} <a href="/">ask</a></nav>
169
${rows.length === 0 ? `<p class="auspice">Nothing here.</p>` : `<ul>${rows.map(card).join("")}</ul>`}`,
170
  );
171
}
172
173
export function notFoundPage(chrome: Chrome): string {
174
  return layout(
175
    chrome,
176
    "not found — the oracle",
177
    `<p class="stance">There is no such verdict.</p>
178
<p class="auspice">It may never have been asked, or you may have invented the number.</p>
179
<nav><a href="/">ask something real</a></nav>`,
180
  );
181
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts