atris 3.72.5 → 3.73.1

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.
@@ -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 decisions only the owner can make sit behind one
17
- // quiet link at the bottom.
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: manipulation; cursor: pointer; }
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-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; }
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: 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; }
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-3); font-size: 15px; }
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
- .mission { margin: 32px 0 0; padding: 16px 0 0; border-top: 1px solid var(--line); color: var(--text-2); text-align: center; }
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
- /* ---- 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; }
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 bet for the business"></canvas>
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="open-only" hidden></button><button type="button" class="quiet" id="working" hidden aria-expanded="false"></button><div class="jobs" id="jobs" hidden></div></div>
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 bet">
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="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>
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
- var light = { x: -0.5, y: -0.6 };
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
- d.vx += (cx + rx * R - d.sx) * SPRING;
360
- d.vy += (cy + d.y * R - d.sy) * SPRING;
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
- 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
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
- 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
-
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
- box.appendChild(act);
703
- return;
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(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.'); });
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(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'); });
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
- links.appendChild(back);
724
- act.appendChild(links);
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, as sentences with the numbers inside
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 the mission')); return; }
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 bet' : 'do it. starts the team on the steps; your yes steps wait for you');
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
- // fast: the bet was waiting in the queue; otherwise it was just made
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
- // one tap: buzz at once, ask the server for the next bet, and play the
872
- // choreography; a card on screen dissolves first
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 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];
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 === '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(); }
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 bet already on screen shows settled, a bet being made
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
- // ---- only you: the decision deck, behind one quiet link ---------------------------
1129
+ // ---- the team: who is on what, from real state, and a word to any of them ----
941
1130
 
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'; });
1131
+ var teamBox = document.getElementById('team');
1132
+ var teamInner = document.getElementById('team-inner');
950
1133
 
951
- function say(text, done) {
952
- var reply = document.getElementById('reply');
953
- reply.hidden = !text;
954
- reply.textContent = text || '';
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 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;
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 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();
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
- 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);
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
- 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
- }
1180
+ document.getElementById('working').addEventListener('click', openTeam);
1042
1181
 
1043
- function drawWorking(jobs) {
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
- 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;
1185
+ btn.textContent = n ? n + ' working' : 'the team';
1079
1186
  }
1080
1187
 
1081
1188
  function refresh() {
1082
- return api('/state').then(function (s) {
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
- 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
- });
1191
+ drawWorking(s.team_working || 0);
1192
+ }).catch(function () { /* the next refresh tries again */ });
1090
1193
  }
1091
1194
 
1092
1195
  firstLook();