atris 3.71.3 → 3.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atris/skills/campaigns/SKILL.md +1 -1
- package/bin/atris.js +7 -1
- package/commands/autoland.js +102 -2
- package/commands/campaign.js +86 -13
- package/commands/clean.js +60 -4
- package/commands/gm.js +1273 -7
- package/commands/member.js +28 -3
- package/commands/mission.js +72 -23
- package/commands/sync.js +2 -2
- package/commands/task.js +49 -13
- package/commands/zen.js +183 -0
- package/lib/auto-accept-certified.js +9 -4
- package/lib/campaign-cadence.js +15 -5
- package/lib/campaign-core.js +44 -17
- package/lib/campaign-loop.js +45 -14
- package/lib/campaign-reading.js +14 -1
- package/lib/campaign-rsi.js +28 -11
- package/lib/campaign-schedule.js +124 -0
- package/lib/campaign-store.js +34 -2
- package/lib/campaign-trace.js +40 -14
- package/lib/engine-resource.js +5 -1
- package/lib/gm-bet.js +2219 -0
- package/lib/gm-phone-page.js +1102 -0
- package/lib/gm-phone.js +682 -0
- package/lib/gm-season.js +1162 -0
- package/lib/gm-target.js +292 -0
- package/lib/job-code-verify.js +169 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +105 -14
- package/lib/member-progress.js +2 -2
- package/lib/member-report.js +11 -1
- package/lib/member-self-next-job.js +6 -1
- package/lib/member-up.js +17 -8
- package/lib/merged-review-close.js +206 -0
- package/lib/mission-protected-lane.js +52 -4
- package/lib/qr.js +338 -0
- package/lib/yes-lock.js +530 -0
- package/lib/zen-apply.js +511 -0
- package/lib/zen-workspace.js +121 -0
- package/lib/zen.js +395 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The one page gm phone serves. Plain html, css, and vanilla js, no build
|
|
4
|
+
// step. Every piece of text from the task list goes in with textContent,
|
|
5
|
+
// never as html. The nonce matches the page's content security policy.
|
|
6
|
+
//
|
|
7
|
+
// One theme, the homepage letter's (atrisos-web LetterHome.tsx, dark values
|
|
8
|
+
// from app/config/theme-css.ts .dark): the
|
|
9
|
+
// brand colors, the letter serif, and the lava orb's amber as the only
|
|
10
|
+
// accent. The orb is a port of the landing orb (StippleOrb.tsx, LavaOrb.tsx,
|
|
11
|
+
// orb.module.css, LetterOrb.tsx) with the homepage tap 1:1: the dots spiral
|
|
12
|
+
// into one bright point and the lava orb is born there and grows. Here it
|
|
13
|
+
// then settles small at the top and the card fades up. Tap again and the
|
|
14
|
+
// card dissolves, the lava melts back into the sphere, and the next bet plays.
|
|
15
|
+
// Bets wait in a queue on the Mac, so a tap rarely waits; when it does, the
|
|
16
|
+
// spiral follows the real progress. Once a card is up nothing moves. The decisions only the owner can make sit behind one
|
|
17
|
+
// quiet link at the bottom.
|
|
18
|
+
|
|
19
|
+
function phonePage(nonce) {
|
|
20
|
+
return `<!doctype html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
25
|
+
<meta name="referrer" content="no-referrer">
|
|
26
|
+
<meta name="theme-color" content="#141110">
|
|
27
|
+
<title>front office</title>
|
|
28
|
+
<style nonce="${nonce}">
|
|
29
|
+
:root {
|
|
30
|
+
--bg: #141110;
|
|
31
|
+
--text: #eae3d9;
|
|
32
|
+
--text-2: #a39b92;
|
|
33
|
+
--text-3: #9a9187;
|
|
34
|
+
--line: #3d332d;
|
|
35
|
+
--amber: rgb(255, 140, 66);
|
|
36
|
+
--amber-ink: #1a0f06;
|
|
37
|
+
--serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
}
|
|
40
|
+
* { box-sizing: border-box; }
|
|
41
|
+
html, body { margin: 0; background: var(--bg); color: var(--text); }
|
|
42
|
+
body {
|
|
43
|
+
font: 17px/1.5 var(--serif);
|
|
44
|
+
min-height: 100vh;
|
|
45
|
+
min-height: 100svh;
|
|
46
|
+
-webkit-font-smoothing: antialiased;
|
|
47
|
+
}
|
|
48
|
+
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
|
49
|
+
:focus-visible { outline: 1px solid var(--amber); outline-offset: 4px; }
|
|
50
|
+
[hidden] { display: none !important; }
|
|
51
|
+
|
|
52
|
+
/* ---- the screen: the orb in the middle, one line under it ---- */
|
|
53
|
+
.screen {
|
|
54
|
+
position: relative;
|
|
55
|
+
min-height: 100vh; min-height: 100svh;
|
|
56
|
+
max-width: 34rem; margin: 0 auto;
|
|
57
|
+
padding: max(24px, env(safe-area-inset-top)) 24px max(24px, env(safe-area-inset-bottom));
|
|
58
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
59
|
+
}
|
|
60
|
+
.stage { position: relative; width: min(72vmin, 520px); aspect-ratio: 1 / 1; }
|
|
61
|
+
.stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; touch-action: manipulation; cursor: pointer; }
|
|
62
|
+
.stage canvas:focus-visible { outline-offset: -8%; }
|
|
63
|
+
.line {
|
|
64
|
+
min-height: 3em; margin: 4px 0 0; max-width: 22em; text-align: center;
|
|
65
|
+
color: var(--text-3); font-size: 17px; line-height: 1.5; letter-spacing: -0.005em;
|
|
66
|
+
opacity: 1; transition: opacity 0.28s ease;
|
|
67
|
+
}
|
|
68
|
+
.line.out { opacity: 0; }
|
|
69
|
+
.line.bad { color: var(--text-2); }
|
|
70
|
+
.foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; }
|
|
71
|
+
.quiet { color: var(--text-3); font-size: 15px; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; }
|
|
72
|
+
.foot .quiet + .quiet { margin-left: 18px; }
|
|
73
|
+
.jobs { margin: 12px auto 0; max-width: 24em; text-align: left; color: var(--text-2); font-size: 14px; line-height: 1.35; }
|
|
74
|
+
.jobs p { margin: 0 0 8px; }
|
|
75
|
+
.jobs .meta { display: block; color: var(--text-3); font-size: 13px; }
|
|
76
|
+
|
|
77
|
+
/* the lava orb, LavaOrb variant sm: three drifting blobs, transform and opacity only */
|
|
78
|
+
.lava {
|
|
79
|
+
position: absolute; left: 0; top: 0; width: 280px; height: 280px; z-index: 5;
|
|
80
|
+
border-radius: 50%; overflow: hidden; pointer-events: none;
|
|
81
|
+
visibility: hidden; opacity: 0;
|
|
82
|
+
background: rgb(20, 18, 16);
|
|
83
|
+
box-shadow: 0 0 40px rgba(255, 140, 66, 0.1);
|
|
84
|
+
filter: saturate(1.3);
|
|
85
|
+
transform-origin: 50% 50%;
|
|
86
|
+
will-change: transform, opacity;
|
|
87
|
+
}
|
|
88
|
+
.lava .tx, .lava .ty { position: absolute; inset: 0; will-change: transform; }
|
|
89
|
+
.lava .blob { position: absolute; will-change: transform; }
|
|
90
|
+
/* one pass only: the lava drifts while it is born, never on a loop */
|
|
91
|
+
.lava.alive .tx { animation: sine-x var(--dur) ease-in-out var(--delay) 1 both; }
|
|
92
|
+
.lava.alive .ty { animation: sine-y var(--dur) ease-in-out var(--delay) 1 both; }
|
|
93
|
+
.lava.alive .blob { animation: spin var(--spin) linear 1 both; }
|
|
94
|
+
.lava.alive .blob.rev { animation-direction: reverse; }
|
|
95
|
+
.lava .b1 { width: 70%; height: 45%; left: 20%; top: 25%; background: rgba(255, 140, 66, 0.55); border-radius: 60% 40% 50% 70% / 50% 60% 40% 50%; filter: blur(24px); }
|
|
96
|
+
.lava .b2 { width: 55%; height: 60%; left: 35%; top: 40%; background: rgba(255, 100, 50, 0.45); border-radius: 40% 60% 70% 30% / 60% 40% 60% 40%; filter: blur(24px); }
|
|
97
|
+
.lava .b3 { width: 50%; height: 40%; left: 10%; top: 50%; background: rgba(255, 180, 100, 0.4); border-radius: 50% 70% 30% 60% / 40% 50% 60% 50%; filter: blur(16px); }
|
|
98
|
+
.lava .core { position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle, rgba(255,232,170,0.95) 0%, rgba(251,150,44,0.6) 30%, rgba(251,130,36,0) 62%); }
|
|
99
|
+
.lava .dim { position: absolute; inset: 0; border-radius: 50%; background: var(--bg); opacity: 0; }
|
|
100
|
+
.lava.tap { pointer-events: auto; cursor: pointer; }
|
|
101
|
+
.lava.still .tx, .lava.still .ty, .lava.still .blob { animation-play-state: paused; }
|
|
102
|
+
@keyframes sine-x { 0%, 100% { transform: translate3d(calc(var(--amp) * -1), 0, 0); } 50% { transform: translate3d(var(--amp), 0, 0); } }
|
|
103
|
+
@keyframes sine-y { 0%, 100% { transform: translate3d(0, calc(var(--amp) * -1), 0); } 50% { transform: translate3d(0, var(--amp), 0); } }
|
|
104
|
+
@keyframes spin { from { transform: rotate(var(--rot0, 0deg)); } to { transform: rotate(calc(var(--rot0, 0deg) + 360deg)); } }
|
|
105
|
+
|
|
106
|
+
/* ---- the card: same dark page, the lava orb small on top ---- */
|
|
107
|
+
body.has-card .screen { justify-content: flex-start; padding-top: calc(max(16px, env(safe-area-inset-top)) + 76px); }
|
|
108
|
+
body.has-card .stage, body.has-card .line { display: none; }
|
|
109
|
+
body.has-card .foot { position: static; margin-top: 28px; }
|
|
110
|
+
.bet { width: 100%; opacity: 0; transform: translateY(20px); transition: opacity 0.45s ease-out, transform 0.55s cubic-bezier(0.22, 1, 0.36, 1); }
|
|
111
|
+
.bet.up { opacity: 1; transform: none; }
|
|
112
|
+
.bet.quick { transition-duration: 0.4s, 0.5s; }
|
|
113
|
+
/* leaving: a soft fade in place, never a cut */
|
|
114
|
+
.bet.leave { opacity: 0; transform: none; transition: opacity 0.4s ease-out; }
|
|
115
|
+
.bet h1 { margin: 0; font-size: clamp(23px, 6.2vw, 30px); line-height: 1.12; font-weight: 700; letter-spacing: -0.03em; text-align: left; }
|
|
116
|
+
.why { margin: 12px 0 0; color: var(--text-2); font-size: 15px; line-height: 1.4; }
|
|
117
|
+
.when { margin: 12px 0 0; color: var(--text-3); font-size: 14px; line-height: 1.4; }
|
|
118
|
+
.steps { margin: 12px 0 0; padding: 10px 0 0; list-style: none; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 6px; counter-reset: step; }
|
|
119
|
+
.steps li { position: relative; padding-left: 22px; counter-increment: step; color: var(--text-2); font-size: 14px; line-height: 1.35; }
|
|
120
|
+
.steps li::before { content: counter(step); position: absolute; left: 0; top: 0; color: var(--text-3); font-variant-numeric: tabular-nums; }
|
|
121
|
+
.tag { display: inline-flex; align-items: center; gap: 5px; margin-right: 8px; font-size: 13px; color: var(--text-3); }
|
|
122
|
+
.tag.you { color: var(--amber); }
|
|
123
|
+
.tag svg { width: 11px; height: 11px; }
|
|
124
|
+
.act { margin: 18px 0 0; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
|
125
|
+
.pill {
|
|
126
|
+
min-width: 12rem; height: 44px; padding: 0 28px; border-radius: 22px;
|
|
127
|
+
background: var(--amber); color: var(--amber-ink); font-size: 17px; font-weight: 600; letter-spacing: -0.01em;
|
|
128
|
+
}
|
|
129
|
+
.pill.armed { background: var(--text); color: var(--bg); }
|
|
130
|
+
.links { color: var(--text-3); font-size: 15px; }
|
|
131
|
+
.links button { color: var(--text-2); text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 44px; padding: 0 6px; }
|
|
132
|
+
.mission { margin: 32px 0 0; padding: 16px 0 0; border-top: 1px solid var(--line); color: var(--text-2); text-align: center; }
|
|
133
|
+
|
|
134
|
+
/* ---- only you: the owner's decisions, same theme, opened from the bottom link ---- */
|
|
135
|
+
.sheet {
|
|
136
|
+
position: fixed; inset: 0; z-index: 20; overflow-y: auto; background: var(--bg);
|
|
137
|
+
padding: max(24px, env(safe-area-inset-top)) 24px max(24px, env(safe-area-inset-bottom));
|
|
138
|
+
}
|
|
139
|
+
.sheet .inner { max-width: 34rem; margin: 0 auto; display: flex; flex-direction: column; gap: 22px; }
|
|
140
|
+
.sheet .head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
141
|
+
.sheet .head h2 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
|
|
142
|
+
.strip { color: var(--text-2); font-size: 16px; }
|
|
143
|
+
.bar { height: 3px; background: var(--line); border-radius: 2px; overflow: hidden; margin-top: 10px; }
|
|
144
|
+
.bar > div { height: 100%; background: var(--amber); width: 0; }
|
|
145
|
+
.card { border-top: 1px solid var(--line); padding-top: 20px; display: flex; flex-direction: column; gap: 14px; }
|
|
146
|
+
.count { color: var(--text-3); font-size: 13px; }
|
|
147
|
+
.say { font-size: 20px; line-height: 1.4; margin: 0; }
|
|
148
|
+
.choice { width: 100%; min-height: 52px; text-align: left; padding: 12px 16px; border-radius: 14px; border: 1px solid var(--line); color: var(--text); }
|
|
149
|
+
.choice .effect { display: block; font-size: 14px; color: var(--text-3); }
|
|
150
|
+
.choice.main { background: var(--amber); color: var(--amber-ink); border-color: var(--amber); font-weight: 600; }
|
|
151
|
+
.choice.main .effect { color: var(--amber-ink); opacity: 0.75; font-weight: 400; }
|
|
152
|
+
.choice.armed { background: var(--text); color: var(--bg); border-color: var(--text); }
|
|
153
|
+
.choice.armed .effect { color: var(--bg); opacity: 0.75; }
|
|
154
|
+
.choice.later { border: 0; min-height: 44px; text-align: center; color: var(--text-3); }
|
|
155
|
+
.job { border-top: 1px solid var(--line); padding-top: 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
156
|
+
.job .meta { color: var(--text-3); font-size: 14px; }
|
|
157
|
+
.row { display: flex; gap: 10px; }
|
|
158
|
+
.row .choice { text-align: center; }
|
|
159
|
+
.reply { color: var(--text-2); font-size: 16px; }
|
|
160
|
+
.live { color: var(--text-3); font-size: 14px; }
|
|
161
|
+
.toast {
|
|
162
|
+
position: fixed; left: 16px; right: 16px; bottom: max(16px, env(safe-area-inset-bottom)); z-index: 30;
|
|
163
|
+
background: var(--text); color: var(--bg); border-radius: 14px; padding: 10px 16px;
|
|
164
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
165
|
+
}
|
|
166
|
+
.toast button { min-height: 44px; padding: 0 12px; color: var(--bg); text-decoration: underline; }
|
|
167
|
+
|
|
168
|
+
@media (prefers-reduced-motion: reduce) {
|
|
169
|
+
.lava .tx, .lava .ty, .lava .blob { animation: none !important; }
|
|
170
|
+
.bet { transform: none; transition: opacity 0.2s linear; }
|
|
171
|
+
.line { transition: none; }
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<main class="screen" id="screen">
|
|
177
|
+
<div class="stage" id="stage">
|
|
178
|
+
<canvas id="orb" tabindex="0" role="button" aria-label="the atris orb. tap to think of one new bet for the business"></canvas>
|
|
179
|
+
</div>
|
|
180
|
+
<p class="line" id="line" aria-live="polite">tap to think</p>
|
|
181
|
+
<article class="bet" id="bet" hidden></article>
|
|
182
|
+
<div class="foot" id="foot"><button type="button" class="quiet" id="open-only" hidden></button><button type="button" class="quiet" id="working" hidden aria-expanded="false"></button><div class="jobs" id="jobs" hidden></div></div>
|
|
183
|
+
</main>
|
|
184
|
+
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap for the next bet">
|
|
185
|
+
<div class="tx" style-vars="15%,8.4s,-2.1s"><div class="ty" style-vars="12%,10.1s,-5.1s"><div class="blob b1" spin-vars="36s,0deg"></div></div></div>
|
|
186
|
+
<div class="tx" style-vars="18%,12.6s,-7.2s"><div class="ty" style-vars="15%,7.2s,-4.7s"><div class="blob b2 rev" spin-vars="48s,45deg"></div></div></div>
|
|
187
|
+
<div class="tx" style-vars="12%,7.2s,-6.4s"><div class="ty" style-vars="18%,10.1s,-9.9s"><div class="blob b3" spin-vars="57.6s,90deg"></div></div></div>
|
|
188
|
+
<div class="core" id="lava-core"></div>
|
|
189
|
+
<div class="dim" id="lava-dim"></div>
|
|
190
|
+
</div>
|
|
191
|
+
<section class="sheet" id="sheet" hidden aria-label="only you">
|
|
192
|
+
<div class="inner">
|
|
193
|
+
<div class="head"><h2>only you</h2><button type="button" class="quiet" id="close-only">done</button></div>
|
|
194
|
+
<div><div class="strip" id="target">loading</div><div class="bar"><div id="bar"></div></div></div>
|
|
195
|
+
<div id="main"></div>
|
|
196
|
+
<div id="reply" class="reply" hidden></div>
|
|
197
|
+
<div class="live" id="live"></div>
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
<div class="toast" id="toast" hidden><span id="toast-text"></span><button id="toast-undo" type="button">undo</button></div>
|
|
201
|
+
<script nonce="${nonce}">
|
|
202
|
+
(function () {
|
|
203
|
+
var token = new URLSearchParams(location.search).get('t') || '';
|
|
204
|
+
var last = null;
|
|
205
|
+
var view = 'card';
|
|
206
|
+
var armed = null;
|
|
207
|
+
var pendingReply = null;
|
|
208
|
+
var undoTimer = null;
|
|
209
|
+
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
210
|
+
|
|
211
|
+
function el(tag, cls, text) {
|
|
212
|
+
var node = document.createElement(tag);
|
|
213
|
+
if (cls) node.className = cls;
|
|
214
|
+
if (text != null) node.textContent = text;
|
|
215
|
+
return node;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function api(path, body) {
|
|
219
|
+
var opts = { headers: { 'x-gm-token': token } };
|
|
220
|
+
if (body) {
|
|
221
|
+
opts.method = 'POST';
|
|
222
|
+
opts.headers['content-type'] = 'application/json';
|
|
223
|
+
opts.body = JSON.stringify(body);
|
|
224
|
+
}
|
|
225
|
+
return fetch(path + (body ? '' : '?t=' + encodeURIComponent(token)), opts)
|
|
226
|
+
.then(function (r) { return r.json().catch(function () { return { ok: false }; }).then(function (j) { j.http = r.status; return j; }); });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- the one line under the orb: fades out, changes, fades in ------------------
|
|
230
|
+
var line = (function () {
|
|
231
|
+
var node = document.getElementById('line');
|
|
232
|
+
var want = null, busy = false;
|
|
233
|
+
function next() {
|
|
234
|
+
if (busy || want == null) return;
|
|
235
|
+
var w = want; want = null;
|
|
236
|
+
if (w.text === node.textContent && w.bad === node.classList.contains('bad')) return;
|
|
237
|
+
if (reduced) { node.textContent = w.text; node.classList.toggle('bad', w.bad); return; }
|
|
238
|
+
busy = true;
|
|
239
|
+
node.classList.add('out');
|
|
240
|
+
setTimeout(function () {
|
|
241
|
+
node.textContent = w.text;
|
|
242
|
+
node.classList.toggle('bad', w.bad);
|
|
243
|
+
node.classList.remove('out');
|
|
244
|
+
// each line stays long enough to read before the next one
|
|
245
|
+
setTimeout(function () { busy = false; next(); }, 650);
|
|
246
|
+
}, 280);
|
|
247
|
+
}
|
|
248
|
+
return { set: function (text, bad) { want = { text: text, bad: Boolean(bad) }; next(); } };
|
|
249
|
+
})();
|
|
250
|
+
|
|
251
|
+
// ---- the orb: StippleOrb (tone dark) and LetterOrb's tap, ported 1:1 -----------
|
|
252
|
+
// StippleOrb: the pull runs per dot over 0.75s after a delay (outer dots
|
|
253
|
+
// first), the bright point arrives from 0.3s to 1.0s, and the lava is born
|
|
254
|
+
// at 1.05s. LetterOrb: the lava grows for GROW_MS from 30px toward the
|
|
255
|
+
// farthest corner (at most MAX_SCALE), with the same ease and smooth curves.
|
|
256
|
+
// Here the grown lava then settles small at the top and the card fades up.
|
|
257
|
+
var orb = (function () {
|
|
258
|
+
var canvas = document.getElementById('orb');
|
|
259
|
+
var lava = document.getElementById('lava');
|
|
260
|
+
var core = document.getElementById('lava-core');
|
|
261
|
+
var dim = document.getElementById('lava-dim');
|
|
262
|
+
var ctx = canvas.getContext('2d');
|
|
263
|
+
var SPRING = 0.07, DAMP = 0.8;
|
|
264
|
+
var GROW_MS = 1050, MAX_SCALE = 3.2;
|
|
265
|
+
var SETTLE_MS = 900;
|
|
266
|
+
// a tap on a card: the card fades for LEAVE_MS while the small orb glides
|
|
267
|
+
// to the centre for GLIDE_MS, then the sphere grows back out of it and
|
|
268
|
+
// holds for BEAT_MS before the next reveal starts from it
|
|
269
|
+
var LEAVE_MS = 400, GLIDE_MS = 520, BEAT_MS = 340, BRIDGE_MS = 340;
|
|
270
|
+
// the fast reveal stays contained: the lava grows to BLOOM of the smaller
|
|
271
|
+
// window side and never washes the page
|
|
272
|
+
var FAST_GROW_MS = 460, FAST_SETTLE_MS = 440, BLOOM = 0.72;
|
|
273
|
+
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
274
|
+
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
275
|
+
// A bet still being made plays the full landing length, and the spiral
|
|
276
|
+
// holds on the real progress until it passes.
|
|
277
|
+
var FAST = 0.4;
|
|
278
|
+
var pace = 1;
|
|
279
|
+
var bridgeAt = 0;
|
|
280
|
+
var REST = 56;
|
|
281
|
+
var dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
282
|
+
var count = window.innerWidth < 768 ? 5500 : 11000;
|
|
283
|
+
var w = 0, h = 0, R = 0, cx = 0, cy = 0;
|
|
284
|
+
var dots = [];
|
|
285
|
+
// idle: the sphere drifts. pull: the dots spiral in by age. card: still.
|
|
286
|
+
var mode = 'idle';
|
|
287
|
+
var angle = 0;
|
|
288
|
+
var age = -1, ageTo = 0, timed = false, fired = false;
|
|
289
|
+
var raf = 0, lastT = 0;
|
|
290
|
+
var onBorn = null;
|
|
291
|
+
var base = 280, origin = { x: 0, y: 0 };
|
|
292
|
+
var light = { x: -0.5, y: -0.6 };
|
|
293
|
+
var lz = Math.sqrt(1 - light.x * light.x - light.y * light.y);
|
|
294
|
+
var ease = function (t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; };
|
|
295
|
+
var smooth = function (a, b, v) { var t = Math.min(1, Math.max(0, (v - a) / (b - a))); return t * t * (3 - 2 * t); };
|
|
296
|
+
|
|
297
|
+
// the lava blobs take their drift numbers from the markup
|
|
298
|
+
Array.prototype.forEach.call(lava.querySelectorAll('[style-vars]'), function (n) {
|
|
299
|
+
var v = n.getAttribute('style-vars').split(',');
|
|
300
|
+
n.style.setProperty('--amp', v[0]); n.style.setProperty('--dur', v[1]); n.style.setProperty('--delay', v[2]);
|
|
301
|
+
});
|
|
302
|
+
Array.prototype.forEach.call(lava.querySelectorAll('[spin-vars]'), function (n) {
|
|
303
|
+
var v = n.getAttribute('spin-vars').split(',');
|
|
304
|
+
n.style.setProperty('--spin', v[0]); n.style.setProperty('--rot0', v[1]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
function measure() {
|
|
308
|
+
w = canvas.clientWidth; h = canvas.clientHeight;
|
|
309
|
+
canvas.width = w * dpr; canvas.height = h * dpr;
|
|
310
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
311
|
+
R = Math.min(w, h) * 0.42; cx = w / 2; cy = h / 2;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function seed() {
|
|
315
|
+
// random points on a sphere (normalised gaussians), so it reads as stipple
|
|
316
|
+
var gauss = function () { return Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()); };
|
|
317
|
+
dots = [];
|
|
318
|
+
for (var i = 0; i < count; i++) {
|
|
319
|
+
var gx = gauss(), gy = gauss(), gz = gauss();
|
|
320
|
+
var n = Math.hypot(gx, gy, gz) || 1;
|
|
321
|
+
dots.push({ x: gx / n, y: gy / n, z: gz / n, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0 });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// StippleOrb startBoom: each dot remembers where it starts, and the outer
|
|
326
|
+
// dots go first. The start is the sphere itself, so a page that opens
|
|
327
|
+
// mid-thought still spirals from the whole orb.
|
|
328
|
+
// fromHere: start from where the dots are now (a quick re-tap, mid re-form)
|
|
329
|
+
function startPull(fromHere) {
|
|
330
|
+
var cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
331
|
+
for (var i = 0; i < dots.length; i++) {
|
|
332
|
+
var d = dots[i];
|
|
333
|
+
if (fromHere) { d.bx = d.sx; d.by = d.sy; } else { d.bx = cx + (d.x * cosA + d.z * sinA) * R; d.by = cy + d.y * R; }
|
|
334
|
+
var len = Math.hypot(d.bx - cx, d.by - cy);
|
|
335
|
+
d.dl = (1 - Math.min(1, len / R)) * 0.28 + Math.random() * 0.05;
|
|
336
|
+
}
|
|
337
|
+
age = 0; ageTo = 0; fired = false;
|
|
338
|
+
mode = 'pull';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function frame(now) {
|
|
342
|
+
var dt = Math.min(0.05, (now - (lastT || now)) / 1000);
|
|
343
|
+
lastT = now;
|
|
344
|
+
ctx.clearRect(0, 0, w, h);
|
|
345
|
+
if (!reduced && mode === 'idle') angle += dt * 0.15;
|
|
346
|
+
if (mode === 'pull') {
|
|
347
|
+
// ready: the age runs on the clock, as on the homepage. Waiting: it
|
|
348
|
+
// follows the real progress and stops short of the point.
|
|
349
|
+
if (timed) age += dt / pace;
|
|
350
|
+
else age += (ageTo - age) * 0.05;
|
|
351
|
+
}
|
|
352
|
+
var cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
353
|
+
for (var i = 0; i < dots.length; i++) {
|
|
354
|
+
var d = dots[i];
|
|
355
|
+
var fade = 1, shrink = 1;
|
|
356
|
+
var rx = d.x * cosA + d.z * sinA;
|
|
357
|
+
var rz = -d.x * sinA + d.z * cosA;
|
|
358
|
+
if (mode === 'idle') {
|
|
359
|
+
d.vx += (cx + rx * R - d.sx) * SPRING;
|
|
360
|
+
d.vy += (cy + d.y * R - d.sy) * SPRING;
|
|
361
|
+
d.vx *= DAMP; d.vy *= DAMP;
|
|
362
|
+
d.sx += d.vx; d.sy += d.vy;
|
|
363
|
+
if (reduced) { d.sx = cx + rx * R; d.sy = cy + d.y * R; }
|
|
364
|
+
} else {
|
|
365
|
+
var t = Math.min(1, Math.max(0, (age - d.dl) / 0.75));
|
|
366
|
+
var e = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
367
|
+
var ox = (d.bx - cx) * (1 - e), oy = (d.by - cy) * (1 - e);
|
|
368
|
+
var turn = e * 1.5;
|
|
369
|
+
d.sx = cx + ox * Math.cos(turn) - oy * Math.sin(turn);
|
|
370
|
+
d.sy = cy + ox * Math.sin(turn) + oy * Math.cos(turn);
|
|
371
|
+
fade = 1 - Math.pow(Math.max(0, (e - 0.8) / 0.2), 1.5);
|
|
372
|
+
shrink = 1 - 0.35 * e;
|
|
373
|
+
}
|
|
374
|
+
// front dots are lit; the back half is a dim ghost. amber in shadow, cream where lit.
|
|
375
|
+
var lit = rx * light.x + d.y * light.y + rz * lz;
|
|
376
|
+
var front = rz > 0;
|
|
377
|
+
var l = Math.max(0, lit);
|
|
378
|
+
var a = (front ? 0.16 + Math.pow(l, 1.3) * 0.84 : 0.05 + l * 0.08) * fade;
|
|
379
|
+
if (a <= 0.003) continue;
|
|
380
|
+
ctx.fillStyle = l > 0.93 ? 'rgba(253, 196, 84, ' + a.toFixed(3) + ')' : 'rgba(251, 130, 36, ' + a.toFixed(3) + ')';
|
|
381
|
+
ctx.beginPath();
|
|
382
|
+
ctx.arc(d.sx, d.sy, d.r * (front ? 0.6 + l * 0.7 : 0.6) * shrink, 0, Math.PI * 2);
|
|
383
|
+
ctx.fill();
|
|
384
|
+
}
|
|
385
|
+
if (mode === 'pull') {
|
|
386
|
+
// where the dots land: one bright point that swells until the lava is born
|
|
387
|
+
var arrived = Math.min(1, Math.max(0, (age - 0.3) / 0.7));
|
|
388
|
+
if (arrived > 0) point(arrived, timed ? 0.88 + 0.12 * Math.sin(age * 5) : 1);
|
|
389
|
+
if (timed && !fired && age > 1.05) { fired = true; grow(); }
|
|
390
|
+
} else if (bridgeAt) {
|
|
391
|
+
// the melted lava's bright point fades as the sphere grows out of it
|
|
392
|
+
var left = 1 - (now - bridgeAt) / BRIDGE_MS;
|
|
393
|
+
if (left > 0) point(left, 1); else bridgeAt = 0;
|
|
394
|
+
}
|
|
395
|
+
if (mode === 'card' || (reduced && mode === 'idle')) { raf = 0; return; }
|
|
396
|
+
raf = requestAnimationFrame(frame);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// one bright point at the centre with its glow, at strength arrived
|
|
400
|
+
function point(arrived, breath) {
|
|
401
|
+
var coreR = (2 + 5 * arrived) * breath;
|
|
402
|
+
var glowR = (10 + 46 * arrived) * breath;
|
|
403
|
+
var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowR);
|
|
404
|
+
g.addColorStop(0, 'rgba(255, 232, 170, ' + (0.9 * arrived).toFixed(3) + ')');
|
|
405
|
+
g.addColorStop(0.35, 'rgba(251, 150, 44, ' + (0.4 * arrived).toFixed(3) + ')');
|
|
406
|
+
g.addColorStop(1, 'rgba(251, 130, 36, 0)');
|
|
407
|
+
ctx.fillStyle = g;
|
|
408
|
+
ctx.beginPath(); ctx.arc(cx, cy, glowR, 0, Math.PI * 2); ctx.fill();
|
|
409
|
+
ctx.fillStyle = 'rgba(255, 244, 214, ' + arrived.toFixed(3) + ')';
|
|
410
|
+
ctx.beginPath(); ctx.arc(cx, cy, coreR, 0, Math.PI * 2); ctx.fill();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function start() { if (!raf) { lastT = 0; raf = requestAnimationFrame(frame); } }
|
|
414
|
+
function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; }
|
|
415
|
+
|
|
416
|
+
function place(s, x, y) { lava.style.transform = 'translate(' + (x - base / 2) + 'px, ' + (y - base / 2) + 'px) scale(' + s + ')'; }
|
|
417
|
+
function restPoint() {
|
|
418
|
+
var top = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-top')) || 0;
|
|
419
|
+
return { x: document.documentElement.clientWidth / 2, y: window.scrollY + Math.max(16, top) + 8 + REST / 2 };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// LetterOrb grow, then the settle: the lava is born at the point, grows
|
|
423
|
+
// toward the farthest corner as the page dims to its own dark, then
|
|
424
|
+
// shrinks back to a small orb at the top. After that nothing moves.
|
|
425
|
+
// A queued bet (the fast pace) blooms contained: the lava grows to BLOOM
|
|
426
|
+
// of the smaller window side, the page never dims, and it settles.
|
|
427
|
+
function grow() {
|
|
428
|
+
var rect = canvas.getBoundingClientRect();
|
|
429
|
+
var contained = pace < 1;
|
|
430
|
+
var growMs = contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
431
|
+
var settleMs = contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
|
|
432
|
+
var vx = rect.left + cx, vy = rect.top + cy;
|
|
433
|
+
var reach = contained
|
|
434
|
+
? BLOOM * Math.min(window.innerWidth, window.innerHeight) / 2
|
|
435
|
+
: Math.hypot(Math.max(vx, window.innerWidth - vx), Math.max(vy, window.innerHeight - vy)) * 1.08;
|
|
436
|
+
base = Math.round(Math.min(1000, Math.max(280, (2 * reach) / MAX_SCALE)));
|
|
437
|
+
var sMax = (2 * reach) / base;
|
|
438
|
+
var sMin = 30 / base;
|
|
439
|
+
var rest = REST / base;
|
|
440
|
+
lava.style.width = base + 'px';
|
|
441
|
+
lava.style.height = base + 'px';
|
|
442
|
+
// fixed while it grows past the screen, so the page never scrolls sideways
|
|
443
|
+
window.scrollTo(0, 0);
|
|
444
|
+
origin = { x: vx, y: vy };
|
|
445
|
+
lava.style.position = 'fixed';
|
|
446
|
+
lava.style.visibility = 'visible';
|
|
447
|
+
lava.classList.remove('still', 'alive', 'tap');
|
|
448
|
+
void lava.offsetWidth;
|
|
449
|
+
lava.classList.add('alive');
|
|
450
|
+
var t0 = performance.now();
|
|
451
|
+
var step = function (now) {
|
|
452
|
+
var t = Math.min(1, (now - t0) / growMs);
|
|
453
|
+
place(sMin + (sMax - sMin) * ease(t), origin.x, origin.y);
|
|
454
|
+
lava.style.opacity = String(smooth(0.02, 0.22, t));
|
|
455
|
+
core.style.opacity = String(1 - smooth(0.05, 0.4, t));
|
|
456
|
+
dim.style.opacity = contained ? '0' : String(smooth(0.88, 1, t));
|
|
457
|
+
if (t < 1) { requestAnimationFrame(step); return; }
|
|
458
|
+
mode = 'card';
|
|
459
|
+
stop();
|
|
460
|
+
ctx.clearRect(0, 0, w, h);
|
|
461
|
+
canvas.style.visibility = 'hidden';
|
|
462
|
+
if (onBorn) onBorn('cover');
|
|
463
|
+
var p = restPoint();
|
|
464
|
+
var s0 = performance.now();
|
|
465
|
+
var settle = function (n2) {
|
|
466
|
+
var u = ease(Math.min(1, (n2 - s0) / settleMs));
|
|
467
|
+
place(sMax + (rest - sMax) * u, origin.x + (p.x - origin.x) * u, origin.y + (p.y - origin.y) * u);
|
|
468
|
+
dim.style.opacity = contained ? '0' : String(1 - smooth(0.35, 0.85, u));
|
|
469
|
+
if (u < 1) { requestAnimationFrame(settle); return; }
|
|
470
|
+
settled();
|
|
471
|
+
};
|
|
472
|
+
requestAnimationFrame(settle);
|
|
473
|
+
};
|
|
474
|
+
requestAnimationFrame(step);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// the small lava orb at the top, still, and itself the next tap
|
|
478
|
+
function settled() {
|
|
479
|
+
var p = restPoint();
|
|
480
|
+
if (base < 280) base = 280;
|
|
481
|
+
lava.style.width = base + 'px';
|
|
482
|
+
lava.style.height = base + 'px';
|
|
483
|
+
// at rest it scrolls with the card
|
|
484
|
+
lava.style.position = 'absolute';
|
|
485
|
+
place(REST / base, p.x, p.y);
|
|
486
|
+
lava.style.visibility = 'visible';
|
|
487
|
+
lava.style.opacity = '1';
|
|
488
|
+
core.style.opacity = '0';
|
|
489
|
+
dim.style.opacity = '0';
|
|
490
|
+
lava.classList.add('still', 'tap');
|
|
491
|
+
mode = 'card';
|
|
492
|
+
stop();
|
|
493
|
+
canvas.style.visibility = 'hidden';
|
|
494
|
+
lava.tabIndex = 0;
|
|
495
|
+
if (onBorn) { var cb = onBorn; onBorn = null; cb('settled'); }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// a tap on a card: the card fades in place while the small lava orb
|
|
499
|
+
// glides down to where the sphere sits (to), shrinking to a bright point.
|
|
500
|
+
// swap() takes the card off the page once it has faded. At the centre the
|
|
501
|
+
// lava melts and the sphere grows back out of the point, holds for a
|
|
502
|
+
// beat so it reads, then calls back. Reduced motion: no travel.
|
|
503
|
+
function melt(cb, to, swap) {
|
|
504
|
+
lava.classList.remove('tap', 'alive');
|
|
505
|
+
lava.tabIndex = -1;
|
|
506
|
+
var swapped = false;
|
|
507
|
+
var takeOff = function () { if (!swapped) { swapped = true; swap(); } };
|
|
508
|
+
var reform = function () {
|
|
509
|
+
takeOff();
|
|
510
|
+
canvas.style.visibility = 'visible';
|
|
511
|
+
measure();
|
|
512
|
+
lava.style.visibility = 'hidden';
|
|
513
|
+
lava.style.opacity = '0';
|
|
514
|
+
for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
|
|
515
|
+
age = -1; timed = false; fired = false;
|
|
516
|
+
mode = 'idle';
|
|
517
|
+
bridgeAt = reduced ? 0 : performance.now();
|
|
518
|
+
start();
|
|
519
|
+
setTimeout(cb, reduced ? 0 : BEAT_MS);
|
|
520
|
+
};
|
|
521
|
+
if (reduced || lava.style.visibility !== 'visible') { reform(); return; }
|
|
522
|
+
var r = lava.getBoundingClientRect();
|
|
523
|
+
var from = { x: r.left + r.width / 2, y: r.top + r.height / 2, s: r.width / base };
|
|
524
|
+
// fixed while it travels, so the card's scroll and the swap never move it
|
|
525
|
+
lava.style.position = 'fixed';
|
|
526
|
+
place(from.s, from.x, from.y);
|
|
527
|
+
var sEnd = 30 / base;
|
|
528
|
+
var t0 = performance.now();
|
|
529
|
+
var step = function (now) {
|
|
530
|
+
var k = Math.min(1, (now - t0) / GLIDE_MS);
|
|
531
|
+
var t = ease(k);
|
|
532
|
+
if (now - t0 >= LEAVE_MS) takeOff();
|
|
533
|
+
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
534
|
+
core.style.opacity = String(smooth(0.3, 0.9, k));
|
|
535
|
+
lava.style.opacity = String(1 - smooth(0.7, 1, k));
|
|
536
|
+
if (k < 1) { requestAnimationFrame(step); return; }
|
|
537
|
+
reform();
|
|
538
|
+
};
|
|
539
|
+
requestAnimationFrame(step);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
measure();
|
|
543
|
+
seed();
|
|
544
|
+
start();
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
// the bet is ready: the homepage tap, on the clock
|
|
548
|
+
play: function (cb, fast) {
|
|
549
|
+
onBorn = cb;
|
|
550
|
+
if (reduced) { settled(); return; }
|
|
551
|
+
pace = fast ? FAST : 1;
|
|
552
|
+
if (mode !== 'pull') startPull(fast);
|
|
553
|
+
timed = true;
|
|
554
|
+
start();
|
|
555
|
+
},
|
|
556
|
+
// no bet yet: the spiral follows the real progress
|
|
557
|
+
wait: function () {
|
|
558
|
+
pace = 1;
|
|
559
|
+
if (mode !== 'pull') { startPull(false); timed = false; }
|
|
560
|
+
start();
|
|
561
|
+
},
|
|
562
|
+
progress: function (p) {
|
|
563
|
+
if (!timed) ageTo = Math.max(ageTo, 0.62 * Math.min(1, p));
|
|
564
|
+
},
|
|
565
|
+
// generation failed: the dots relax back to the sphere
|
|
566
|
+
relax: function () {
|
|
567
|
+
age = -1; timed = false; fired = false;
|
|
568
|
+
mode = 'idle';
|
|
569
|
+
start();
|
|
570
|
+
},
|
|
571
|
+
melt: melt,
|
|
572
|
+
leaveMs: reduced ? 200 : LEAVE_MS,
|
|
573
|
+
// a bet is already up when the page loads: the settled orb, no motion
|
|
574
|
+
settled: function () { stop(); onBorn = null; settled(); },
|
|
575
|
+
moving: function () { return mode === 'pull'; },
|
|
576
|
+
};
|
|
577
|
+
})();
|
|
578
|
+
|
|
579
|
+
// ---- the bet ------------------------------------------------------------------
|
|
580
|
+
var bet = { status: null, polling: false, armed: false, drawn: null, busy: false, last: null };
|
|
581
|
+
// how long "builder has it" (or any ack) stays before the next card
|
|
582
|
+
var AFTER_MS = 1500;
|
|
583
|
+
|
|
584
|
+
function buzz() { if (navigator.vibrate) { try { navigator.vibrate(10); } catch (e) { /* not on this device */ } } }
|
|
585
|
+
|
|
586
|
+
function lockIcon() {
|
|
587
|
+
var ns = 'http://www.w3.org/2000/svg';
|
|
588
|
+
var svg = document.createElementNS(ns, 'svg');
|
|
589
|
+
svg.setAttribute('viewBox', '0 0 12 12');
|
|
590
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
591
|
+
var body = document.createElementNS(ns, 'rect');
|
|
592
|
+
body.setAttribute('x', '2'); body.setAttribute('y', '5.5'); body.setAttribute('width', '8'); body.setAttribute('height', '5.5'); body.setAttribute('rx', '1');
|
|
593
|
+
body.setAttribute('fill', 'currentColor');
|
|
594
|
+
var hoop = document.createElementNS(ns, 'path');
|
|
595
|
+
hoop.setAttribute('d', 'M4 5.5V4a2 2 0 0 1 4 0v1.5');
|
|
596
|
+
hoop.setAttribute('fill', 'none'); hoop.setAttribute('stroke', 'currentColor'); hoop.setAttribute('stroke-width', '1.3');
|
|
597
|
+
svg.appendChild(hoop); svg.appendChild(body);
|
|
598
|
+
return svg;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// real progress from the server's events: each source read, then the
|
|
602
|
+
// words the model has written as it works
|
|
603
|
+
function progressOf(events) {
|
|
604
|
+
var p = 0;
|
|
605
|
+
events.forEach(function (e) {
|
|
606
|
+
if (e.kind === 'source' || e.kind === 'skip') p = Math.max(p, Math.min(0.4, p + 0.4 / 6));
|
|
607
|
+
else if (e.kind === 'model') p = Math.max(p, 0.45);
|
|
608
|
+
else if (e.kind === 'thinking') p = Math.max(p, 0.5 + 0.45 * (1 - Math.exp(-(Number(e.n) || 0) / 250)));
|
|
609
|
+
else if (e.kind === 'skeptic') p = Math.max(p, 0.9);
|
|
610
|
+
else if (e.kind === 'done') p = 1;
|
|
611
|
+
});
|
|
612
|
+
return p;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
var SOURCE_WORDS = {
|
|
616
|
+
now: 'reading your notes',
|
|
617
|
+
briefs: 'reading the latest research',
|
|
618
|
+
season: 'reading the target for this week',
|
|
619
|
+
ledger: 'reading how past bets did',
|
|
620
|
+
team: 'reading who is on the team'
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// the newest event, in plain words for the one line under the orb
|
|
624
|
+
function sayEvent(e) {
|
|
625
|
+
if (!e) return 'thinking of the next bet';
|
|
626
|
+
if (e.kind === 'source') {
|
|
627
|
+
// business numbers show as they are; the team's plumbing (the review
|
|
628
|
+
// queue, waiting jobs, merges, releases) never reaches this line
|
|
629
|
+
if (e.key === 'installs' || e.key === 'installs_best') return e.line;
|
|
630
|
+
return SOURCE_WORDS[e.key] || 'reading what is true right now';
|
|
631
|
+
}
|
|
632
|
+
if (e.kind === 'skip') return 'reading what is true right now';
|
|
633
|
+
if (e.kind === 'model') return e.line.indexOf('again') >= 0 ? 'asking again, sharper' : 'thinking of one new bet';
|
|
634
|
+
if (e.kind === 'thinking') return e.line;
|
|
635
|
+
if (e.kind === 'skeptic') return 'a second look: does it make sense';
|
|
636
|
+
if (e.kind === 'score') return 'checking how past bets did';
|
|
637
|
+
if (e.kind === 'reject') return 'the first idea did not hold up, asking again';
|
|
638
|
+
if (e.kind === 'done' || e.kind === 'check') return 'got one';
|
|
639
|
+
return e.line;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function failLine(error) {
|
|
643
|
+
var e = String(error || '');
|
|
644
|
+
if (/longer than/.test(e)) return 'the thinking took too long. tap to try again';
|
|
645
|
+
if (/could not start|exited with code|empty answer|the model said/.test(e)) return 'could not reach the thinking model. tap to try again';
|
|
646
|
+
return 'couldn\\'t land on a good bet. tap to try again';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// after a yes, a nice, or a send back, the next card comes the way a re-tap brings it
|
|
650
|
+
function moveOn(ack) {
|
|
651
|
+
var box = document.getElementById('bet');
|
|
652
|
+
var act = box.querySelector('.act');
|
|
653
|
+
if (act) act.replaceWith(el('p', 'mission', ack));
|
|
654
|
+
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function armedTap(go, label, armedLabel, run) {
|
|
658
|
+
go.addEventListener('click', function () {
|
|
659
|
+
buzz();
|
|
660
|
+
if (!bet.armed) {
|
|
661
|
+
bet.armed = true; go.textContent = armedLabel; go.className = 'pill armed';
|
|
662
|
+
setTimeout(function () { if (bet.armed) { bet.armed = false; go.textContent = label; go.className = 'pill'; } }, 5000);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
bet.armed = false;
|
|
666
|
+
run();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// a line that ends like a sentence
|
|
671
|
+
function sentence(t) {
|
|
672
|
+
var s = String(t || '').trim();
|
|
673
|
+
return s && !/[.!?]$/.test(s) ? s + '.' : s;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
var MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
677
|
+
// "we'll know by nov 1. about 30 hours of agent time, $0."
|
|
678
|
+
function whenLine(b) {
|
|
679
|
+
var m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(String(b.check_date || ''));
|
|
680
|
+
var day = m ? MONTHS[Number(m[2]) - 1] + ' ' + Number(m[3]) : b.check_date;
|
|
681
|
+
return [day ? "we'll know by " + day : '', b.cost].filter(Boolean).map(sentence).join(' ');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function drawJobCard(s) {
|
|
685
|
+
var box = document.getElementById('bet');
|
|
686
|
+
box.textContent = '';
|
|
687
|
+
var j = s.job;
|
|
688
|
+
var act = el('div', 'act');
|
|
689
|
+
if (s.kind === 'yes') {
|
|
690
|
+
box.appendChild(el('h1', null, j.ask));
|
|
691
|
+
box.appendChild(el('p', 'why', sentence(j.owner + ' is working on ' + j.title) + ' this step waits for your yes.'));
|
|
692
|
+
// the yes covers exactly this command, so the owner sees it
|
|
693
|
+
if (j.command) box.appendChild(el('p', 'when', 'your yes lets this run once: ' + j.command));
|
|
694
|
+
var yes = el('button', 'pill', 'yes');
|
|
695
|
+
yes.type = 'button';
|
|
696
|
+
yes.setAttribute('aria-label', 'yes. tells ' + j.owner + ' to go ahead');
|
|
697
|
+
armedTap(yes, 'yes', 'tap again to say yes', function () {
|
|
698
|
+
api('/bet/yes', { confirm: true }).then(function (r) { moveOn(r.ack || r.message || 'sent'); });
|
|
699
|
+
});
|
|
700
|
+
act.appendChild(yes);
|
|
701
|
+
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
702
|
+
box.appendChild(act);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
box.appendChild(el('h1', null, j.title));
|
|
706
|
+
var head = j.failed ? (j.halted ? 'this stopped before it was done.' : 'this did not pass.') : 'this is done.';
|
|
707
|
+
box.appendChild(el('p', 'why', [head].concat(j.lines.map(sentence)).join(' ')));
|
|
708
|
+
box.appendChild(el('p', 'when', [j.check, j.review, j.proof].map(sentence).join(' ')));
|
|
709
|
+
var ok = el('button', 'pill', 'nice');
|
|
710
|
+
ok.type = 'button';
|
|
711
|
+
ok.addEventListener('click', function () {
|
|
712
|
+
buzz();
|
|
713
|
+
api('/bet/nice', {}).then(function (r) { moveOn(r.ack || 'nice.'); });
|
|
714
|
+
});
|
|
715
|
+
act.appendChild(ok);
|
|
716
|
+
if (j.failed) {
|
|
717
|
+
var links = el('div', 'links');
|
|
718
|
+
var back = el('button', null, 'send back');
|
|
719
|
+
back.type = 'button';
|
|
720
|
+
armedTap(back, 'send back', 'tap again to send it back', function () {
|
|
721
|
+
api('/bet/sendback', { confirm: true }).then(function (r) { moveOn(r.ack || r.message || 'sent back'); });
|
|
722
|
+
});
|
|
723
|
+
links.appendChild(back);
|
|
724
|
+
act.appendChild(links);
|
|
725
|
+
}
|
|
726
|
+
box.appendChild(act);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function drawBetCard(s) {
|
|
730
|
+
if (s.kind && s.kind !== 'bet') { drawJobCard(s); return; }
|
|
731
|
+
var box = document.getElementById('bet');
|
|
732
|
+
box.textContent = '';
|
|
733
|
+
var b = s.bet;
|
|
734
|
+
box.appendChild(el('h1', null, b.title));
|
|
735
|
+
// what is going on and what we expect, as sentences with the numbers inside
|
|
736
|
+
box.appendChild(el('p', 'why', [b.why_now, b.expect].map(sentence).join(' ')));
|
|
737
|
+
var ol = el('ol', 'steps');
|
|
738
|
+
b.steps.forEach(function (st) {
|
|
739
|
+
var li = el('li');
|
|
740
|
+
var tag = el('span', 'tag ' + (st.who === 'you' ? 'you' : 'team'));
|
|
741
|
+
if (st.who === 'you') tag.appendChild(lockIcon());
|
|
742
|
+
tag.appendChild(document.createTextNode(st.who === 'you' ? 'your yes' : (st.member || 'team')));
|
|
743
|
+
li.appendChild(tag);
|
|
744
|
+
li.appendChild(document.createTextNode(st.text));
|
|
745
|
+
ol.appendChild(li);
|
|
746
|
+
});
|
|
747
|
+
box.appendChild(ol);
|
|
748
|
+
box.appendChild(el('p', 'when', whenLine(b)));
|
|
749
|
+
if (s.mission) { box.appendChild(el('p', 'mission', s.mission.line)); return; }
|
|
750
|
+
if (s.status === 'starting') { box.appendChild(el('p', 'mission', 'starting the mission')); return; }
|
|
751
|
+
var act = el('div', 'act');
|
|
752
|
+
var go = el('button', 'pill' + (bet.armed ? ' armed' : ''), bet.armed ? 'tap again to start' : 'do it');
|
|
753
|
+
go.type = 'button';
|
|
754
|
+
go.setAttribute('aria-label', bet.armed ? 'tap again to start the team on this bet' : 'do it. starts the team on the steps; your yes steps wait for you');
|
|
755
|
+
go.addEventListener('click', function () {
|
|
756
|
+
buzz();
|
|
757
|
+
if (!bet.armed) {
|
|
758
|
+
bet.armed = true; drawBetCard(s);
|
|
759
|
+
setTimeout(function () { if (bet.armed) { bet.armed = false; if (bet.last) drawBetCard(bet.last); } }, 5000);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
bet.armed = false;
|
|
763
|
+
s.status = 'starting'; drawBetCard(s);
|
|
764
|
+
api('/bet/do', { confirm: true }).then(function (r) {
|
|
765
|
+
loadBet(); refresh();
|
|
766
|
+
if (r && r.ok) setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
act.appendChild(go);
|
|
770
|
+
var links = el('div', 'links');
|
|
771
|
+
var sharp = el('button', null, 'sharper idea');
|
|
772
|
+
sharp.type = 'button';
|
|
773
|
+
sharp.addEventListener('click', function () { tapOrb(true); });
|
|
774
|
+
var skip = el('button', null, 'not now');
|
|
775
|
+
skip.type = 'button';
|
|
776
|
+
skip.addEventListener('click', function () {
|
|
777
|
+
buzz();
|
|
778
|
+
api('/bet/skip', {}).then(function () {
|
|
779
|
+
dissolve(function () { bet.status = 'skipped'; line.set('not now. tap when you want another', false); });
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
links.appendChild(sharp);
|
|
783
|
+
links.appendChild(document.createTextNode(' · '));
|
|
784
|
+
links.appendChild(skip);
|
|
785
|
+
act.appendChild(links);
|
|
786
|
+
box.appendChild(act);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// the time from the tap to the card fully on screen, kept on the page so
|
|
790
|
+
// the speed can be measured
|
|
791
|
+
function measured() {
|
|
792
|
+
if (bet.tapAt) document.body.setAttribute('data-reveal-ms', String(Math.round(performance.now() - bet.tapAt)));
|
|
793
|
+
bet.tapAt = 0;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function showCard(s, animate, fast) {
|
|
797
|
+
var box = document.getElementById('bet');
|
|
798
|
+
bet.last = s;
|
|
799
|
+
bet.drawn = s.id;
|
|
800
|
+
drawBetCard(s);
|
|
801
|
+
document.body.classList.add('has-card');
|
|
802
|
+
box.hidden = false;
|
|
803
|
+
box.classList.remove('leave');
|
|
804
|
+
box.classList.toggle('quick', Boolean(fast));
|
|
805
|
+
if (!animate) { box.classList.add('up'); measured(); return; }
|
|
806
|
+
box.classList.remove('up');
|
|
807
|
+
var done = function (e) { if (e.propertyName === 'opacity') { box.removeEventListener('transitionend', done); measured(); } };
|
|
808
|
+
box.addEventListener('transitionend', done);
|
|
809
|
+
void box.offsetWidth;
|
|
810
|
+
requestAnimationFrame(function () { box.classList.add('up'); });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// where the sphere sits once the card is gone, read before anything moves,
|
|
814
|
+
// in page coordinates (the screen's once the page is back at the top)
|
|
815
|
+
function sphereSpot() {
|
|
816
|
+
var box = document.getElementById('bet');
|
|
817
|
+
var y0 = window.scrollY, was = box.hidden;
|
|
818
|
+
document.body.classList.remove('has-card');
|
|
819
|
+
box.hidden = true;
|
|
820
|
+
var r = document.getElementById('orb').getBoundingClientRect();
|
|
821
|
+
var spot = { x: r.left + r.width / 2, y: r.top + window.scrollY + r.height / 2 };
|
|
822
|
+
document.body.classList.add('has-card');
|
|
823
|
+
box.hidden = was;
|
|
824
|
+
if (window.scrollY !== y0) window.scrollTo(0, y0);
|
|
825
|
+
return spot;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// the card fades out in place while the small orb glides down and becomes
|
|
829
|
+
// the sphere again; the card leaves the page once it has faded
|
|
830
|
+
function dissolve(then) {
|
|
831
|
+
var box = document.getElementById('bet');
|
|
832
|
+
var to = sphereSpot();
|
|
833
|
+
box.classList.remove('quick', 'up');
|
|
834
|
+
box.classList.add('leave');
|
|
835
|
+
var swap = function () {
|
|
836
|
+
box.hidden = true;
|
|
837
|
+
box.classList.remove('leave');
|
|
838
|
+
document.body.classList.remove('has-card');
|
|
839
|
+
window.scrollTo(0, 0);
|
|
840
|
+
};
|
|
841
|
+
if (reduced) { setTimeout(function () { orb.melt(then, to, swap); }, orb.leaveMs); return; }
|
|
842
|
+
orb.melt(then, to, swap);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// what the page does with the server's answer once the sphere is ready
|
|
846
|
+
// fast: the bet was waiting in the queue; otherwise it was just made
|
|
847
|
+
function reveal(s, fast) {
|
|
848
|
+
bet.status = s.status;
|
|
849
|
+
if ((s.bet || s.job) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
850
|
+
line.set('', false);
|
|
851
|
+
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(s, true, fast); } }, fast);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (s.status === 'thinking') {
|
|
855
|
+
orb.wait();
|
|
856
|
+
orb.progress(progressOf(s.events));
|
|
857
|
+
line.set(sayEvent(s.events[s.events.length - 1]), false);
|
|
858
|
+
poll();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (s.status === 'failed') {
|
|
862
|
+
bet.busy = false;
|
|
863
|
+
orb.relax();
|
|
864
|
+
line.set(failLine(s.error), true);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
bet.busy = false;
|
|
868
|
+
line.set('tap to think', false);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// one tap: buzz at once, ask the server for the next bet, and play the
|
|
872
|
+
// choreography; a card on screen dissolves first
|
|
873
|
+
function tapOrb(sharper) {
|
|
874
|
+
if (bet.busy) return;
|
|
875
|
+
buzz();
|
|
876
|
+
bet.busy = true;
|
|
877
|
+
bet.armed = false;
|
|
878
|
+
bet.tapAt = performance.now();
|
|
879
|
+
document.body.removeAttribute('data-reveal-ms');
|
|
880
|
+
var answer = api('/bet/think', { sharper: Boolean(sharper) });
|
|
881
|
+
var ready = new Promise(function (resolve) {
|
|
882
|
+
if (document.body.classList.contains('has-card')) dissolve(resolve);
|
|
883
|
+
else resolve();
|
|
884
|
+
});
|
|
885
|
+
Promise.all([answer, ready]).then(function (both) {
|
|
886
|
+
var s = both[0];
|
|
887
|
+
if (s.http !== 200) { bet.busy = false; orb.relax(); line.set('that did not go through. tap to try again', true); return; }
|
|
888
|
+
reveal(s, true);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function loadBet() {
|
|
893
|
+
return api('/bet').then(function (s) {
|
|
894
|
+
if (s.http !== 200) return;
|
|
895
|
+
var was = bet.status;
|
|
896
|
+
bet.status = s.status;
|
|
897
|
+
if (s.status === 'thinking') {
|
|
898
|
+
orb.progress(progressOf(s.events));
|
|
899
|
+
line.set(sayEvent(s.events[s.events.length - 1]), false);
|
|
900
|
+
poll();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (was === 'thinking') { reveal(s); return; }
|
|
904
|
+
if ((s.bet || s.job) && bet.drawn === s.id) { bet.last = s; drawBetCard(s); if (s.status === 'starting') poll(); }
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function poll() {
|
|
909
|
+
if (bet.polling) return;
|
|
910
|
+
bet.polling = true;
|
|
911
|
+
setTimeout(function () { bet.polling = false; loadBet(); }, 400);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// first look: a bet already on screen shows settled, a bet being made
|
|
915
|
+
// shows its progress, anything else is the sphere
|
|
916
|
+
function firstLook() {
|
|
917
|
+
api('/bet').then(function (s) {
|
|
918
|
+
if (s.http !== 200) return;
|
|
919
|
+
bet.status = s.status;
|
|
920
|
+
if ((s.bet || s.job) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
921
|
+
orb.settled();
|
|
922
|
+
showCard(s, false);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (s.status === 'thinking') { bet.busy = true; reveal(s); return; }
|
|
926
|
+
if (s.status === 'failed') { line.set(failLine(s.error), true); return; }
|
|
927
|
+
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
var canvas = document.getElementById('orb');
|
|
932
|
+
var lavaTap = document.getElementById('lava');
|
|
933
|
+
[canvas, lavaTap].forEach(function (node) {
|
|
934
|
+
node.addEventListener('click', function () { tapOrb(false); });
|
|
935
|
+
node.addEventListener('keydown', function (e) {
|
|
936
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tapOrb(false); }
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// ---- only you: the decision deck, behind one quiet link ---------------------------
|
|
941
|
+
|
|
942
|
+
var sheet = document.getElementById('sheet');
|
|
943
|
+
document.getElementById('open-only').addEventListener('click', function () { sheet.hidden = false; draw(); });
|
|
944
|
+
document.getElementById('working').addEventListener('click', function () {
|
|
945
|
+
var list = document.getElementById('jobs');
|
|
946
|
+
list.hidden = !list.hidden;
|
|
947
|
+
this.setAttribute('aria-expanded', list.hidden ? 'false' : 'true');
|
|
948
|
+
});
|
|
949
|
+
document.getElementById('close-only').addEventListener('click', function () { sheet.hidden = true; armed = null; view = 'card'; });
|
|
950
|
+
|
|
951
|
+
function say(text, done) {
|
|
952
|
+
var reply = document.getElementById('reply');
|
|
953
|
+
reply.hidden = !text;
|
|
954
|
+
reply.textContent = text || '';
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function choiceButton(card, choice, main) {
|
|
958
|
+
var b = el('button', 'choice' + (main ? ' main' : (choice.id === 'later' ? ' later' : '')));
|
|
959
|
+
b.type = 'button';
|
|
960
|
+
var label = el('span', null, choice.label);
|
|
961
|
+
b.appendChild(label);
|
|
962
|
+
if (choice.effect) b.appendChild(el('span', 'effect', choice.effect));
|
|
963
|
+
var key = card.id + ':' + choice.id;
|
|
964
|
+
if (armed && armed.key === key) {
|
|
965
|
+
b.className = 'choice armed';
|
|
966
|
+
label.textContent = 'tap again to confirm';
|
|
967
|
+
}
|
|
968
|
+
b.addEventListener('click', function () { tap(card, choice, key); });
|
|
969
|
+
return b;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function tap(card, choice, key) {
|
|
973
|
+
if (choice.view === 'oldest') { view = 'oldest'; draw(); return; }
|
|
974
|
+
if (choice.confirm && !(armed && armed.key === key && Date.now() < armed.until)) {
|
|
975
|
+
armed = { key: key, until: Date.now() + 5000 };
|
|
976
|
+
draw();
|
|
977
|
+
setTimeout(function () { if (armed && armed.key === key && Date.now() >= armed.until) { armed = null; draw(); } }, 5100);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
armed = null;
|
|
981
|
+
if (choice.ack) say(choice.ack, false);
|
|
982
|
+
api('/action', { card: card.id, choice: choice.id, confirm: Boolean(choice.confirm) }).then(function (res) {
|
|
983
|
+
if (!res.ok) { say(res.message || 'that did not go through', false); return refresh(); }
|
|
984
|
+
if (choice.id === 'later') { say('', false); view = 'card'; }
|
|
985
|
+
if (res.undo) showUndo(res.ack, res.undo);
|
|
986
|
+
else if (res.id) pendingReply = res.id;
|
|
987
|
+
if (choice.id !== 'later' && !/^(approve|sendback):/.test(choice.id)) view = 'card';
|
|
988
|
+
refresh();
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function showUndo(text, undo) {
|
|
993
|
+
var toast = document.getElementById('toast');
|
|
994
|
+
document.getElementById('toast-text').textContent = text;
|
|
995
|
+
toast.hidden = false;
|
|
996
|
+
var b = document.getElementById('toast-undo');
|
|
997
|
+
b.onclick = function () {
|
|
998
|
+
toast.hidden = true;
|
|
999
|
+
api('/undo', { id: undo.id }).then(function (res) { say(res.ack || res.message || '', false); refresh(); });
|
|
1000
|
+
};
|
|
1001
|
+
clearTimeout(undoTimer);
|
|
1002
|
+
undoTimer = setTimeout(function () { toast.hidden = true; }, undo.ms);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function drawCard(main, s) {
|
|
1006
|
+
var card = s.card;
|
|
1007
|
+
var box = el('section', 'card');
|
|
1008
|
+
box.appendChild(el('div', 'count', 'decision ' + (s.decided + 1) + ' of ' + Math.min(s.card_limit, s.decided + s.cards_left)));
|
|
1009
|
+
if (view === 'oldest' && card.jobs) {
|
|
1010
|
+
box.appendChild(el('p', 'say', 'the oldest jobs waiting on review'));
|
|
1011
|
+
card.jobs.forEach(function (job) {
|
|
1012
|
+
var j = el('div', 'job');
|
|
1013
|
+
j.appendChild(el('div', null, job.title));
|
|
1014
|
+
j.appendChild(el('div', 'meta', job.member + ', waiting ' + job.age + ', ' + job.ref));
|
|
1015
|
+
var row = el('div', 'row');
|
|
1016
|
+
row.appendChild(choiceButton(card, { id: 'approve:' + job.ref, label: 'approve', confirm: true, ack: job.member + ': thanks, landing it' }, false));
|
|
1017
|
+
row.appendChild(choiceButton(card, { id: 'sendback:' + job.ref, label: 'send back', confirm: true, ack: job.member + ': on it, taking another pass' }, false));
|
|
1018
|
+
j.appendChild(row);
|
|
1019
|
+
box.appendChild(j);
|
|
1020
|
+
});
|
|
1021
|
+
var back = el('button', 'choice later', 'back');
|
|
1022
|
+
back.type = 'button';
|
|
1023
|
+
back.addEventListener('click', function () { view = 'card'; armed = null; draw(); });
|
|
1024
|
+
box.appendChild(back);
|
|
1025
|
+
} else {
|
|
1026
|
+
box.appendChild(el('p', 'say', card.text));
|
|
1027
|
+
card.choices.forEach(function (c, i) { box.appendChild(choiceButton(card, c, i === 0 && c.id !== 'later')); });
|
|
1028
|
+
var cmds = [];
|
|
1029
|
+
card.choices.forEach(function (c) { if (/^yes-/.test(card.id) && c.commands) cmds = cmds.concat(c.commands); });
|
|
1030
|
+
if (cmds.length) box.appendChild(el('div', 'count', 'yes runs: ' + cmds.join('; ')));
|
|
1031
|
+
}
|
|
1032
|
+
main.appendChild(box);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function drawClear(main, s) {
|
|
1036
|
+
var box = el('section', 'card');
|
|
1037
|
+
box.appendChild(el('p', 'say', s.decided >= s.card_limit ? 'that is five decisions. the rest can wait.' : (s.decided ? 'you have been through everything for now.' : 'nothing needs you right now.')));
|
|
1038
|
+
var running = s.all_clear.still_running;
|
|
1039
|
+
box.appendChild(el('div', 'count', running.length ? 'still running: ' + running.join(', ') : 'nothing is running right now'));
|
|
1040
|
+
main.appendChild(box);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function drawWorking(jobs) {
|
|
1044
|
+
var btn = document.getElementById('working');
|
|
1045
|
+
var list = document.getElementById('jobs');
|
|
1046
|
+
btn.hidden = !jobs.length;
|
|
1047
|
+
btn.textContent = jobs.length + ' working';
|
|
1048
|
+
if (!jobs.length) { list.hidden = true; btn.setAttribute('aria-expanded', 'false'); return; }
|
|
1049
|
+
list.textContent = '';
|
|
1050
|
+
jobs.forEach(function (j) {
|
|
1051
|
+
var row = el('p', null, j.title);
|
|
1052
|
+
row.appendChild(el('span', 'meta', j.owner + ' has it' + (j.running_for ? ', running ' + j.running_for : '')));
|
|
1053
|
+
list.appendChild(row);
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function draw() {
|
|
1058
|
+
var s = last;
|
|
1059
|
+
if (!s) return;
|
|
1060
|
+
var link = document.getElementById('open-only');
|
|
1061
|
+
var n = s.cards_left;
|
|
1062
|
+
link.hidden = !n && sheet.hidden;
|
|
1063
|
+
link.textContent = n === 1 ? '1 thing only you can do' : n + ' things only you can do';
|
|
1064
|
+
drawWorking(s.bet && s.bet.working ? s.bet.working : []);
|
|
1065
|
+
document.getElementById('target').textContent = s.strip.line;
|
|
1066
|
+
var pct = s.strip.goal ? Math.min(100, Math.round(100 * s.strip.landed / s.strip.goal)) : 0;
|
|
1067
|
+
document.getElementById('bar').style.width = pct + '%';
|
|
1068
|
+
if (sheet.hidden) return;
|
|
1069
|
+
var main = document.getElementById('main');
|
|
1070
|
+
main.textContent = '';
|
|
1071
|
+
if (s.error) main.appendChild(el('p', 'count', 'could not read the task list: ' + s.error));
|
|
1072
|
+
if (s.card && !(view === 'oldest' && !s.card.jobs)) drawCard(main, s);
|
|
1073
|
+
else drawClear(main, s);
|
|
1074
|
+
if (pendingReply) {
|
|
1075
|
+
var a = s.actions.filter(function (x) { return x.id === pendingReply; })[0];
|
|
1076
|
+
if (a) say(a.line, a.status === 'done');
|
|
1077
|
+
}
|
|
1078
|
+
document.getElementById('live').textContent = s.live;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function refresh() {
|
|
1082
|
+
return api('/state').then(function (s) {
|
|
1083
|
+
if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
|
|
1084
|
+
if (!s.card || s.card.id !== 'review') view = 'card';
|
|
1085
|
+
last = s;
|
|
1086
|
+
draw();
|
|
1087
|
+
}).catch(function () {
|
|
1088
|
+
document.getElementById('live').textContent = 'cannot reach the mac right now. retrying.';
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
firstLook();
|
|
1093
|
+
refresh();
|
|
1094
|
+
setInterval(refresh, 5000);
|
|
1095
|
+
})();
|
|
1096
|
+
</script>
|
|
1097
|
+
</body>
|
|
1098
|
+
</html>
|
|
1099
|
+
`;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
module.exports = { phonePage };
|