atris 3.72.5 → 3.73.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/README.md +2 -0
- package/atris/skills/commerce/SKILL.md +24 -3
- package/bin/atris.js +10 -0
- package/commands/gm.js +16 -5
- package/commands/member.js +59 -9
- package/commands/pay.js +329 -0
- package/commands/trust.js +117 -0
- package/commands/zen.js +17 -0
- package/lib/gm-bet.js +1443 -162
- package/lib/gm-phone-page.js +460 -357
- package/lib/gm-phone.js +58 -7
- package/lib/known-commands.js +1 -1
- package/lib/try-repo.js +442 -0
- package/lib/zen-apply.js +23 -3
- package/lib/zen-fleet.js +100 -0
- package/lib/zen.js +41 -6
- package/package.json +1 -1
package/lib/gm-phone-page.js
CHANGED
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
// then settles small at the top and the card fades up. Tap again and the
|
|
14
14
|
// card dissolves, the lava melts back into the sphere, and the next bet plays.
|
|
15
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
|
|
17
|
-
//
|
|
16
|
+
// spiral follows the real progress. Once a card is up nothing moves. The
|
|
17
|
+
// footer is only the count of jobs the team is working on.
|
|
18
|
+
//
|
|
19
|
+
// Every card is big type in two parts: what is going on, and why it matters
|
|
20
|
+
// to the business. Nothing about how work is run or checked reaches the page.
|
|
18
21
|
|
|
19
22
|
function phonePage(nonce) {
|
|
20
23
|
return `<!doctype html>
|
|
@@ -58,7 +61,7 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
58
61
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
59
62
|
}
|
|
60
63
|
.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:
|
|
64
|
+
.stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; touch-action: none; cursor: pointer; }
|
|
62
65
|
.stage canvas:focus-visible { outline-offset: -8%; }
|
|
63
66
|
.line {
|
|
64
67
|
min-height: 3em; margin: 4px 0 0; max-width: 22em; text-align: center;
|
|
@@ -68,11 +71,25 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
68
71
|
.line.out { opacity: 0; }
|
|
69
72
|
.line.bad { color: var(--text-2); }
|
|
70
73
|
.foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; }
|
|
71
|
-
.quiet { color: var(--text-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
.
|
|
75
|
-
.
|
|
74
|
+
.quiet { color: var(--text-2); font-size: 17px; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 44px; }
|
|
75
|
+
/* the team: one row per member, who, what, the status, and the time */
|
|
76
|
+
.team { position: fixed; inset: 0; z-index: 20; overflow-y: auto; background: var(--bg); padding: max(24px, env(safe-area-inset-top)) 24px max(24px, env(safe-area-inset-bottom)); }
|
|
77
|
+
.team .inner { max-width: 34rem; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
|
|
78
|
+
.team .head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
79
|
+
.team h1 { margin: 0; font-size: clamp(23px, 6.2vw, 30px); line-height: 1.12; font-weight: 700; letter-spacing: -0.03em; }
|
|
80
|
+
.team .row { width: 100%; text-align: left; display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; padding: 12px 14px; border: 1px solid var(--line); border-radius: 14px; color: var(--text); }
|
|
81
|
+
.team .who { font-size: 20px; font-weight: 700; }
|
|
82
|
+
.team .st { font-size: 17px; color: var(--text-2); text-align: right; }
|
|
83
|
+
.team .st.working { color: var(--text); }
|
|
84
|
+
.team .st.waiting-for-your-yes { color: var(--amber); }
|
|
85
|
+
.team .on { grid-column: 1 / -1; font-size: 18px; line-height: 1.35; }
|
|
86
|
+
.team .time { grid-column: 1 / -1; font-size: 17px; color: var(--text-2); }
|
|
87
|
+
.sayline { margin: 10px 0 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
88
|
+
.sayline .thread, .sayline .reply { width: 100%; text-align: left; margin: 0; }
|
|
89
|
+
.turn { margin: 0 0 6px; font-size: 18px; line-height: 1.4; }
|
|
90
|
+
.turn.you { color: var(--text-2); }
|
|
91
|
+
.turn.orb { color: var(--text); }
|
|
92
|
+
.say { width: 100%; padding: 12px 14px; border-radius: 14px; border: 1px solid var(--line); background: transparent; color: var(--text); font: inherit; font-size: 18px; line-height: 1.4; resize: none; }
|
|
76
93
|
|
|
77
94
|
/* the lava orb, LavaOrb variant sm: three drifting blobs, transform and opacity only */
|
|
78
95
|
.lava {
|
|
@@ -110,60 +127,37 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
110
127
|
.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
128
|
.bet.up { opacity: 1; transform: none; }
|
|
112
129
|
.bet.quick { transition-duration: 0.4s, 0.5s; }
|
|
130
|
+
/* card to card: the old card lifts and fades, the box eases to the new
|
|
131
|
+
height, and the new card settles up from a small drift */
|
|
132
|
+
.bet.turning { overflow: hidden; transition: opacity 0.22s ease-in, transform 0.22s ease-in, height 0.36s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
133
|
+
.bet.turning.settle { transition: opacity 0.36s ease-out, transform 0.36s cubic-bezier(0.22, 1, 0.36, 1), height 0.36s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
134
|
+
.bet.turn-out { opacity: 0; transform: translateY(-10px); }
|
|
135
|
+
.bet.turn-in { opacity: 0; transform: translateY(10px); transition: none; }
|
|
113
136
|
/* leaving: a soft fade in place, never a cut */
|
|
114
137
|
.bet.leave { opacity: 0; transform: none; transition: opacity 0.4s ease-out; }
|
|
115
138
|
.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:
|
|
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; }
|
|
139
|
+
.why { margin: 14px 0 0; color: var(--text); font-size: 20px; line-height: 1.4; }
|
|
124
140
|
.act { margin: 18px 0 0; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
|
125
141
|
.pill {
|
|
126
142
|
min-width: 12rem; height: 44px; padding: 0 28px; border-radius: 22px;
|
|
127
143
|
background: var(--amber); color: var(--amber-ink); font-size: 17px; font-weight: 600; letter-spacing: -0.01em;
|
|
128
144
|
}
|
|
129
145
|
.pill.armed { background: var(--text); color: var(--bg); }
|
|
130
|
-
.links { color: var(--text-
|
|
146
|
+
.links { color: var(--text-2); font-size: 17px; }
|
|
131
147
|
.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
|
-
.
|
|
148
|
+
.links .pill { color: var(--amber-ink); text-decoration: none; padding: 0 28px; }
|
|
149
|
+
.mission { margin: 32px 0 0; padding: 16px 0 0; border-top: 1px solid var(--line); color: var(--text); font-size: 20px; line-height: 1.4; text-align: center; }
|
|
150
|
+
/* a choice: two or three ideas to tap, each a big title and one plain line of why */
|
|
151
|
+
.options { margin: 14px 0 0; display: flex; flex-direction: column; gap: 10px; }
|
|
152
|
+
.option { width: 100%; text-align: left; padding: 12px 14px; border-radius: 14px; border: 1px solid var(--line); color: var(--text); }
|
|
153
|
+
.option .t { display: block; font-size: 20px; line-height: 1.2; font-weight: 700; letter-spacing: -0.02em; }
|
|
154
|
+
.option .w { display: block; margin-top: 4px; font-size: 17px; line-height: 1.3; color: var(--text-2); }
|
|
155
|
+
/* a question card: the answers stack full width under the question */
|
|
156
|
+
.act.answers { align-items: stretch; gap: 10px; }
|
|
157
|
+
.act.answers .links { text-align: center; }
|
|
133
158
|
|
|
134
|
-
/*
|
|
135
|
-
.
|
|
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; }
|
|
159
|
+
/* an answer to a question card */
|
|
160
|
+
.choice { width: 100%; min-height: 52px; text-align: left; padding: 12px 16px; border-radius: 14px; border: 1px solid var(--line); color: var(--text); font-size: 19px; }
|
|
167
161
|
|
|
168
162
|
@media (prefers-reduced-motion: reduce) {
|
|
169
163
|
.lava .tx, .lava .ty, .lava .blob { animation: none !important; }
|
|
@@ -175,37 +169,23 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
175
169
|
<body>
|
|
176
170
|
<main class="screen" id="screen">
|
|
177
171
|
<div class="stage" id="stage">
|
|
178
|
-
<canvas id="orb" tabindex="0" role="button" aria-label="the atris orb. tap to think of one new
|
|
172
|
+
<canvas id="orb" tabindex="0" role="button" aria-label="the atris orb. tap to think of one new idea for the business"></canvas>
|
|
179
173
|
</div>
|
|
180
174
|
<p class="line" id="line" aria-live="polite">tap to think</p>
|
|
181
175
|
<article class="bet" id="bet" hidden></article>
|
|
182
|
-
<div class="foot" id="foot"><button type="button" class="quiet" id="
|
|
176
|
+
<div class="foot" id="foot"><button type="button" class="quiet" id="working" aria-haspopup="dialog">the team</button></div>
|
|
183
177
|
</main>
|
|
184
|
-
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap for the next
|
|
178
|
+
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap for the next idea">
|
|
185
179
|
<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
180
|
<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
181
|
<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
182
|
<div class="core" id="lava-core"></div>
|
|
189
183
|
<div class="dim" id="lava-dim"></div>
|
|
190
184
|
</div>
|
|
191
|
-
<section class="
|
|
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>
|
|
185
|
+
<section class="team" id="team" hidden role="dialog" aria-label="the team"><div class="inner" id="team-inner"></div></section>
|
|
201
186
|
<script nonce="${nonce}">
|
|
202
187
|
(function () {
|
|
203
188
|
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
189
|
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
210
190
|
|
|
211
191
|
function el(tag, cls, text) {
|
|
@@ -261,6 +241,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
261
241
|
var dim = document.getElementById('lava-dim');
|
|
262
242
|
var ctx = canvas.getContext('2d');
|
|
263
243
|
var SPRING = 0.07, DAMP = 0.8;
|
|
244
|
+
// StippleOrb's pointer: dots within RADIUS of it are pushed out by PUSH
|
|
245
|
+
// and spring home; a ripple runs out at WAVE_SPEED from where it came in
|
|
246
|
+
var RADIUS = 90, PUSH = 14;
|
|
247
|
+
var WAVE_SPEED = 620, WAVE_WIDTH = 70, WAVE_LIFE = 1.6;
|
|
264
248
|
var GROW_MS = 1050, MAX_SCALE = 3.2;
|
|
265
249
|
var SETTLE_MS = 900;
|
|
266
250
|
// a tap on a card: the card fades for LEAVE_MS while the small orb glides
|
|
@@ -289,8 +273,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
289
273
|
var raf = 0, lastT = 0;
|
|
290
274
|
var onBorn = null;
|
|
291
275
|
var base = 280, origin = { x: 0, y: 0 };
|
|
292
|
-
|
|
276
|
+
// the light eases toward the pointer; at rest it comes from the upper left
|
|
277
|
+
var light = { x: -0.5, y: -0.6, tx: -0.5, ty: -0.6 };
|
|
293
278
|
var lz = Math.sqrt(1 - light.x * light.x - light.y * light.y);
|
|
279
|
+
var pointer = { x: -9999, y: -9999, inside: false };
|
|
280
|
+
var wave = { x: 0, y: 0, age: 99 };
|
|
294
281
|
var ease = function (t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; };
|
|
295
282
|
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
283
|
|
|
@@ -343,6 +330,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
343
330
|
lastT = now;
|
|
344
331
|
ctx.clearRect(0, 0, w, h);
|
|
345
332
|
if (!reduced && mode === 'idle') angle += dt * 0.15;
|
|
333
|
+
wave.age += dt;
|
|
334
|
+
light.x += (light.tx - light.x) * 0.06;
|
|
335
|
+
light.y += (light.ty - light.y) * 0.06;
|
|
336
|
+
lz = Math.sqrt(Math.max(0, 1 - light.x * light.x - light.y * light.y));
|
|
337
|
+
var r2 = RADIUS * RADIUS;
|
|
338
|
+
var waveFront = wave.age * WAVE_SPEED;
|
|
339
|
+
var waveOn = mode === 'idle' && wave.age < WAVE_LIFE && !reduced;
|
|
340
|
+
var waveGain = waveOn ? 1 - wave.age / WAVE_LIFE : 0;
|
|
346
341
|
if (mode === 'pull') {
|
|
347
342
|
// ready: the age runs on the clock, as on the homepage. Waiting: it
|
|
348
343
|
// follows the real progress and stops short of the point.
|
|
@@ -356,8 +351,29 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
356
351
|
var rx = d.x * cosA + d.z * sinA;
|
|
357
352
|
var rz = -d.x * sinA + d.z * cosA;
|
|
358
353
|
if (mode === 'idle') {
|
|
359
|
-
|
|
360
|
-
|
|
354
|
+
var homeX = cx + rx * R, homeY = cy + d.y * R;
|
|
355
|
+
if (waveOn) {
|
|
356
|
+
var wx = homeX - wave.x, wy = homeY - wave.y;
|
|
357
|
+
var wd = Math.sqrt(wx * wx + wy * wy) || 1;
|
|
358
|
+
var off = (wd - waveFront) / WAVE_WIDTH;
|
|
359
|
+
if (off > -1 && off < 1) {
|
|
360
|
+
var bump = Math.cos(off * Math.PI * 0.5) * 22 * waveGain;
|
|
361
|
+
homeX += (wx / wd) * bump;
|
|
362
|
+
homeY += (wy / wd) * bump;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (pointer.inside && !reduced) {
|
|
366
|
+
var dx = d.sx - pointer.x, dy = d.sy - pointer.y;
|
|
367
|
+
var dist2 = dx * dx + dy * dy;
|
|
368
|
+
if (dist2 < r2 && dist2 > 0.01) {
|
|
369
|
+
var dist = Math.sqrt(dist2);
|
|
370
|
+
var f = (1 - dist / RADIUS) * PUSH;
|
|
371
|
+
d.vx += (dx / dist) * f * 0.2;
|
|
372
|
+
d.vy += (dy / dist) * f * 0.2;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
d.vx += (homeX - d.sx) * SPRING;
|
|
376
|
+
d.vy += (homeY - d.sy) * SPRING;
|
|
361
377
|
d.vx *= DAMP; d.vy *= DAMP;
|
|
362
378
|
d.sx += d.vx; d.sy += d.vy;
|
|
363
379
|
if (reduced) { d.sx = cx + rx * R; d.sy = cy + d.y * R; }
|
|
@@ -512,6 +528,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
512
528
|
lava.style.visibility = 'hidden';
|
|
513
529
|
lava.style.opacity = '0';
|
|
514
530
|
for (var i = 0; i < dots.length; i++) { dots[i].sx = cx; dots[i].sy = cy; dots[i].vx = 0; dots[i].vy = 0; }
|
|
531
|
+
pointer.inside = false;
|
|
515
532
|
age = -1; timed = false; fired = false;
|
|
516
533
|
mode = 'idle';
|
|
517
534
|
bridgeAt = reduced ? 0 : performance.now();
|
|
@@ -543,6 +560,36 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
543
560
|
seed();
|
|
544
561
|
start();
|
|
545
562
|
|
|
563
|
+
// StippleOrb's pointer, ported with trackWindow on, as LetterOrb uses it.
|
|
564
|
+
// Over the sphere the dots part around the pointer and spring back; the
|
|
565
|
+
// light follows the pointer anywhere on the page; a ripple runs out from
|
|
566
|
+
// where the pointer comes in. When it leaves, the dots settle home. On a
|
|
567
|
+
// phone a press and drag pushes the dots, and a tap still thinks. Only
|
|
568
|
+
// the idle sphere answers, and never under reduced motion.
|
|
569
|
+
function local(e) {
|
|
570
|
+
var r = canvas.getBoundingClientRect();
|
|
571
|
+
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
572
|
+
}
|
|
573
|
+
canvas.addEventListener('pointermove', function (e) {
|
|
574
|
+
var p = local(e);
|
|
575
|
+
pointer.x = p.x; pointer.y = p.y; pointer.inside = true;
|
|
576
|
+
});
|
|
577
|
+
canvas.addEventListener('pointerenter', function (e) {
|
|
578
|
+
if (reduced || mode !== 'idle') return;
|
|
579
|
+
var p = local(e);
|
|
580
|
+
wave.x = p.x; wave.y = p.y; wave.age = 0;
|
|
581
|
+
});
|
|
582
|
+
canvas.addEventListener('pointerleave', function () { pointer.inside = false; });
|
|
583
|
+
canvas.addEventListener('pointercancel', function () { pointer.inside = false; });
|
|
584
|
+
function lightFrom(e) {
|
|
585
|
+
var r = canvas.getBoundingClientRect();
|
|
586
|
+
if (!r.width) return;
|
|
587
|
+
var reach = Math.max(280, window.innerWidth * 0.4);
|
|
588
|
+
light.tx = Math.max(-0.9, Math.min(0.9, (e.clientX - (r.left + r.width / 2)) / reach));
|
|
589
|
+
light.ty = Math.max(-0.9, Math.min(0.9, (e.clientY - (r.top + r.height / 2)) / reach));
|
|
590
|
+
}
|
|
591
|
+
window.addEventListener('pointermove', lightFrom);
|
|
592
|
+
|
|
546
593
|
return {
|
|
547
594
|
// the bet is ready: the homepage tap, on the clock
|
|
548
595
|
play: function (cb, fast) {
|
|
@@ -583,74 +630,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
583
630
|
|
|
584
631
|
function buzz() { if (navigator.vibrate) { try { navigator.vibrate(10); } catch (e) { /* not on this device */ } } }
|
|
585
632
|
|
|
586
|
-
|
|
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
|
|
633
|
+
// after a yes or a nice, the next card comes the way a re-tap brings it
|
|
650
634
|
function moveOn(ack) {
|
|
651
635
|
var box = document.getElementById('bet');
|
|
636
|
+
box.style.minHeight = box.offsetHeight + 'px';
|
|
652
637
|
var act = box.querySelector('.act');
|
|
638
|
+
var line = box.querySelector('.sayline');
|
|
639
|
+
if (line) line.remove();
|
|
653
640
|
if (act) act.replaceWith(el('p', 'mission', ack));
|
|
641
|
+
else box.appendChild(el('p', 'mission', ack));
|
|
654
642
|
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
655
643
|
}
|
|
656
644
|
|
|
@@ -673,85 +661,270 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
673
661
|
return s && !/[.!?]$/.test(s) ? s + '.' : s;
|
|
674
662
|
}
|
|
675
663
|
|
|
676
|
-
|
|
677
|
-
//
|
|
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
|
-
|
|
664
|
+
// a yes the owner owes, or a finished business move: what is going on,
|
|
665
|
+
// then why it matters, both written by the server
|
|
684
666
|
function drawJobCard(s) {
|
|
685
667
|
var box = document.getElementById('bet');
|
|
686
668
|
box.textContent = '';
|
|
687
669
|
var j = s.job;
|
|
670
|
+
box.appendChild(el('h1', null, j.what));
|
|
671
|
+
box.appendChild(el('p', 'why', j.why));
|
|
688
672
|
var act = el('div', 'act');
|
|
689
673
|
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
674
|
var yes = el('button', 'pill', 'yes');
|
|
695
675
|
yes.type = 'button';
|
|
696
|
-
yes.setAttribute('aria-label', 'yes. tells ' + j.owner + ' to go ahead');
|
|
697
676
|
armedTap(yes, 'yes', 'tap again to say yes', function () {
|
|
698
677
|
api('/bet/yes', { confirm: true }).then(function (r) { moveOn(r.ack || r.message || 'sent'); });
|
|
699
678
|
});
|
|
700
679
|
act.appendChild(yes);
|
|
701
680
|
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
702
|
-
|
|
703
|
-
|
|
681
|
+
} else if (s.kind === 'wait') {
|
|
682
|
+
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
683
|
+
} else {
|
|
684
|
+
var ok = el('button', 'pill', 'nice');
|
|
685
|
+
ok.type = 'button';
|
|
686
|
+
ok.addEventListener('click', function () {
|
|
687
|
+
buzz();
|
|
688
|
+
api('/bet/nice', {}).then(function (r) { moveOn(r.ack || 'nice.'); });
|
|
689
|
+
});
|
|
690
|
+
act.appendChild(ok);
|
|
704
691
|
}
|
|
705
|
-
box.appendChild(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
692
|
+
box.appendChild(act);
|
|
693
|
+
box.appendChild(sayBox());
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// no bet was ready: one question, then what is going on and why the answer matters
|
|
697
|
+
function drawQuestionCard(s) {
|
|
698
|
+
var box = document.getElementById('bet');
|
|
699
|
+
box.textContent = '';
|
|
700
|
+
var q = s.question;
|
|
701
|
+
box.appendChild(el('h1', null, q.ask));
|
|
702
|
+
if (q.line) box.appendChild(el('p', 'why', q.line));
|
|
703
|
+
var act = el('div', 'act answers');
|
|
704
|
+
q.choices.forEach(function (c) {
|
|
705
|
+
var b = el('button', 'choice', c.label);
|
|
706
|
+
b.type = 'button';
|
|
707
|
+
b.addEventListener('click', function () {
|
|
708
|
+
buzz();
|
|
709
|
+
if (c.other) { ownAnswer(act, c); return; }
|
|
710
|
+
act.querySelectorAll('button').forEach(function (x) { x.disabled = true; });
|
|
711
|
+
api('/bet/answer', { choice: c.id }).then(function (r) {
|
|
712
|
+
if (r.ok) { moveOn(r.ack); return; }
|
|
713
|
+
act.replaceWith(el('p', 'mission', r.message || 'that did not go through'));
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
act.appendChild(b);
|
|
714
717
|
});
|
|
715
|
-
act.appendChild(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
718
|
+
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
719
|
+
box.appendChild(act);
|
|
720
|
+
box.appendChild(sayBox());
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// "something else": the owner's own words, one box and one tap to save
|
|
724
|
+
function ownAnswer(act, c) {
|
|
725
|
+
var input = el('input', 'say');
|
|
726
|
+
input.type = 'text';
|
|
727
|
+
input.maxLength = 60;
|
|
728
|
+
input.setAttribute('aria-label', c.label);
|
|
729
|
+
var save = el('button', 'pill', 'save');
|
|
730
|
+
save.type = 'button';
|
|
731
|
+
var go = function () {
|
|
732
|
+
var text = input.value.trim();
|
|
733
|
+
if (!text) return;
|
|
734
|
+
save.disabled = true;
|
|
735
|
+
api('/bet/answer', { choice: 'other', text: text }).then(function (r) {
|
|
736
|
+
if (r.ok) { moveOn(r.ack); return; }
|
|
737
|
+
save.disabled = false;
|
|
738
|
+
input.value = '';
|
|
739
|
+
input.placeholder = r.message || 'that did not go through';
|
|
722
740
|
});
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
741
|
+
};
|
|
742
|
+
save.addEventListener('click', go);
|
|
743
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
|
744
|
+
act.textContent = '';
|
|
745
|
+
act.appendChild(input);
|
|
746
|
+
act.appendChild(save);
|
|
747
|
+
input.focus();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// the owner's own number, once a week: one box and one tap to save
|
|
751
|
+
function drawReportCard(s) {
|
|
752
|
+
var box = document.getElementById('bet');
|
|
753
|
+
box.textContent = '';
|
|
754
|
+
box.appendChild(el('h1', null, s.report.what));
|
|
755
|
+
box.appendChild(el('p', 'why', s.report.why));
|
|
756
|
+
var act = el('div', 'act answers');
|
|
757
|
+
var input = el('input', 'say');
|
|
758
|
+
input.type = 'number';
|
|
759
|
+
input.inputMode = 'decimal';
|
|
760
|
+
input.setAttribute('aria-label', s.report.what);
|
|
761
|
+
var save = el('button', 'pill', 'save');
|
|
762
|
+
save.type = 'button';
|
|
763
|
+
var go = function () {
|
|
764
|
+
if (!String(input.value).trim()) return;
|
|
765
|
+
save.disabled = true;
|
|
766
|
+
api('/bet/report', { value: input.value }).then(function (r) {
|
|
767
|
+
if (r.ok) { moveOn(r.ack); return; }
|
|
768
|
+
save.disabled = false;
|
|
769
|
+
input.value = '';
|
|
770
|
+
input.placeholder = r.message || 'that did not go through';
|
|
771
|
+
});
|
|
772
|
+
};
|
|
773
|
+
save.addEventListener('click', go);
|
|
774
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
|
775
|
+
act.appendChild(input);
|
|
776
|
+
act.appendChild(save);
|
|
777
|
+
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
726
778
|
box.appendChild(act);
|
|
727
779
|
}
|
|
728
780
|
|
|
781
|
+
// A conversation in one box. While the words are read the reply spot shows
|
|
782
|
+
// a quiet ellipsis; a question back is answered in place, with the turns so
|
|
783
|
+
// far above it and up to three suggested replies to tap. Starting a member
|
|
784
|
+
// who is not running takes a second tap, or it is kept as a note.
|
|
785
|
+
// post(body) sends it; done(ack) runs when something real came of it.
|
|
786
|
+
function convo(post, done, placeholder) {
|
|
787
|
+
var wrap = el('div', 'links sayline');
|
|
788
|
+
var thread = el('div', 'thread');
|
|
789
|
+
var reply = el('p', 'why reply');
|
|
790
|
+
reply.hidden = true;
|
|
791
|
+
var input = el('input', 'say');
|
|
792
|
+
input.type = 'text';
|
|
793
|
+
input.maxLength = 280;
|
|
794
|
+
input.setAttribute('aria-label', placeholder);
|
|
795
|
+
input.placeholder = placeholder;
|
|
796
|
+
var send = el('button', 'pill', 'send');
|
|
797
|
+
send.type = 'button';
|
|
798
|
+
var extra = el('div', 'links');
|
|
799
|
+
var pending = null;
|
|
800
|
+
// an armed second tap belongs to the words it was asked about: any edit clears it
|
|
801
|
+
function disarm() {
|
|
802
|
+
pending = null;
|
|
803
|
+
send.textContent = 'send';
|
|
804
|
+
send.className = 'pill';
|
|
805
|
+
extra.textContent = '';
|
|
806
|
+
reply.textContent = '';
|
|
807
|
+
reply.hidden = true;
|
|
808
|
+
}
|
|
809
|
+
function drawThread(turns) {
|
|
810
|
+
thread.textContent = '';
|
|
811
|
+
turns.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
|
|
812
|
+
}
|
|
813
|
+
function go(more, said) {
|
|
814
|
+
var text = said || input.value.trim();
|
|
815
|
+
if (!text) return;
|
|
816
|
+
send.disabled = true;
|
|
817
|
+
reply.hidden = false;
|
|
818
|
+
reply.textContent = '\u2026';
|
|
819
|
+
extra.textContent = '';
|
|
820
|
+
var body = { text: text };
|
|
821
|
+
if (more) Object.keys(more).forEach(function (k) { body[k] = more[k]; });
|
|
822
|
+
post(body).then(function (r) {
|
|
823
|
+
if (r.ok && !r.stay) { done(r.ack); return; }
|
|
824
|
+
send.disabled = false;
|
|
825
|
+
if (r.stay) { reply.textContent = r.reply;
|
|
826
|
+
input.value = '';
|
|
827
|
+
drawThread(r.thread.slice(0, -1));
|
|
828
|
+
(r.suggestions || []).forEach(function (sug) {
|
|
829
|
+
var b = el('button', 'quiet', sug);
|
|
830
|
+
b.type = 'button';
|
|
831
|
+
b.addEventListener('click', function () { buzz(); go(null, sug); });
|
|
832
|
+
extra.appendChild(b);
|
|
833
|
+
});
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (r.reason === 'needs_confirm') {
|
|
837
|
+
pending = text;
|
|
838
|
+
reply.textContent = r.message;
|
|
839
|
+
send.textContent = 'tap again';
|
|
840
|
+
send.className = 'pill armed';
|
|
841
|
+
var keep = el('button', 'quiet', 'keep it as a note instead');
|
|
842
|
+
keep.type = 'button';
|
|
843
|
+
keep.addEventListener('click', function () { disarm(); go({ note: true }); });
|
|
844
|
+
extra.appendChild(keep);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
reply.textContent = r.message || 'that did not go through';
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
send.addEventListener('click', function () {
|
|
851
|
+
buzz();
|
|
852
|
+
if (pending) { disarm(); go({ confirm: true }); return; }
|
|
853
|
+
go();
|
|
854
|
+
});
|
|
855
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); send.click(); } });
|
|
856
|
+
input.addEventListener('input', function () { if (pending) disarm(); });
|
|
857
|
+
wrap.appendChild(thread);
|
|
858
|
+
wrap.appendChild(reply);
|
|
859
|
+
wrap.appendChild(input);
|
|
860
|
+
wrap.appendChild(send);
|
|
861
|
+
wrap.appendChild(extra);
|
|
862
|
+
return wrap;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// a quiet line under every card: tap it for one box. on a question it is the
|
|
866
|
+
// answer, on an idea a note to rework it, on a yes or a finished job a word
|
|
867
|
+
// to that member; the server reads it first, and a question back is answered
|
|
868
|
+
function sayBox() {
|
|
869
|
+
var wrap = el('div', 'links sayline');
|
|
870
|
+
var open = el('button', 'quiet', 'or say something');
|
|
871
|
+
open.type = 'button';
|
|
872
|
+
open.addEventListener('click', function () {
|
|
873
|
+
var box = sayConvo();
|
|
874
|
+
wrap.replaceWith(box);
|
|
875
|
+
box.querySelector('input').focus();
|
|
876
|
+
});
|
|
877
|
+
wrap.appendChild(open);
|
|
878
|
+
return wrap;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function sayConvo() {
|
|
882
|
+
return convo(function (body) { return api('/bet/say', body); }, moveOn, 'say something');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// two or three ideas side by side: one tap picks one and opens its full card
|
|
886
|
+
function drawChoiceCard(s) {
|
|
887
|
+
var box = document.getElementById('bet');
|
|
888
|
+
box.textContent = '';
|
|
889
|
+
box.appendChild(el('h1', null, 'which one next?'));
|
|
890
|
+
var list = el('div', 'options');
|
|
891
|
+
s.choice.options.forEach(function (o) {
|
|
892
|
+
var b = el('button', 'option');
|
|
893
|
+
b.type = 'button';
|
|
894
|
+
b.appendChild(el('span', 't', o.title));
|
|
895
|
+
b.appendChild(el('span', 'w', o.why));
|
|
896
|
+
b.addEventListener('click', function () {
|
|
897
|
+
buzz();
|
|
898
|
+
list.querySelectorAll('button').forEach(function (x) { x.disabled = true; });
|
|
899
|
+
api('/bet/pick', { id: o.id }).then(function (r) {
|
|
900
|
+
if (r.http === 200 && r.bet) { bet.busy = true; turn(r); return; }
|
|
901
|
+
tapOrb(false);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
list.appendChild(b);
|
|
905
|
+
});
|
|
906
|
+
box.appendChild(list);
|
|
907
|
+
box.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
908
|
+
box.appendChild(sayBox());
|
|
909
|
+
}
|
|
910
|
+
|
|
729
911
|
function drawBetCard(s) {
|
|
912
|
+
if (s.kind === 'report') { drawReportCard(s); return; }
|
|
913
|
+
if (s.kind === 'choice') { drawChoiceCard(s); return; }
|
|
914
|
+
if (s.kind === 'question') { drawQuestionCard(s); return; }
|
|
730
915
|
if (s.kind && s.kind !== 'bet') { drawJobCard(s); return; }
|
|
731
916
|
var box = document.getElementById('bet');
|
|
732
917
|
box.textContent = '';
|
|
733
918
|
var b = s.bet;
|
|
734
919
|
box.appendChild(el('h1', null, b.title));
|
|
735
|
-
// what is going on and what we expect,
|
|
920
|
+
// why it matters: what is going on and what we expect, in the same big type
|
|
736
921
|
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
922
|
if (s.mission) { box.appendChild(el('p', 'mission', s.mission.line)); return; }
|
|
750
|
-
if (s.status === 'starting') { box.appendChild(el('p', 'mission', 'starting
|
|
923
|
+
if (s.status === 'starting') { box.appendChild(el('p', 'mission', 'starting now')); return; }
|
|
751
924
|
var act = el('div', 'act');
|
|
752
925
|
var go = el('button', 'pill' + (bet.armed ? ' armed' : ''), bet.armed ? 'tap again to start' : 'do it');
|
|
753
926
|
go.type = 'button';
|
|
754
|
-
go.setAttribute('aria-label', bet.armed ? 'tap again to start the team on this
|
|
927
|
+
go.setAttribute('aria-label', bet.armed ? 'tap again to start the team on this idea' : 'do it. starts the team on the steps; your yes steps wait for you');
|
|
755
928
|
go.addEventListener('click', function () {
|
|
756
929
|
buzz();
|
|
757
930
|
if (!bet.armed) {
|
|
@@ -784,6 +957,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
784
957
|
links.appendChild(skip);
|
|
785
958
|
act.appendChild(links);
|
|
786
959
|
box.appendChild(act);
|
|
960
|
+
box.appendChild(sayBox());
|
|
787
961
|
}
|
|
788
962
|
|
|
789
963
|
// the time from the tap to the card fully on screen, kept on the page so
|
|
@@ -833,6 +1007,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
833
1007
|
box.classList.remove('quick', 'up');
|
|
834
1008
|
box.classList.add('leave');
|
|
835
1009
|
var swap = function () {
|
|
1010
|
+
box.style.minHeight = '';
|
|
836
1011
|
box.hidden = true;
|
|
837
1012
|
box.classList.remove('leave');
|
|
838
1013
|
document.body.classList.remove('has-card');
|
|
@@ -842,34 +1017,61 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
842
1017
|
orb.melt(then, to, swap);
|
|
843
1018
|
}
|
|
844
1019
|
|
|
845
|
-
// what the page does with the server's answer once the sphere is ready
|
|
846
|
-
//
|
|
1020
|
+
// what the page does with the server's answer once the sphere is ready.
|
|
1021
|
+
// Every tap comes back with a card, so the orb plays straight into it.
|
|
847
1022
|
function reveal(s, fast) {
|
|
848
1023
|
bet.status = s.status;
|
|
849
|
-
if ((s.bet || s.job) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
1024
|
+
if ((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
850
1025
|
line.set('', false);
|
|
851
1026
|
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(s, true, fast); } }, fast);
|
|
852
1027
|
return;
|
|
853
1028
|
}
|
|
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
1029
|
bet.busy = false;
|
|
1030
|
+
orb.relax();
|
|
868
1031
|
line.set('tap to think', false);
|
|
869
1032
|
}
|
|
870
1033
|
|
|
871
|
-
|
|
872
|
-
|
|
1034
|
+
function isCard(s) {
|
|
1035
|
+
return Boolean((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting'));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Card to card: one calm turn in place. The small orb stays where it is.
|
|
1039
|
+
// The box holds its height while the old card lifts and fades for
|
|
1040
|
+
// TURN_OUT_MS, then the new card settles up from DRIFT below over
|
|
1041
|
+
// TURN_IN_MS while the box eases to its new height. No layout jump.
|
|
1042
|
+
var TURN_OUT_MS = 220, TURN_IN_MS = 360, DRIFT = 10;
|
|
1043
|
+
function turn(s) {
|
|
1044
|
+
var box = document.getElementById('bet');
|
|
1045
|
+
if (reduced) { box.style.minHeight = ''; bet.last = s; bet.drawn = s.id; drawBetCard(s); bet.busy = false; measured(); return; }
|
|
1046
|
+
var from = box.offsetHeight;
|
|
1047
|
+
box.style.height = from + 'px';
|
|
1048
|
+
box.style.minHeight = '';
|
|
1049
|
+
box.classList.remove('quick', 'settle');
|
|
1050
|
+
box.classList.add('turning', 'turn-out');
|
|
1051
|
+
setTimeout(function () {
|
|
1052
|
+
bet.last = s;
|
|
1053
|
+
bet.drawn = s.id;
|
|
1054
|
+
drawBetCard(s);
|
|
1055
|
+
box.style.height = 'auto';
|
|
1056
|
+
var to = box.offsetHeight;
|
|
1057
|
+
box.style.height = from + 'px';
|
|
1058
|
+
box.classList.remove('turning', 'turn-out');
|
|
1059
|
+
box.classList.add('turn-in');
|
|
1060
|
+
void box.offsetHeight;
|
|
1061
|
+
box.classList.add('turning', 'settle');
|
|
1062
|
+
box.classList.remove('turn-in');
|
|
1063
|
+
box.style.height = to + 'px';
|
|
1064
|
+
setTimeout(function () {
|
|
1065
|
+
box.classList.remove('turning', 'settle');
|
|
1066
|
+
box.style.height = '';
|
|
1067
|
+
bet.busy = false;
|
|
1068
|
+
measured();
|
|
1069
|
+
}, TURN_IN_MS + 20);
|
|
1070
|
+
}, TURN_OUT_MS);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// one tap: buzz at once and ask the server for the next card. From the
|
|
1074
|
+
// sphere the orb plays into it; with a card up it is one calm turn.
|
|
873
1075
|
function tapOrb(sharper) {
|
|
874
1076
|
if (bet.busy) return;
|
|
875
1077
|
buzz();
|
|
@@ -877,14 +1079,12 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
877
1079
|
bet.armed = false;
|
|
878
1080
|
bet.tapAt = performance.now();
|
|
879
1081
|
document.body.removeAttribute('data-reveal-ms');
|
|
880
|
-
var
|
|
881
|
-
|
|
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];
|
|
1082
|
+
var onCard = document.body.classList.contains('has-card');
|
|
1083
|
+
api('/bet/think', { sharper: Boolean(sharper) }).then(function (s) {
|
|
887
1084
|
if (s.http !== 200) { bet.busy = false; orb.relax(); line.set('that did not go through. tap to try again', true); return; }
|
|
1085
|
+
bet.status = s.status;
|
|
1086
|
+
if (onCard && isCard(s)) { turn(s); return; }
|
|
1087
|
+
if (onCard) { dissolve(function () { reveal(s, true); }); return; }
|
|
888
1088
|
reveal(s, true);
|
|
889
1089
|
});
|
|
890
1090
|
}
|
|
@@ -892,16 +1092,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
892
1092
|
function loadBet() {
|
|
893
1093
|
return api('/bet').then(function (s) {
|
|
894
1094
|
if (s.http !== 200) return;
|
|
895
|
-
var was = bet.status;
|
|
896
1095
|
bet.status = s.status;
|
|
897
|
-
if (s.status === '
|
|
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(); }
|
|
1096
|
+
if ((s.bet || s.job || s.question || s.choice || s.report) && bet.drawn === s.id) { bet.last = s; drawBetCard(s); if (s.status === 'starting') poll(); }
|
|
905
1097
|
});
|
|
906
1098
|
}
|
|
907
1099
|
|
|
@@ -911,19 +1103,16 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
911
1103
|
setTimeout(function () { bet.polling = false; loadBet(); }, 400);
|
|
912
1104
|
}
|
|
913
1105
|
|
|
914
|
-
// first look: a
|
|
915
|
-
// shows its progress, anything else is the sphere
|
|
1106
|
+
// first look: a card already on screen shows settled, anything else is the sphere
|
|
916
1107
|
function firstLook() {
|
|
917
1108
|
api('/bet').then(function (s) {
|
|
918
1109
|
if (s.http !== 200) return;
|
|
919
1110
|
bet.status = s.status;
|
|
920
|
-
if ((s.bet || s.job) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
1111
|
+
if ((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
921
1112
|
orb.settled();
|
|
922
1113
|
showCard(s, false);
|
|
923
1114
|
return;
|
|
924
1115
|
}
|
|
925
|
-
if (s.status === 'thinking') { bet.busy = true; reveal(s); return; }
|
|
926
|
-
if (s.status === 'failed') { line.set(failLine(s.error), true); return; }
|
|
927
1116
|
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
928
1117
|
});
|
|
929
1118
|
}
|
|
@@ -937,156 +1126,70 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
937
1126
|
});
|
|
938
1127
|
});
|
|
939
1128
|
|
|
940
|
-
// ----
|
|
1129
|
+
// ---- the team: who is on what, from real state, and a word to any of them ----
|
|
941
1130
|
|
|
942
|
-
var
|
|
943
|
-
document.getElementById('
|
|
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'; });
|
|
1131
|
+
var teamBox = document.getElementById('team');
|
|
1132
|
+
var teamInner = document.getElementById('team-inner');
|
|
950
1133
|
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1134
|
+
function teamHead(title) {
|
|
1135
|
+
teamInner.textContent = '';
|
|
1136
|
+
var head = el('div', 'head');
|
|
1137
|
+
head.appendChild(el('h1', null, title));
|
|
1138
|
+
var close = el('button', 'quiet', 'close');
|
|
1139
|
+
close.type = 'button';
|
|
1140
|
+
close.addEventListener('click', function () { teamBox.hidden = true; });
|
|
1141
|
+
head.appendChild(close);
|
|
1142
|
+
teamInner.appendChild(head);
|
|
955
1143
|
}
|
|
956
1144
|
|
|
957
|
-
function
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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;
|
|
1145
|
+
function openTeam() {
|
|
1146
|
+
teamBox.hidden = false;
|
|
1147
|
+
teamHead('the team');
|
|
1148
|
+
api('/bet/team').then(function (s) { if (s.http === 200) drawTeam(s.rows || []); });
|
|
970
1149
|
}
|
|
971
1150
|
|
|
972
|
-
function
|
|
973
|
-
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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();
|
|
1151
|
+
function drawTeam(rows) {
|
|
1152
|
+
teamHead('the team');
|
|
1153
|
+
if (!rows.length) { teamInner.appendChild(el('p', 'why', 'nobody has had work in the last day.')); return; }
|
|
1154
|
+
rows.forEach(function (r) {
|
|
1155
|
+
var row = el('button', 'row');
|
|
1156
|
+
row.type = 'button';
|
|
1157
|
+
row.appendChild(el('span', 'who', r.member));
|
|
1158
|
+
row.appendChild(el('span', 'st ' + r.status.replace(/ /g, '-'), r.status));
|
|
1159
|
+
row.appendChild(el('span', 'on', r.on));
|
|
1160
|
+
row.appendChild(el('span', 'time', r.time));
|
|
1161
|
+
row.addEventListener('click', function () { drawTalk(r); });
|
|
1162
|
+
teamInner.appendChild(row);
|
|
989
1163
|
});
|
|
990
1164
|
}
|
|
991
1165
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
var
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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);
|
|
1166
|
+
// a talk card: what the member is on, and a word to steer them
|
|
1167
|
+
function drawTalk(r) {
|
|
1168
|
+
teamHead(r.member);
|
|
1169
|
+
teamInner.appendChild(el('p', 'why', r.status + ': ' + r.on + '. ' + r.time + '.'));
|
|
1170
|
+
var talkBox = convo(function (body) { body.member = r.member; return api('/bet/steer', body); }, function (ack) {
|
|
1171
|
+
talkBox.replaceWith(el('p', 'mission', ack));
|
|
1172
|
+
}, 'tell ' + r.member + ' something');
|
|
1173
|
+
var back = el('button', 'quiet', 'back to the team');
|
|
1174
|
+
back.type = 'button';
|
|
1175
|
+
back.addEventListener('click', openTeam);
|
|
1176
|
+
teamInner.appendChild(talkBox);
|
|
1177
|
+
teamInner.appendChild(back);
|
|
1033
1178
|
}
|
|
1034
1179
|
|
|
1035
|
-
|
|
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
|
-
}
|
|
1180
|
+
document.getElementById('working').addEventListener('click', openTeam);
|
|
1042
1181
|
|
|
1043
|
-
|
|
1182
|
+
// the footer: how many members are really working, or the team when none are
|
|
1183
|
+
function drawWorking(n) {
|
|
1044
1184
|
var btn = document.getElementById('working');
|
|
1045
|
-
|
|
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;
|
|
1185
|
+
btn.textContent = n ? n + ' working' : 'the team';
|
|
1079
1186
|
}
|
|
1080
1187
|
|
|
1081
1188
|
function refresh() {
|
|
1082
|
-
return api('/
|
|
1189
|
+
return api('/bet').then(function (s) {
|
|
1083
1190
|
if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
draw();
|
|
1087
|
-
}).catch(function () {
|
|
1088
|
-
document.getElementById('live').textContent = 'cannot reach the mac right now. retrying.';
|
|
1089
|
-
});
|
|
1191
|
+
drawWorking(s.team_working || 0);
|
|
1192
|
+
}).catch(function () { /* the next refresh tries again */ });
|
|
1090
1193
|
}
|
|
1091
1194
|
|
|
1092
1195
|
firstLook();
|