atris 3.73.1 → 3.74.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.
- package/bin/atris.js +85 -82
- package/commands/aeo.js +2 -2
- package/commands/align.js +10 -19
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +4 -8
- package/commands/avail.js +36 -7
- package/commands/computer.js +3 -1
- package/commands/console.js +1 -1
- package/commands/context-sync.js +1 -60
- package/commands/fleet.js +0 -1
- package/commands/lifecycle.js +17 -6
- package/commands/log-sync.js +1 -1
- package/commands/member.js +158 -18
- package/commands/mission.js +157 -69
- package/commands/now.js +0 -7
- package/commands/plugin.js +1 -1
- package/commands/pull.js +1 -1
- package/commands/push.js +0 -1
- package/commands/radar.js +0 -4
- package/commands/recap.js +25 -1
- package/commands/report.js +0 -4
- package/commands/review.js +1 -1
- package/commands/setup.js +6 -2
- package/commands/slop.js +8 -2
- package/commands/task.js +0 -522
- package/commands/terminal.js +1 -1
- package/commands/youtube.js +4 -6
- package/commands/zen.js +4 -4
- package/lib/agent-today.js +412 -0
- package/lib/autoland.js +0 -8
- package/lib/bench/runner.js +0 -4
- package/lib/chat-log-scan.js +0 -28
- package/lib/cli-scope.js +4 -2
- package/lib/engine-registry.js +0 -4
- package/lib/falsifier-probe.js +2 -1
- package/lib/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +125 -30
- package/lib/gm-phone-page.js +398 -99
- package/lib/gm-phone.js +30 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/review-integrity.js +0 -4
- package/lib/spawn-env.js +92 -3
- package/lib/task-proof.js +2 -1
- package/lib/task-receipt.js +3 -2
- package/lib/yes-lock.js +633 -81
- package/package.json +1 -1
package/lib/gm-phone-page.js
CHANGED
|
@@ -86,6 +86,8 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
86
86
|
.team .time { grid-column: 1 / -1; font-size: 17px; color: var(--text-2); }
|
|
87
87
|
.sayline { margin: 10px 0 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
88
88
|
.sayline .thread, .sayline .reply { width: 100%; text-align: left; margin: 0; }
|
|
89
|
+
.unsent { margin: 0; text-align: center; }
|
|
90
|
+
.act > .links:empty { display: none; }
|
|
89
91
|
.turn { margin: 0 0 6px; font-size: 18px; line-height: 1.4; }
|
|
90
92
|
.turn.you { color: var(--text-2); }
|
|
91
93
|
.turn.orb { color: var(--text); }
|
|
@@ -127,12 +129,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
127
129
|
.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); }
|
|
128
130
|
.bet.up { opacity: 1; transform: none; }
|
|
129
131
|
.bet.quick { transition-duration: 0.4s, 0.5s; }
|
|
130
|
-
/* card to card:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.bet.
|
|
135
|
-
.bet.
|
|
132
|
+
/* card to card: a crossfade in place. the old card is a still copy that
|
|
133
|
+
fades over the box while the new card fades in under it from a small
|
|
134
|
+
drift, its title landing where the old title was; the box eases to the
|
|
135
|
+
new height */
|
|
136
|
+
.bet.ghost { position: absolute; z-index: 1; margin: 0; pointer-events: none; transition: opacity 0.2s ease-out; }
|
|
137
|
+
.bet.ghost.gone { opacity: 0; }
|
|
138
|
+
.bet.turning { overflow: hidden; transition: opacity 0.32s ease-out 0.08s, transform 0.32s cubic-bezier(0.22, 1, 0.36, 1) 0.08s, height 0.36s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
139
|
+
.bet.turn-in { opacity: 0; transform: translateY(4px); transition: none; }
|
|
136
140
|
/* leaving: a soft fade in place, never a cut */
|
|
137
141
|
.bet.leave { opacity: 0; transform: none; transition: opacity 0.4s ease-out; }
|
|
138
142
|
.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; }
|
|
@@ -159,6 +163,20 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
159
163
|
/* an answer to a question card */
|
|
160
164
|
.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; }
|
|
161
165
|
|
|
166
|
+
/* typing in place: the tapped answer becomes this box at its own size and
|
|
167
|
+
spot, the cursor in it, and the rest of the card dims and stays put */
|
|
168
|
+
.field { position: relative; display: block; width: 100%; padding: 0; border: 1px solid var(--line); border-radius: 14px; color: var(--text); }
|
|
169
|
+
.field input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0 76px 0 16px; border: 0; border-radius: 14px; background: transparent; color: var(--text); font: inherit; outline: none; }
|
|
170
|
+
.field input::placeholder { color: var(--text-2); opacity: 1; }
|
|
171
|
+
.field:focus-within { border-color: var(--text-2); }
|
|
172
|
+
.field .send { position: absolute; top: 0; right: 0; bottom: 0; min-width: 64px; padding: 0 14px; color: var(--text-2); font-size: 17px; text-decoration: none; }
|
|
173
|
+
.sayline .field input { padding: 0 76px; text-align: center; }
|
|
174
|
+
/* a chosen answer holds its spot with an amber edge, and the card takes no taps */
|
|
175
|
+
.choice.chosen, .option.chosen, .field.chosen { border-color: var(--amber); background: rgba(255, 140, 66, 0.1); }
|
|
176
|
+
.chosen .send { visibility: hidden; }
|
|
177
|
+
.bet.holding { pointer-events: none; }
|
|
178
|
+
.muted { opacity: 0.35; transition: opacity 0.2s ease; }
|
|
179
|
+
|
|
162
180
|
@media (prefers-reduced-motion: reduce) {
|
|
163
181
|
.lava .tx, .lava .ty, .lava .blob { animation: none !important; }
|
|
164
182
|
.bet { transform: none; transition: opacity 0.2s linear; }
|
|
@@ -175,7 +193,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
175
193
|
<article class="bet" id="bet" hidden></article>
|
|
176
194
|
<div class="foot" id="foot"><button type="button" class="quiet" id="working" aria-haspopup="dialog">the team</button></div>
|
|
177
195
|
</main>
|
|
178
|
-
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap
|
|
196
|
+
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap to go back to the big orb">
|
|
179
197
|
<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>
|
|
180
198
|
<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>
|
|
181
199
|
<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>
|
|
@@ -624,7 +642,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
624
642
|
})();
|
|
625
643
|
|
|
626
644
|
// ---- the bet ------------------------------------------------------------------
|
|
627
|
-
var bet = { status: null, polling: false, armed: false, drawn: null, busy: false, last: null };
|
|
645
|
+
var bet = { status: null, polling: false, armed: false, drawn: null, busy: false, last: null, watching: null };
|
|
628
646
|
// how long "builder has it" (or any ack) stays before the next card
|
|
629
647
|
var AFTER_MS = 1500;
|
|
630
648
|
|
|
@@ -668,7 +686,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
668
686
|
box.textContent = '';
|
|
669
687
|
var j = s.job;
|
|
670
688
|
box.appendChild(el('h1', null, j.what));
|
|
671
|
-
box.appendChild(el('p', 'why', j.why));
|
|
689
|
+
if (j.why) box.appendChild(el('p', 'why', j.why));
|
|
672
690
|
var act = el('div', 'act');
|
|
673
691
|
if (s.kind === 'yes') {
|
|
674
692
|
var yes = el('button', 'pill', 'yes');
|
|
@@ -677,9 +695,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
677
695
|
api('/bet/yes', { confirm: true }).then(function (r) { moveOn(r.ack || r.message || 'sent'); });
|
|
678
696
|
});
|
|
679
697
|
act.appendChild(yes);
|
|
680
|
-
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
681
698
|
} else if (s.kind === 'wait') {
|
|
682
|
-
|
|
699
|
+
// one calm line: the idea turns in on its own once it is written
|
|
700
|
+
watchWait();
|
|
701
|
+
box.appendChild(sayBox());
|
|
702
|
+
return;
|
|
683
703
|
} else {
|
|
684
704
|
var ok = el('button', 'pill', 'nice');
|
|
685
705
|
ok.type = 'button';
|
|
@@ -693,6 +713,31 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
693
713
|
box.appendChild(sayBox());
|
|
694
714
|
}
|
|
695
715
|
|
|
716
|
+
// While the calm line shows, look every WAIT_LOOK_MS for the idea; once the
|
|
717
|
+
// server has one ready, it is asked for the way a tap does and turns in
|
|
718
|
+
// with one crossfade. No tap, and nothing moves on the page while it
|
|
719
|
+
// waits. It also waits while the owner types there (the box has the
|
|
720
|
+
// cursor or any words), so it comes in after they send, or once they
|
|
721
|
+
// clear the box and leave it; and on the big orb, it waits for their tap.
|
|
722
|
+
var WAIT_LOOK_MS = 1500;
|
|
723
|
+
function typingOnCard() {
|
|
724
|
+
var input = document.getElementById('bet').querySelector('.field input');
|
|
725
|
+
return Boolean(input && (input === document.activeElement || input.value.trim()));
|
|
726
|
+
}
|
|
727
|
+
function watchWait() {
|
|
728
|
+
if (bet.watching) return;
|
|
729
|
+
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
730
|
+
bet.watching = setInterval(function () {
|
|
731
|
+
if (!bet.last || bet.last.kind !== 'wait') { clearInterval(bet.watching); bet.watching = null; return; }
|
|
732
|
+
if (!quiet()) return;
|
|
733
|
+
api('/bet').then(function (s) {
|
|
734
|
+
if (s.http !== 200 || !quiet() || !bet.last || bet.last.kind !== 'wait') return;
|
|
735
|
+
if (s.id !== bet.last.id && s.kind !== 'wait' && isCard(s)) { bet.status = s.status; turn(s); return; }
|
|
736
|
+
if (s.kind === 'wait' && s.queue > 0) think(false, 0);
|
|
737
|
+
}).catch(function () { /* the next look tries again */ });
|
|
738
|
+
}, WAIT_LOOK_MS);
|
|
739
|
+
}
|
|
740
|
+
|
|
696
741
|
// no bet was ready: one question, then what is going on and why the answer matters
|
|
697
742
|
function drawQuestionCard(s) {
|
|
698
743
|
var box = document.getElementById('bet');
|
|
@@ -705,48 +750,210 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
705
750
|
var b = el('button', 'choice', c.label);
|
|
706
751
|
b.type = 'button';
|
|
707
752
|
b.addEventListener('click', function () {
|
|
753
|
+
if (bet.busy) return;
|
|
708
754
|
buzz();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (r.ok) { moveOn(r.ack); return; }
|
|
713
|
-
act.replaceWith(el('p', 'mission', r.message || 'that did not go through'));
|
|
714
|
-
});
|
|
755
|
+
// typed words are read first, so a question back is explained and nothing is saved
|
|
756
|
+
if (c.other) { typeHere(b, c.label, 60, function (text, field) { say(field, { text: text }); }); return; }
|
|
757
|
+
answer(b, { choice: c.id });
|
|
715
758
|
});
|
|
759
|
+
if (c.other) b.classList.add('other');
|
|
716
760
|
act.appendChild(b);
|
|
717
761
|
});
|
|
718
|
-
|
|
762
|
+
// a quiet line under the answers, empty until an answer does not go through
|
|
763
|
+
act.appendChild(el('div', 'links'));
|
|
719
764
|
box.appendChild(act);
|
|
720
|
-
box
|
|
765
|
+
// its own something else box is the one place to type
|
|
766
|
+
if (!q.choices.some(function (c) { return c.other; })) box.appendChild(sayBox());
|
|
721
767
|
}
|
|
722
768
|
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
769
|
+
// ---- answering in place: the eyes stay in one spot ----------------------------
|
|
770
|
+
// A tapped answer, or the words typed in its place, holds that spot as
|
|
771
|
+
// chosen for HOLD_MS while the rest of the card dims and stays put. The
|
|
772
|
+
// next card is asked for during the hold and turns in once it is over.
|
|
773
|
+
var HOLD_MS = 400;
|
|
774
|
+
|
|
775
|
+
// a failed fetch reads like a refusal, so a hold never hangs
|
|
776
|
+
function offline() { return { ok: false, http: 0 }; }
|
|
777
|
+
|
|
778
|
+
// every answer row of the card dims but the one in use; no spot, none
|
|
779
|
+
function dimOthers(spot) {
|
|
780
|
+
var box = document.getElementById('bet');
|
|
781
|
+
box.querySelectorAll('.act > *, .options > *, .sayline, :scope > .links').forEach(function (n) {
|
|
782
|
+
n.classList.toggle('muted', Boolean(spot) && n !== spot && !n.contains(spot));
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// the spot holds as chosen and the card takes no taps; returns when it
|
|
787
|
+
// started, which is also where the time to the next card counts from
|
|
788
|
+
function hold(spot) {
|
|
789
|
+
var box = document.getElementById('bet');
|
|
790
|
+
dimOthers(spot);
|
|
791
|
+
spot.classList.add('chosen');
|
|
792
|
+
box.classList.add('holding');
|
|
793
|
+
var input = spot.querySelector('input');
|
|
794
|
+
if (input) { input.readOnly = true; input.blur(); }
|
|
795
|
+
bet.busy = true;
|
|
796
|
+
bet.armed = false;
|
|
797
|
+
bet.tapAt = performance.now();
|
|
798
|
+
document.body.removeAttribute('data-reveal-ms');
|
|
799
|
+
return bet.tapAt;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// it did not go through: the card is as it was
|
|
803
|
+
function unhold() {
|
|
804
|
+
var box = document.getElementById('bet');
|
|
805
|
+
box.classList.remove('holding');
|
|
806
|
+
dimOthers(null);
|
|
807
|
+
box.querySelectorAll('.chosen').forEach(function (n) { n.classList.remove('chosen'); });
|
|
808
|
+
bet.busy = false;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// resolves once the hold that started at heldAt has run HOLD_MS
|
|
812
|
+
function afterHold(heldAt) {
|
|
813
|
+
var left = heldAt ? heldAt + HOLD_MS - performance.now() : 0;
|
|
814
|
+
return new Promise(function (resolve) { setTimeout(resolve, Math.max(0, left)); });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// The tapped button becomes the field at its own size and spot, with the
|
|
818
|
+
// cursor already in it, and the other answers dim. Enter or send hands
|
|
819
|
+
// the words to sent(text, field). Left empty, it turns back into the button.
|
|
820
|
+
function typeHere(btn, placeholder, maxLength, sent) {
|
|
821
|
+
var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
|
|
822
|
+
// its exact height, to the subpixel, so nothing under it moves
|
|
823
|
+
field.style.height = btn.getBoundingClientRect().height + 'px';
|
|
824
|
+
var input = el('input');
|
|
726
825
|
input.type = 'text';
|
|
727
|
-
input.maxLength =
|
|
728
|
-
input.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
var
|
|
826
|
+
input.maxLength = maxLength;
|
|
827
|
+
input.placeholder = placeholder;
|
|
828
|
+
input.enterKeyHint = 'send';
|
|
829
|
+
input.setAttribute('aria-label', placeholder);
|
|
830
|
+
var send = el('button', 'send', 'send');
|
|
831
|
+
send.type = 'button';
|
|
832
|
+
var submit = function () {
|
|
732
833
|
var text = input.value.trim();
|
|
733
|
-
if (!text)
|
|
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';
|
|
740
|
-
});
|
|
834
|
+
if (text && !bet.busy) sent(text, field);
|
|
741
835
|
};
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
836
|
+
// a press on send keeps the cursor in the field, so it never closes first
|
|
837
|
+
send.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
838
|
+
send.addEventListener('click', function () { buzz(); submit(); });
|
|
839
|
+
input.addEventListener('keydown', function (e) {
|
|
840
|
+
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
841
|
+
if (e.key === 'Escape') { input.value = ''; input.blur(); }
|
|
842
|
+
});
|
|
843
|
+
input.addEventListener('blur', function () {
|
|
844
|
+
if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
845
|
+
field.replaceWith(btn);
|
|
846
|
+
dimOthers(null);
|
|
847
|
+
});
|
|
848
|
+
field.appendChild(input);
|
|
849
|
+
field.appendChild(send);
|
|
850
|
+
btn.replaceWith(field);
|
|
851
|
+
dimOthers(field);
|
|
747
852
|
input.focus();
|
|
748
853
|
}
|
|
749
854
|
|
|
855
|
+
// An answer to a question holds as chosen while it saves. If it does not
|
|
856
|
+
// go through, the card comes back as it was, the words still in the
|
|
857
|
+
// field, and why takes the hint's line under the answers.
|
|
858
|
+
function answer(spot, body) {
|
|
859
|
+
body.card = shownCard();
|
|
860
|
+
var heldAt = hold(spot);
|
|
861
|
+
api('/bet/answer', body).catch(offline).then(function (r) {
|
|
862
|
+
if (r.ok) { think(false, heldAt); return; }
|
|
863
|
+
unhold();
|
|
864
|
+
var hint = document.getElementById('bet').querySelector('.act .links');
|
|
865
|
+
if (hint) hint.textContent = r.message || 'that did not go through';
|
|
866
|
+
var input = spot.querySelector('input');
|
|
867
|
+
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// the card the owner is looking at, named with every answer and every
|
|
872
|
+
// word said, so the server acts on that card or on nothing
|
|
873
|
+
function shownCard() { return bet.last && bet.last.id ? bet.last.id : ''; }
|
|
874
|
+
|
|
875
|
+
// Words said on a card hold their spot as chosen while the server reads
|
|
876
|
+
// them. An answer or an instruction brings the next card; a question
|
|
877
|
+
// back or a second tap it needs turns in as the orb's reply, in the
|
|
878
|
+
// title's spot. If it did not send, the words stay where they were.
|
|
879
|
+
// One id per typed send: made when these words are first sent from this
|
|
880
|
+
// spot, the same on every retry of them, and new when the words change,
|
|
881
|
+
// so a send whose reply was lost is never sent twice. getRandomValues
|
|
882
|
+
// works on the plain http of the wifi link, where randomUUID does not.
|
|
883
|
+
function sendId(spot, text) {
|
|
884
|
+
if (spot.sendText !== text) {
|
|
885
|
+
var n = new Uint32Array(2);
|
|
886
|
+
crypto.getRandomValues(n);
|
|
887
|
+
spot.sendText = text;
|
|
888
|
+
spot.sendId = ('000000' + n[0].toString(36)).slice(-7) + ('000000' + n[1].toString(36)).slice(-7);
|
|
889
|
+
}
|
|
890
|
+
return spot.sendId;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function say(spot, body) {
|
|
894
|
+
body.card = shownCard();
|
|
895
|
+
body.send = sendId(spot, body.text);
|
|
896
|
+
var heldAt = hold(spot);
|
|
897
|
+
api('/bet/say', body).catch(offline).then(function (r) {
|
|
898
|
+
if (r.ok && !r.stay) { think(false, heldAt); return; }
|
|
899
|
+
if (!r.ok && r.reason !== 'needs_confirm') { unsent(spot, r.message || 'that did not send. tap send to try again'); return; }
|
|
900
|
+
afterHold(heldAt).then(function () { crossfade(function () { drawReply(r, body.text); }); });
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// It did not send: the card is as it was, the words still in the box with
|
|
905
|
+
// the cursor in it, and one short line under it says why, so Enter (or
|
|
906
|
+
// the same tap) tries again. Left empty, the box turns back into the
|
|
907
|
+
// button and the line goes with it.
|
|
908
|
+
function unsent(spot, why) {
|
|
909
|
+
unhold();
|
|
910
|
+
var note = spot.nextElementSibling;
|
|
911
|
+
var input = spot.querySelector('input');
|
|
912
|
+
if (!note || !note.classList.contains('unsent')) {
|
|
913
|
+
note = el('p', 'links unsent');
|
|
914
|
+
spot.after(note);
|
|
915
|
+
if (input) input.addEventListener('blur', function () { if (!spot.isConnected) note.remove(); });
|
|
916
|
+
}
|
|
917
|
+
note.textContent = why;
|
|
918
|
+
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// one reply to tap: it holds as chosen and is said
|
|
922
|
+
function sayChoice(label, body) {
|
|
923
|
+
var b = el('button', 'choice', label);
|
|
924
|
+
b.type = 'button';
|
|
925
|
+
b.addEventListener('click', function () {
|
|
926
|
+
if (bet.busy) return;
|
|
927
|
+
buzz();
|
|
928
|
+
say(b, body);
|
|
929
|
+
});
|
|
930
|
+
return b;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// the orb's reply is the title, with the card's own answer right under it
|
|
934
|
+
// (do it, yes, nice, or the answers), then up to three suggested replies;
|
|
935
|
+
// starting a member who is not running takes one more tap, or it is kept
|
|
936
|
+
// as a note. The say line takes anything else.
|
|
937
|
+
function drawReply(r, said) {
|
|
938
|
+
var box = document.getElementById('bet');
|
|
939
|
+
// a reply never takes the card's own answer away: the card is drawn
|
|
940
|
+
// once more, and its row is kept, buttons and all
|
|
941
|
+
var own = null;
|
|
942
|
+
if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
|
|
943
|
+
box.textContent = '';
|
|
944
|
+
bet.drawn = null;
|
|
945
|
+
box.appendChild(el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through')));
|
|
946
|
+
if (own) box.appendChild(own);
|
|
947
|
+
var act = el('div', 'act answers');
|
|
948
|
+
if (r.stay) (r.suggestions || []).forEach(function (sug) { act.appendChild(sayChoice(sug, { text: sug })); });
|
|
949
|
+
if (r.reason === 'needs_confirm') {
|
|
950
|
+
act.appendChild(sayChoice('start it', { text: said, confirm: true }));
|
|
951
|
+
act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
|
|
952
|
+
}
|
|
953
|
+
box.appendChild(act);
|
|
954
|
+
if (!own || !own.querySelector('.other')) box.appendChild(sayBox());
|
|
955
|
+
}
|
|
956
|
+
|
|
750
957
|
// the owner's own number, once a week: one box and one tap to save
|
|
751
958
|
function drawReportCard(s) {
|
|
752
959
|
var box = document.getElementById('bet');
|
|
@@ -774,8 +981,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
774
981
|
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
|
775
982
|
act.appendChild(input);
|
|
776
983
|
act.appendChild(save);
|
|
777
|
-
|
|
984
|
+
// a quiet way out: moves on, saves nothing, and is not asked again today
|
|
985
|
+
var links = el('div', 'links');
|
|
986
|
+
var later = el('button', null, 'not now');
|
|
987
|
+
later.type = 'button';
|
|
988
|
+
later.addEventListener('click', function () { if (bet.busy) return; buzz(); think(false, 0); });
|
|
989
|
+
links.appendChild(later);
|
|
990
|
+
act.appendChild(links);
|
|
778
991
|
box.appendChild(act);
|
|
992
|
+
box.appendChild(sayBox());
|
|
779
993
|
}
|
|
780
994
|
|
|
781
995
|
// A conversation in one box. While the words are read the reply spot shows
|
|
@@ -817,9 +1031,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
817
1031
|
reply.hidden = false;
|
|
818
1032
|
reply.textContent = '\u2026';
|
|
819
1033
|
extra.textContent = '';
|
|
820
|
-
|
|
1034
|
+
// the same words carry the same id on every try, so a retry never sends twice
|
|
1035
|
+
var body = { text: text, send: sendId(wrap, text) };
|
|
821
1036
|
if (more) Object.keys(more).forEach(function (k) { body[k] = more[k]; });
|
|
822
|
-
post(body).then(function (r) {
|
|
1037
|
+
post(body).catch(offline).then(function (r) {
|
|
823
1038
|
if (r.ok && !r.stay) { done(r.ack); return; }
|
|
824
1039
|
send.disabled = false;
|
|
825
1040
|
if (r.stay) { reply.textContent = r.reply;
|
|
@@ -844,7 +1059,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
844
1059
|
extra.appendChild(keep);
|
|
845
1060
|
return;
|
|
846
1061
|
}
|
|
847
|
-
|
|
1062
|
+
// it did not send: the words are in the box, send works again, and
|
|
1063
|
+
// one short line says so
|
|
1064
|
+
if (said && !input.value) input.value = said;
|
|
1065
|
+
reply.textContent = r.message || 'that did not send. tap send to try again';
|
|
1066
|
+
input.focus();
|
|
848
1067
|
});
|
|
849
1068
|
}
|
|
850
1069
|
send.addEventListener('click', function () {
|
|
@@ -862,26 +1081,22 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
862
1081
|
return wrap;
|
|
863
1082
|
}
|
|
864
1083
|
|
|
865
|
-
// a quiet line under every card: tap it
|
|
866
|
-
//
|
|
867
|
-
//
|
|
1084
|
+
// a quiet line under every card: tap it and the line becomes the box, in
|
|
1085
|
+
// its own spot. on a question it is the answer, on an idea a note to
|
|
1086
|
+
// rework it, on a yes or a finished job a word to that member; the server
|
|
1087
|
+
// reads it first, and a question back is answered
|
|
868
1088
|
function sayBox() {
|
|
869
1089
|
var wrap = el('div', 'links sayline');
|
|
870
1090
|
var open = el('button', 'quiet', 'or say something');
|
|
871
1091
|
open.type = 'button';
|
|
872
1092
|
open.addEventListener('click', function () {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
box.querySelector('input').focus();
|
|
1093
|
+
if (bet.busy) return;
|
|
1094
|
+
typeHere(open, 'say something', 280, function (text, field) { say(field, { text: text }); });
|
|
876
1095
|
});
|
|
877
1096
|
wrap.appendChild(open);
|
|
878
1097
|
return wrap;
|
|
879
1098
|
}
|
|
880
1099
|
|
|
881
|
-
function sayConvo() {
|
|
882
|
-
return convo(function (body) { return api('/bet/say', body); }, moveOn, 'say something');
|
|
883
|
-
}
|
|
884
|
-
|
|
885
1100
|
// two or three ideas side by side: one tap picks one and opens its full card
|
|
886
1101
|
function drawChoiceCard(s) {
|
|
887
1102
|
var box = document.getElementById('bet');
|
|
@@ -894,17 +1109,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
894
1109
|
b.appendChild(el('span', 't', o.title));
|
|
895
1110
|
b.appendChild(el('span', 'w', o.why));
|
|
896
1111
|
b.addEventListener('click', function () {
|
|
1112
|
+
if (bet.busy) return;
|
|
897
1113
|
buzz();
|
|
898
|
-
|
|
1114
|
+
var heldAt = hold(b);
|
|
899
1115
|
api('/bet/pick', { id: o.id }).then(function (r) {
|
|
900
|
-
if (r.http === 200 && r.bet) {
|
|
901
|
-
|
|
902
|
-
});
|
|
1116
|
+
if (r.http === 200 && r.bet) { afterHold(heldAt).then(function () { turn(r); }); return; }
|
|
1117
|
+
think(false, heldAt);
|
|
1118
|
+
}, function () { think(false, heldAt); });
|
|
903
1119
|
});
|
|
904
1120
|
list.appendChild(b);
|
|
905
1121
|
});
|
|
906
1122
|
box.appendChild(list);
|
|
907
|
-
box.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
908
1123
|
box.appendChild(sayBox());
|
|
909
1124
|
}
|
|
910
1125
|
|
|
@@ -933,13 +1148,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
933
1148
|
return;
|
|
934
1149
|
}
|
|
935
1150
|
bet.armed = false;
|
|
936
|
-
s.status = 'starting'; drawBetCard(s);
|
|
937
|
-
api('/bet/do', { confirm: true }).then(function (r) {
|
|
1151
|
+
s.status = 'starting'; s.unstarted = null; drawBetCard(s);
|
|
1152
|
+
api('/bet/do', { confirm: true }).catch(offline).then(function (r) {
|
|
1153
|
+
if (!r || !r.ok) {
|
|
1154
|
+
// it did not get going: the card comes back as it was, do it and
|
|
1155
|
+
// all, with one line why, and the page follows this card again
|
|
1156
|
+
s.status = 'ready';
|
|
1157
|
+
s.unstarted = (r && r.message) || 'that did not start. tap do it to try again';
|
|
1158
|
+
bet.drawn = s.id;
|
|
1159
|
+
drawBetCard(s);
|
|
1160
|
+
refresh();
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
938
1163
|
loadBet(); refresh();
|
|
939
|
-
|
|
1164
|
+
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
940
1165
|
});
|
|
941
1166
|
});
|
|
942
1167
|
act.appendChild(go);
|
|
1168
|
+
if (s.unstarted) act.appendChild(el('p', 'links unsent', s.unstarted));
|
|
943
1169
|
var links = el('div', 'links');
|
|
944
1170
|
var sharp = el('button', null, 'sharper idea');
|
|
945
1171
|
sharp.type = 'button';
|
|
@@ -974,7 +1200,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
974
1200
|
drawBetCard(s);
|
|
975
1201
|
document.body.classList.add('has-card');
|
|
976
1202
|
box.hidden = false;
|
|
977
|
-
box.classList.remove('leave');
|
|
1203
|
+
box.classList.remove('leave', 'holding');
|
|
978
1204
|
box.classList.toggle('quick', Boolean(fast));
|
|
979
1205
|
if (!animate) { box.classList.add('up'); measured(); return; }
|
|
980
1206
|
box.classList.remove('up');
|
|
@@ -1035,39 +1261,53 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1035
1261
|
return Boolean((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting'));
|
|
1036
1262
|
}
|
|
1037
1263
|
|
|
1038
|
-
// Card to card:
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1264
|
+
// Card to card: a crossfade in place. The small orb stays where it is.
|
|
1265
|
+
// A still copy of the old card fades over the box while the new card,
|
|
1266
|
+
// drawn under it, fades in from DRIFT below over TURN_IN_MS, starting
|
|
1267
|
+
// 80ms in, so its title lands where the old title was. The box eases to
|
|
1268
|
+
// its new height. No layout jump.
|
|
1269
|
+
var TURN_IN_MS = 320, DRIFT = 4;
|
|
1043
1270
|
function turn(s) {
|
|
1271
|
+
crossfade(function () { bet.last = s; bet.drawn = s.id; drawBetCard(s); });
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// draw() puts the next card in the box
|
|
1275
|
+
function crossfade(draw) {
|
|
1044
1276
|
var box = document.getElementById('bet');
|
|
1045
|
-
|
|
1277
|
+
box.classList.remove('holding');
|
|
1278
|
+
if (reduced) { box.style.minHeight = ''; draw(); bet.busy = false; measured(); return; }
|
|
1046
1279
|
var from = box.offsetHeight;
|
|
1047
1280
|
box.style.height = from + 'px';
|
|
1048
1281
|
box.style.minHeight = '';
|
|
1049
|
-
box.
|
|
1050
|
-
|
|
1282
|
+
var ghost = box.cloneNode(true);
|
|
1283
|
+
ghost.removeAttribute('id');
|
|
1284
|
+
ghost.classList.remove('quick', 'turning', 'turn-in');
|
|
1285
|
+
ghost.classList.add('up', 'ghost');
|
|
1286
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
1287
|
+
ghost.inert = true;
|
|
1288
|
+
var r = box.getBoundingClientRect(), p = box.offsetParent.getBoundingClientRect();
|
|
1289
|
+
ghost.style.top = (r.top - p.top) + 'px';
|
|
1290
|
+
ghost.style.left = (r.left - p.left) + 'px';
|
|
1291
|
+
ghost.style.width = r.width + 'px';
|
|
1292
|
+
box.after(ghost);
|
|
1293
|
+
box.classList.remove('quick', 'turning');
|
|
1294
|
+
draw();
|
|
1295
|
+
box.style.height = 'auto';
|
|
1296
|
+
var to = box.offsetHeight;
|
|
1297
|
+
box.style.height = from + 'px';
|
|
1298
|
+
box.classList.add('turn-in');
|
|
1299
|
+
void box.offsetHeight;
|
|
1300
|
+
box.classList.add('turning');
|
|
1301
|
+
box.classList.remove('turn-in');
|
|
1302
|
+
box.style.height = to + 'px';
|
|
1303
|
+
ghost.classList.add('gone');
|
|
1051
1304
|
setTimeout(function () {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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);
|
|
1305
|
+
ghost.remove();
|
|
1306
|
+
box.classList.remove('turning');
|
|
1307
|
+
box.style.height = '';
|
|
1308
|
+
bet.busy = false;
|
|
1309
|
+
measured();
|
|
1310
|
+
}, 80 + TURN_IN_MS + 20);
|
|
1071
1311
|
}
|
|
1072
1312
|
|
|
1073
1313
|
// one tap: buzz at once and ask the server for the next card. From the
|
|
@@ -1075,18 +1315,32 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1075
1315
|
function tapOrb(sharper) {
|
|
1076
1316
|
if (bet.busy) return;
|
|
1077
1317
|
buzz();
|
|
1318
|
+
think(sharper, 0);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Ask for the next card. heldAt: an answer has held as chosen since then,
|
|
1322
|
+
// so the card is asked for during the hold and turns in once it is over,
|
|
1323
|
+
// and the time to the card counts from that answer.
|
|
1324
|
+
function think(sharper, heldAt) {
|
|
1078
1325
|
bet.busy = true;
|
|
1079
1326
|
bet.armed = false;
|
|
1080
|
-
bet.tapAt = performance.now();
|
|
1327
|
+
bet.tapAt = heldAt || performance.now();
|
|
1081
1328
|
document.body.removeAttribute('data-reveal-ms');
|
|
1082
1329
|
var onCard = document.body.classList.contains('has-card');
|
|
1083
|
-
|
|
1084
|
-
|
|
1330
|
+
var failed = function () {
|
|
1331
|
+
bet.busy = false;
|
|
1332
|
+
document.getElementById('bet').classList.remove('holding');
|
|
1333
|
+
orb.relax();
|
|
1334
|
+
line.set('that did not go through. tap to try again', true);
|
|
1335
|
+
};
|
|
1336
|
+
Promise.all([api('/bet/think', { sharper: Boolean(sharper) }), afterHold(heldAt)]).then(function (both) {
|
|
1337
|
+
var s = both[0];
|
|
1338
|
+
if (s.http !== 200) { failed(); return; }
|
|
1085
1339
|
bet.status = s.status;
|
|
1086
1340
|
if (onCard && isCard(s)) { turn(s); return; }
|
|
1087
1341
|
if (onCard) { dissolve(function () { reveal(s, true); }); return; }
|
|
1088
1342
|
reveal(s, true);
|
|
1089
|
-
});
|
|
1343
|
+
}, failed);
|
|
1090
1344
|
}
|
|
1091
1345
|
|
|
1092
1346
|
function loadBet() {
|
|
@@ -1117,12 +1371,32 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1117
1371
|
});
|
|
1118
1372
|
}
|
|
1119
1373
|
|
|
1374
|
+
// The small orb on a card goes back to the big dotted orb, the same calm
|
|
1375
|
+
// way the page goes home before a new idea: the card fades, the small orb
|
|
1376
|
+
// glides down and the dots form again, idle under the pointer. The card
|
|
1377
|
+
// stays where it was on the server, so nothing is passed yet.
|
|
1378
|
+
function goHome() {
|
|
1379
|
+
if (bet.busy) return;
|
|
1380
|
+
buzz();
|
|
1381
|
+
bet.busy = true;
|
|
1382
|
+
bet.armed = false;
|
|
1383
|
+
dissolve(function () { bet.busy = false; line.set('tap to think', false); });
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// The big orb: the next idea, with the spiral, as on page open. An idea
|
|
1387
|
+
// that landed behind the calm line is the next one, so it is never passed.
|
|
1388
|
+
function tapBig() { tapOrb(false); }
|
|
1389
|
+
|
|
1390
|
+
function tapSmallOrb() {
|
|
1391
|
+
if (document.body.classList.contains('has-card')) goHome(); else tapBig();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1120
1394
|
var canvas = document.getElementById('orb');
|
|
1121
1395
|
var lavaTap = document.getElementById('lava');
|
|
1122
|
-
[canvas, lavaTap].forEach(function (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault();
|
|
1396
|
+
[[canvas, tapBig], [lavaTap, tapSmallOrb]].forEach(function (pair) {
|
|
1397
|
+
pair[0].addEventListener('click', pair[1]);
|
|
1398
|
+
pair[0].addEventListener('keydown', function (e) {
|
|
1399
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pair[1](); }
|
|
1126
1400
|
});
|
|
1127
1401
|
});
|
|
1128
1402
|
|
|
@@ -1130,14 +1404,19 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1130
1404
|
|
|
1131
1405
|
var teamBox = document.getElementById('team');
|
|
1132
1406
|
var teamInner = document.getElementById('team-inner');
|
|
1407
|
+
// the way back from what is on top: the team closes, a talk card goes back to the team
|
|
1408
|
+
var teamBack = null;
|
|
1409
|
+
|
|
1410
|
+
function closeTeam() { teamBox.hidden = true; }
|
|
1133
1411
|
|
|
1134
1412
|
function teamHead(title) {
|
|
1135
1413
|
teamInner.textContent = '';
|
|
1414
|
+
teamBack = closeTeam;
|
|
1136
1415
|
var head = el('div', 'head');
|
|
1137
1416
|
head.appendChild(el('h1', null, title));
|
|
1138
1417
|
var close = el('button', 'quiet', 'close');
|
|
1139
1418
|
close.type = 'button';
|
|
1140
|
-
close.addEventListener('click',
|
|
1419
|
+
close.addEventListener('click', closeTeam);
|
|
1141
1420
|
head.appendChild(close);
|
|
1142
1421
|
teamInner.appendChild(head);
|
|
1143
1422
|
}
|
|
@@ -1166,6 +1445,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1166
1445
|
// a talk card: what the member is on, and a word to steer them
|
|
1167
1446
|
function drawTalk(r) {
|
|
1168
1447
|
teamHead(r.member);
|
|
1448
|
+
teamBack = openTeam;
|
|
1169
1449
|
teamInner.appendChild(el('p', 'why', r.status + ': ' + r.on + '. ' + r.time + '.'));
|
|
1170
1450
|
var talkBox = convo(function (body) { body.member = r.member; return api('/bet/steer', body); }, function (ack) {
|
|
1171
1451
|
talkBox.replaceWith(el('p', 'mission', ack));
|
|
@@ -1179,6 +1459,25 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1179
1459
|
|
|
1180
1460
|
document.getElementById('working').addEventListener('click', openTeam);
|
|
1181
1461
|
|
|
1462
|
+
// Esc on the team page steps back one layer, the way its own buttons do:
|
|
1463
|
+
// words being typed are cancelled and the cursor leaves first, then the
|
|
1464
|
+
// talk card goes back to the team, then the team closes to the card.
|
|
1465
|
+
// On the card itself it does nothing. It listens on the way down, so the
|
|
1466
|
+
// focused element is still the field the key was pressed in.
|
|
1467
|
+
document.addEventListener('keydown', function (e) {
|
|
1468
|
+
if (e.key !== 'Escape' || e.isComposing || teamBox.hidden) return;
|
|
1469
|
+
e.preventDefault();
|
|
1470
|
+
var field = document.activeElement;
|
|
1471
|
+
if (field && (field.tagName === 'INPUT' || field.tagName === 'TEXTAREA')) {
|
|
1472
|
+
field.value = '';
|
|
1473
|
+
// the field's own input listener drops a second tap armed for those words
|
|
1474
|
+
field.dispatchEvent(new Event('input'));
|
|
1475
|
+
field.blur();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
teamBack();
|
|
1479
|
+
}, true);
|
|
1480
|
+
|
|
1182
1481
|
// the footer: how many members are really working, or the team when none are
|
|
1183
1482
|
function drawWorking(n) {
|
|
1184
1483
|
var btn = document.getElementById('working');
|