eklavya 1.72.0 → 1.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/assets/dashboard.html +399 -82
- package/dist/assets/tokens.css +2 -0
- package/dist/dashboard.js +3 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/feedback-review.js +25 -3
- package/dist/feedback-review.js.map +1 -1
- package/dist/feedback.js +3 -1
- package/dist/feedback.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
|
@@ -338,7 +338,7 @@
|
|
|
338
338
|
/* The correction bar: pinned to the window's bottom edge while you read, and
|
|
339
339
|
resting under the page at its end, so it never covers the last lines. The
|
|
340
340
|
left rule is the state: warning while open, verdigris once corrected. */
|
|
341
|
-
.fixbar { position:sticky; bottom:0; z-index:5; min-height:
|
|
341
|
+
.fixbar { position:sticky; bottom:0; z-index:5; min-height:88px; display:flex; align-items:center;
|
|
342
342
|
justify-content:space-between; gap:var(--space-4); padding:12px var(--space-5);
|
|
343
343
|
background:var(--panel); border-top:1px solid var(--line-2); border-left:3px solid transparent; }
|
|
344
344
|
.fixbar.is-open { border-left-color:var(--warning); }
|
|
@@ -346,18 +346,21 @@
|
|
|
346
346
|
.fixbar.is-fresh > * { animation:loader-in 180ms var(--ease); }
|
|
347
347
|
.fixbar p { margin:0; }
|
|
348
348
|
.fixbar .loader { padding:0; }
|
|
349
|
-
/*
|
|
350
|
-
.
|
|
351
|
-
.
|
|
349
|
+
/* The Eklavya face sits at the left, its expression the bar's state. The 88px row holds every state, so nothing moves. */
|
|
350
|
+
.fixbar eklavya-mascot { flex:none; display:block; }
|
|
351
|
+
.fixbar__text { min-width:0; flex:1; line-height:1.4; }
|
|
352
|
+
.fixbar__lead { color:var(--ink); font-size:15px; font-weight:600; }
|
|
352
353
|
.fixbar__note { color:var(--dim); font-size:13px; }
|
|
353
354
|
.fixbar__lead, .fixbar__note, .fixbar__done { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
354
|
-
.fixbar__done { display:flex; align-items:center; gap:var(--space-2); color:var(--ink); min-width:0; }
|
|
355
|
+
.fixbar__done { display:flex; align-items:center; gap:var(--space-2); color:var(--ink); min-width:0; flex:1; }
|
|
355
356
|
.fixbar__done svg { width:16px; height:16px; color:var(--spot); flex:none; }
|
|
356
357
|
.fixbar__done span { color:var(--dim); }
|
|
357
|
-
.fixbar__err { color:var(--warning); overflow-wrap:anywhere; }
|
|
358
|
+
.fixbar__err { color:var(--warning); overflow-wrap:anywhere; flex:1; }
|
|
358
359
|
.fixbar .btn, .fixbar .set__btn { flex:none; }
|
|
359
360
|
.fixbar .set__btn { padding:8px 14px; }
|
|
360
361
|
@media (max-width:560px) {
|
|
362
|
+
.fixbar { min-height:72px; }
|
|
363
|
+
.fixbar eklavya-mascot svg { width:40px; height:40px; }
|
|
361
364
|
.fixbar__note { display:none; }
|
|
362
365
|
.fixbar .btn { padding:10px 12px; }
|
|
363
366
|
}
|
|
@@ -394,6 +397,21 @@
|
|
|
394
397
|
.fix__msg.wrong { color:var(--error); }
|
|
395
398
|
.fix__msg.warn { color:var(--warning); }
|
|
396
399
|
.fix__msg.ok { color:var(--spot); }
|
|
400
|
+
/* After a right answer the dialog closes itself in 3s. Close carries the count and a fill that sweeps across it;
|
|
401
|
+
Stay open (first in tab order) cancels. Both are hidden unless a countdown runs. */
|
|
402
|
+
#fix-stay { display:none; }
|
|
403
|
+
.fix__foot.is-closing #fix-stay { display:inline-block; }
|
|
404
|
+
#fix-close { position:relative; overflow:hidden; isolation:isolate; min-width:116px; }
|
|
405
|
+
.fix__foot.is-closing #fix-close { border-color:var(--spot); }
|
|
406
|
+
.fix__foot.is-closing #fix-close::before { content:''; position:absolute; inset:0; z-index:-1; background:var(--spot-soft);
|
|
407
|
+
transform-origin:left; animation:fix-fill 3s linear both; }
|
|
408
|
+
@keyframes fix-fill { from { transform:scaleX(0); } to { transform:scaleX(1); } }
|
|
409
|
+
.fix__foot.is-paused #fix-close::before { animation-play-state:paused; }
|
|
410
|
+
@media (prefers-reduced-motion:reduce) { .fix__foot.is-closing #fix-close::before { display:none; } }
|
|
411
|
+
@media (max-width:560px) {
|
|
412
|
+
.fix__foot { flex-wrap:wrap; }
|
|
413
|
+
.fix__msg { flex-basis:100%; }
|
|
414
|
+
}
|
|
397
415
|
.g2 > *, .g3 > * { margin-top:0 !important; }
|
|
398
416
|
|
|
399
417
|
/* ---------- stat tiles ---------- */
|
|
@@ -700,37 +718,146 @@
|
|
|
700
718
|
.fb__rule { border:1px solid var(--line-2); border-left:2px solid var(--spot); background:var(--panel);
|
|
701
719
|
padding:var(--space-4) var(--space-5); }
|
|
702
720
|
.fb__rule p { margin:0; color:var(--ink); font-size:14px; max-width:72ch; }
|
|
703
|
-
|
|
704
|
-
.fb { display:grid; gap:var(--space-6); }
|
|
705
|
-
.
|
|
721
|
+
/* The item: a story column and a decision rail that stays in view; a fixed bar replaces the rail's button at 900px and below. */
|
|
722
|
+
.fb { display:grid; grid-template-columns:minmax(0, 1fr) 300px; gap:var(--space-6); align-items:start; }
|
|
723
|
+
.fb__story { display:grid; gap:var(--space-6); min-width:0; }
|
|
724
|
+
.fb__rail { position:sticky; top:var(--space-6); display:grid; gap:var(--space-4); min-width:0; }
|
|
725
|
+
.fb__rail.is-tall { position:static; }
|
|
726
|
+
.fb__card { padding:var(--space-5); }
|
|
706
727
|
.fb__h { font-family:var(--font-mono); font-size:10.5px; letter-spacing:var(--tracking-caps); text-transform:uppercase;
|
|
707
728
|
color:var(--faint); font-weight:var(--weight-semibold); margin:0 0 var(--space-2); }
|
|
708
|
-
.
|
|
709
|
-
|
|
729
|
+
.fb__cap { margin:var(--space-1) 0 0; color:var(--faint); font-size:12.5px; }
|
|
730
|
+
.fb__note { margin:0; color:var(--dim); font-size:13.5px; overflow-wrap:anywhere; }
|
|
731
|
+
/* ① The verdict */
|
|
732
|
+
.fb__verdict { display:flex; gap:var(--space-5); align-items:flex-start; }
|
|
733
|
+
.fb__verdict > eklavya-mascot { flex:none; }
|
|
734
|
+
.fb__vb { flex:1; min-width:0; }
|
|
735
|
+
.fb__vh { font-family:var(--font-disp); font-weight:800; font-size:var(--text-xl); line-height:1.15; letter-spacing:-0.02em;
|
|
736
|
+
margin:0; color:var(--ink); overflow-wrap:anywhere; }
|
|
737
|
+
.fb__vh--old { font-size:var(--text-md); }
|
|
738
|
+
.fb__vs { margin:var(--space-2) 0 0; color:var(--dim); font-size:13.5px; }
|
|
739
|
+
.fb__cover { list-style:none; margin:var(--space-5) 0 0; padding:0; display:grid; grid-template-columns:repeat(4, minmax(0, 1fr));
|
|
740
|
+
border:1px solid var(--line-2); background:var(--bg); }
|
|
741
|
+
.fb__cell { display:grid; grid-template-columns:auto minmax(0, 1fr); column-gap:var(--space-2); align-items:center;
|
|
742
|
+
padding:var(--space-3) var(--space-4); color:var(--faint); }
|
|
743
|
+
.fb__cell + .fb__cell { border-left:1px solid var(--line-2); }
|
|
744
|
+
.fb__cell svg { width:16px; height:16px; grid-row:span 2; }
|
|
745
|
+
.fb__cn { color:var(--ink); font-size:13px; font-weight:var(--weight-semibold); }
|
|
746
|
+
.fb__cs { font-family:var(--font-mono); font-size:11px; letter-spacing:0.04em; }
|
|
747
|
+
.fb__cell.is-miss { color:var(--warning); box-shadow:inset 0 -2px 0 var(--warning); }
|
|
748
|
+
.fb__cell.is-ok { color:var(--spot); box-shadow:inset 0 -2px 0 var(--spot); }
|
|
749
|
+
/* ② The two prompts, one block */
|
|
750
|
+
.fb__grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); border:1px solid var(--line-2); }
|
|
751
|
+
.fb__grid > section { padding:var(--space-5); min-width:0; }
|
|
752
|
+
.fb__mine { background:var(--panel); border-right:1px solid var(--line-2); }
|
|
753
|
+
.fb__better { background:var(--bg); }
|
|
754
|
+
.fb__ch { display:flex; align-items:center; justify-content:space-between; gap:var(--space-3); min-height:32px; margin-bottom:var(--space-2); }
|
|
755
|
+
.fb__ch .fb__h { margin:0; }
|
|
756
|
+
.fb__better .fb__h { color:var(--spot); }
|
|
757
|
+
.fb__quote { margin:0; color:var(--ink); white-space:pre-wrap; overflow-wrap:anywhere; font-size:14px; }
|
|
758
|
+
.fb__mine .fb__quote { color:var(--dim); }
|
|
710
759
|
.cmd--block { margin-top:0; align-items:flex-start; }
|
|
711
760
|
.cmd--block code { white-space:pre-wrap; overflow-wrap:anywhere; overflow-x:visible; }
|
|
712
|
-
.
|
|
713
|
-
.
|
|
714
|
-
.
|
|
715
|
-
.
|
|
716
|
-
.
|
|
717
|
-
|
|
718
|
-
.
|
|
719
|
-
.
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
.
|
|
761
|
+
.fb__better .cmd--block code { background:none; border:0; padding:0; color:var(--ink); font-size:13px; }
|
|
762
|
+
.fb__ph { background:var(--spot-soft); color:inherit; padding:0 2px; }
|
|
763
|
+
.fb__clamp { position:relative; overflow:hidden; transition:max-height var(--duration-base) var(--ease); }
|
|
764
|
+
.fb__clamp.is-clamped { max-height:15.5em; }
|
|
765
|
+
.fb__clamp.is-clamped::after { content:''; position:absolute; inset:auto 0 0; height:4em; pointer-events:none;
|
|
766
|
+
background:linear-gradient(transparent, var(--bg)); }
|
|
767
|
+
.fb__mine .fb__clamp.is-clamped::after { background:linear-gradient(transparent, var(--panel)); }
|
|
768
|
+
.fb__copy, .fb__more, .fb__del { font:inherit; font-size:13px; cursor:pointer; background:none; color:var(--dim);
|
|
769
|
+
border:1px solid transparent; display:inline-flex; align-items:center; gap:var(--space-2);
|
|
770
|
+
padding:var(--space-1) var(--space-2); }
|
|
771
|
+
.fb__copy svg { width:14px; height:14px; }
|
|
772
|
+
.fb__copy:hover, .fb__more:hover { color:var(--spot); border-color:var(--spot); }
|
|
773
|
+
.fb__more { margin-top:var(--space-2); padding-inline:0; color:var(--spot); }
|
|
774
|
+
.fb__more:hover { border-color:transparent; text-decoration:underline; }
|
|
775
|
+
.fb__copy:focus-visible, .fb__more:focus-visible, .fb__del:focus-visible { outline:none; box-shadow:var(--ring); }
|
|
776
|
+
/* ③ What worked */
|
|
777
|
+
.fb__worked { display:flex; gap:var(--space-4); align-items:flex-start; padding:var(--space-4) var(--space-5);
|
|
778
|
+
background:var(--panel); border:1px solid var(--line-2); border-left:2px solid var(--spot); }
|
|
779
|
+
.fb__worked > eklavya-mascot, .fb__sh > eklavya-mascot, .fb__tip > eklavya-mascot, .fb__dh > eklavya-mascot { flex:none; }
|
|
780
|
+
.fb__worked .fb__h { color:var(--spot); margin-bottom:var(--space-1); }
|
|
781
|
+
.fb__worked p { margin:0; color:var(--ink); font-size:14px; overflow-wrap:anywhere; }
|
|
782
|
+
/* ④ What it left out and ⑤ next time */
|
|
783
|
+
.fb__sec { display:grid; gap:var(--space-4); }
|
|
784
|
+
.fb__sh { display:flex; gap:var(--space-3); align-items:center; }
|
|
785
|
+
.fb__sh h3, .fb__dh h3 { font-family:var(--font-disp); font-weight:800; font-size:var(--text-md); margin:0; color:var(--ink); }
|
|
786
|
+
.fb__ds, .fb__tips { list-style:none; margin:0; padding:0; }
|
|
787
|
+
.fb__ds { border:1px solid var(--line-2); background:var(--panel); }
|
|
788
|
+
.fb__ds li { display:grid; grid-template-columns:44px minmax(0, 1fr); gap:var(--space-4); padding:var(--space-5) var(--space-6); }
|
|
789
|
+
.fb__ds li + li { border-top:1px solid var(--line); }
|
|
790
|
+
.fb__tile { width:44px; height:44px; display:grid; place-items:center; background:var(--warning-soft); color:var(--warning); }
|
|
791
|
+
.fb__tile svg { width:20px; height:20px; }
|
|
792
|
+
.fb__gb { display:grid; gap:var(--space-1); min-width:0; }
|
|
793
|
+
.fb__gt { margin:0; color:var(--ink); font-weight:var(--weight-semibold); font-size:14px; }
|
|
794
|
+
.fb__gt small { font-family:var(--font-mono); font-weight:400; font-size:11px; color:var(--faint); margin-left:var(--space-2); }
|
|
795
|
+
.fb__ev { display:flex; gap:var(--space-3); align-items:baseline; flex-wrap:wrap; margin:var(--space-2) 0 0; font-size:13px; }
|
|
796
|
+
.fb__evl { font-family:var(--font-mono); font-size:10.5px; letter-spacing:var(--tracking-caps); text-transform:uppercase; color:var(--faint); }
|
|
797
|
+
.fb__ev q { background:var(--mass); color:var(--ink); padding:var(--space-1) var(--space-2); overflow-wrap:anywhere; quotes:'“' '”'; }
|
|
798
|
+
.fb__tips { display:grid; gap:var(--space-3); counter-reset:t; }
|
|
799
|
+
.fb__tips li { counter-increment:t; display:grid; grid-template-columns:auto minmax(0, 1fr); gap:var(--space-4); align-items:baseline;
|
|
800
|
+
background:var(--panel); border:1px solid var(--line-2); padding:var(--space-4) var(--space-5);
|
|
801
|
+
color:var(--ink); font-size:14px; overflow-wrap:anywhere; }
|
|
802
|
+
.fb__tips li::before { content:'0' counter(t); font-family:var(--font-disp); font-weight:900; font-size:var(--text-2xl);
|
|
803
|
+
line-height:1; color:var(--spot); }
|
|
804
|
+
@media (min-width:1100px) {
|
|
805
|
+
.fb__tips[data-n="2"] { grid-template-columns:repeat(2, minmax(0, 1fr)); }
|
|
806
|
+
.fb__tips[data-n="3"] { grid-template-columns:repeat(3, minmax(0, 1fr)); }
|
|
807
|
+
.fb__tips[data-n="2"] li, .fb__tips[data-n="3"] li { grid-template-columns:minmax(0, 1fr); align-content:start; }
|
|
808
|
+
}
|
|
809
|
+
/* ⑥ Found a problem */
|
|
810
|
+
.fb__tip { display:flex; gap:var(--space-3); align-items:center; flex-wrap:wrap; padding-top:var(--space-4); border-top:1px solid var(--line-2); }
|
|
811
|
+
.fb__tip p { flex:1 1 260px; margin:0; color:var(--dim); font-size:13px; }
|
|
812
|
+
.fb__tip code { background:var(--mass); padding:1px 6px; font-size:12px; color:var(--ink); }
|
|
813
|
+
/* The rail */
|
|
814
|
+
.fb__ctx { display:grid; gap:var(--space-3); margin:0; font-size:13px; }
|
|
815
|
+
.fb__ctx dt { font-family:var(--font-mono); font-size:10.5px; letter-spacing:var(--tracking-caps); text-transform:uppercase; color:var(--faint); }
|
|
816
|
+
.fb__ctx dd { margin:var(--space-1) 0 0; color:var(--ink); overflow-wrap:anywhere; }
|
|
817
|
+
.fb__ctx .slug { display:block; font-family:var(--font-mono); font-size:11px; color:var(--faint); overflow-wrap:anywhere; }
|
|
818
|
+
.fb__decide { border-color:var(--spot); display:grid; gap:var(--space-3); }
|
|
819
|
+
.fb__dh { display:flex; gap:var(--space-3); align-items:center; }
|
|
820
|
+
.fb__dp { margin:0; color:var(--dim); font-size:13.5px; }
|
|
821
|
+
.fb__actions { display:grid; gap:var(--space-2); }
|
|
822
|
+
.fb__ack { width:100%; min-height:44px; justify-content:center; display:inline-flex; align-items:center; gap:var(--space-2); }
|
|
823
|
+
.fb__ack svg { width:16px; height:16px; }
|
|
824
|
+
.fb__del { color:var(--faint); justify-content:center; min-height:32px; }
|
|
825
|
+
.fb__del:hover { color:var(--error); }
|
|
723
826
|
.fb__ask { display:inline-flex; gap:var(--space-3); flex-wrap:wrap; align-items:center; color:var(--dim); font-size:13.5px; }
|
|
724
827
|
.fb__msg { margin:0; color:var(--warning); font-size:13px; }
|
|
828
|
+
.fb__msg:empty { display:none; }
|
|
829
|
+
.fb__foot { display:flex; justify-content:space-between; margin:0; padding-top:var(--space-3); border-top:1px solid var(--line);
|
|
830
|
+
font-family:var(--font-mono); font-size:11px; color:var(--faint); }
|
|
831
|
+
.fb__acked { margin:0; font-family:var(--font-mono); font-size:12px; color:var(--faint); }
|
|
832
|
+
.fb__bar { display:none; }
|
|
725
833
|
.fb__sts { display:flex; gap:var(--space-2); flex-wrap:wrap; }
|
|
726
|
-
.fb__tip h2 { font-family:var(--font-disp); font-size:var(--text-md); font-weight:var(--weight-semibold); margin:0 0 var(--space-2); }
|
|
727
|
-
.fb__tip p { margin:0 0 var(--space-4); color:var(--dim); max-width:70ch; }
|
|
728
|
-
.fb__tip .fb__quote { margin-bottom:var(--space-4); }
|
|
729
834
|
.fb__why p { margin:0 0 var(--space-3); color:var(--dim); max-width:70ch; }
|
|
835
|
+
@media (max-width:900px) {
|
|
836
|
+
.fb { grid-template-columns:minmax(0, 1fr); }
|
|
837
|
+
.fb__rail { position:static; }
|
|
838
|
+
.fb__ctx { grid-template-columns:repeat(2, minmax(0, 1fr)); column-gap:var(--space-5); }
|
|
839
|
+
/* The bar holds Acknowledge here; the rail card keeps Delete and the message. */
|
|
840
|
+
.fb__decide .fb__ack { display:none; }
|
|
841
|
+
.fb__bar { display:flex; align-items:center; gap:var(--space-4); position:fixed; inset:auto 0 0; z-index:20;
|
|
842
|
+
background:var(--panel); border-top:1px solid var(--spot); padding:var(--space-3) var(--space-5); }
|
|
843
|
+
.fb__bar p { flex:1; margin:0; color:var(--dim); font-size:13.5px; }
|
|
844
|
+
.fb__bar .fb__ack { width:auto; flex:none; }
|
|
845
|
+
main:has(.fb__bar) { padding-bottom:120px; }
|
|
846
|
+
}
|
|
730
847
|
@media (max-width:560px) {
|
|
848
|
+
.fb__verdict > eklavya-mascot { display:none; }
|
|
731
849
|
.fb__grid { grid-template-columns:minmax(0, 1fr); }
|
|
732
|
-
.
|
|
733
|
-
.
|
|
850
|
+
.fb__mine { border-right:0; border-bottom:1px solid var(--line-2); }
|
|
851
|
+
.fb__cover { grid-template-columns:repeat(2, minmax(0, 1fr)); }
|
|
852
|
+
.fb__cell + .fb__cell { border-left:0; }
|
|
853
|
+
.fb__cell:nth-child(even) { border-left:1px solid var(--line-2); }
|
|
854
|
+
.fb__cell:nth-child(n+3) { border-top:1px solid var(--line-2); }
|
|
855
|
+
.fb__ds li { padding:var(--space-4); gap:var(--space-3); }
|
|
856
|
+
.fb__ctx { grid-template-columns:minmax(0, 1fr); }
|
|
857
|
+
.fb__bar p { display:none; }
|
|
858
|
+
.fb__bar .fb__ack { flex:1; width:100%; }
|
|
859
|
+
.fb__copy, .fb__del, .fb__tip .set__btn { min-height:44px; }
|
|
860
|
+
.fb__del { width:100%; }
|
|
734
861
|
.fb__ask > button { flex:1 1 40%; min-height:44px; }
|
|
735
862
|
}
|
|
736
863
|
.starts__more > summary { cursor:pointer; color:var(--dim); font-size:13px; padding:var(--space-2) 0; }
|
|
@@ -1024,6 +1151,7 @@
|
|
|
1024
1151
|
<div class="fix__opts" id="fix-opts" role="group" aria-labelledby="fix-q"></div>
|
|
1025
1152
|
<div class="fix__foot">
|
|
1026
1153
|
<p class="fix__msg" id="fix-msg" role="status"></p>
|
|
1154
|
+
<button type="button" class="set__btn" id="fix-stay">Stay open</button>
|
|
1027
1155
|
<button type="button" class="set__btn" id="fix-close">Close</button>
|
|
1028
1156
|
</div>
|
|
1029
1157
|
</dialog>
|
|
@@ -1538,12 +1666,14 @@ function startBlocks(title, cmds) {
|
|
|
1538
1666
|
|
|
1539
1667
|
/** Copies a block's command; if the clipboard refuses, selects it for the keyboard shortcut instead. */
|
|
1540
1668
|
function copyCommand(btn) {
|
|
1541
|
-
|
|
1669
|
+
// `data-copy` may name the text it copies, for a button that is not beside it; its text is plain, without the marks.
|
|
1670
|
+
const code = btn.dataset.copy ? $(btn.dataset.copy) : btn.parentElement.querySelector('code');
|
|
1671
|
+
const label = btn.querySelector('span') ?? btn;
|
|
1542
1672
|
const live = $('copy-live');
|
|
1543
1673
|
Promise.resolve().then(() => navigator.clipboard.writeText(code.textContent)).then(() => {
|
|
1544
|
-
|
|
1674
|
+
label.textContent = 'Copied';
|
|
1545
1675
|
live.textContent = 'Command copied';
|
|
1546
|
-
setTimeout(() => {
|
|
1676
|
+
setTimeout(() => { label.textContent = 'Copy'; }, 1500);
|
|
1547
1677
|
}, () => {
|
|
1548
1678
|
getSelection().selectAllChildren(code);
|
|
1549
1679
|
live.textContent = `Press ${/Mac|iPhone|iPad/.test(navigator.userAgent) ? '⌘C' : 'Ctrl+C'} to copy`;
|
|
@@ -2335,8 +2465,10 @@ function viewConcepts(param) {
|
|
|
2335
2465
|
const t = T.concepts;
|
|
2336
2466
|
t.state = CONCEPT_STATES.includes(param) ? param : 'all';
|
|
2337
2467
|
t.domain = CUR.query.get('domain') || 'all';
|
|
2468
|
+
t.tag = CUR.query.get('tag') || 'all';
|
|
2338
2469
|
const all = conceptsInScope();
|
|
2339
2470
|
const domains = [...new Set(all.map((c) => c.domain))].sort();
|
|
2471
|
+
const tags = [...new Set(all.flatMap((c) => c.tags))].sort();
|
|
2340
2472
|
const q = t.q.trim().toLowerCase();
|
|
2341
2473
|
|
|
2342
2474
|
let list = all.filter((c) => {
|
|
@@ -2345,7 +2477,8 @@ function viewConcepts(param) {
|
|
|
2345
2477
|
if (t.state === 'due' && !c.due) return false;
|
|
2346
2478
|
if (t.state === 'unseen' && c.seen) return false;
|
|
2347
2479
|
if (t.domain !== 'all' && c.domain !== t.domain) return false;
|
|
2348
|
-
if (
|
|
2480
|
+
if (t.tag !== 'all' && !c.tags.includes(t.tag)) return false;
|
|
2481
|
+
if (q && !(`${c.name} ${c.slug} ${c.domain} ${c.tags.join(' ')} ${c.context ?? ''}`.toLowerCase().includes(q))) return false;
|
|
2349
2482
|
return true;
|
|
2350
2483
|
});
|
|
2351
2484
|
|
|
@@ -2365,7 +2498,8 @@ function viewConcepts(param) {
|
|
|
2365
2498
|
const rows = slice(list, p).map((c) => `
|
|
2366
2499
|
<tr class="row" data-go="${esc(LH('concept', c.slug))}">
|
|
2367
2500
|
<td style="min-width:220px"><span class="title">${esc(c.name)}</span>
|
|
2368
|
-
<div class="slug">${esc(c.domain)} · ${esc(tierLabel(c.tier))}</div
|
|
2501
|
+
<div class="slug">${esc(c.domain)} · ${esc(tierLabel(c.tier))}</div>
|
|
2502
|
+
${c.tags.length ? `<div class="tags" style="margin-top:var(--space-2)">${tagPills(c.tags)}</div>` : ''}</td>
|
|
2369
2503
|
<td style="min-width:104px">${c.seen ? meter(c.score * 100, c.score < 0.5 ? 'warn' : '') +
|
|
2370
2504
|
`<div class="slug">${Math.round(c.score * 100)}%</div>` : '<span class="slug">—</span>'}</td>
|
|
2371
2505
|
<td class="num">${c.attempts || '—'}</td>
|
|
@@ -2393,6 +2527,10 @@ function viewConcepts(param) {
|
|
|
2393
2527
|
<option value="all">All domains</option>
|
|
2394
2528
|
${domains.map((d) => `<option value="${esc(d)}"${t.domain === d ? ' selected' : ''}>${esc(d)}</option>`).join('')}
|
|
2395
2529
|
</select>
|
|
2530
|
+
${tags.length ? `<select id="ctag" aria-label="Tag">
|
|
2531
|
+
<option value="all">All tags</option>
|
|
2532
|
+
${tags.map((x) => `<option value="${esc(x)}"${t.tag === x ? ' selected' : ''}>${esc(x)}</option>`).join('')}
|
|
2533
|
+
</select>` : ''}
|
|
2396
2534
|
<select id="sort" aria-label="Sort">
|
|
2397
2535
|
${[['weakest','Weakest first'],['strongest','Strongest first'],['recent','Recently seen'],['asked','Most asked'],['tier','By tier'],['name','A–Z']]
|
|
2398
2536
|
.map(([v, l]) => `<option value="${v}"${t.sort === v ? ' selected' : ''}>${l}</option>`).join('')}
|
|
@@ -2433,6 +2571,7 @@ function viewConcept(slug) {
|
|
|
2433
2571
|
`<code class="slug">${esc(c.slug)}</code>`, esc(c.domain), esc(tierLabel(c.tier)),
|
|
2434
2572
|
esc(c.source === 'seed' ? 'from the seed graph' : 'minted from your work'),
|
|
2435
2573
|
], c.description ? esc(c.description) : '', back),
|
|
2574
|
+
c.tags.length ? `<div class="tags">${c.tags.map((x) => `<a class="pill link" data-go="${esc(LH('concepts', null, { tag: x }))}">${esc(x)}</a>`).join(' ')}</div>` : '',
|
|
2436
2575
|
|
|
2437
2576
|
tiles([
|
|
2438
2577
|
['Score', c.seen ? Math.round(c.score * 100) + '%' : '—', decayed ? `decayed from ${Math.round(c.stored_score * 100)}%` : 'last five grades, recent first', c.mastered ? 'good' : c.score < 0.5 && c.seen ? 'warn' : ''],
|
|
@@ -3495,10 +3634,13 @@ function loadFix(a) {
|
|
|
3495
3634
|
.catch((e) => {
|
|
3496
3635
|
const el = bar();
|
|
3497
3636
|
if (!el || drawn) return;
|
|
3498
|
-
el.innerHTML =
|
|
3637
|
+
el.innerHTML = `${faceFor('error')}<p class="fixbar__err">${esc(e.message)}</p><button type="button" class="set__btn" id="fix-retry">Retry</button>`;
|
|
3499
3638
|
});
|
|
3500
3639
|
}
|
|
3501
3640
|
|
|
3641
|
+
/** The bar's Eklavya face: its expression is the mascot's own map from the bar's state. */
|
|
3642
|
+
const faceFor = (state) => `<eklavya-mascot state="${state}" variant="face" size="56"></eklavya-mascot>`;
|
|
3643
|
+
|
|
3502
3644
|
/** Draws the bar for `d`, /api/attempts/correction's answer; `fresh` crossfades into it. */
|
|
3503
3645
|
function paintFix(el, a, d, fresh) {
|
|
3504
3646
|
FIX = { attempt: a.attempt, artifact: a.id, q: d };
|
|
@@ -3510,11 +3652,11 @@ function paintFix(el, a, d, fresh) {
|
|
|
3510
3652
|
if (dot) dot.className = `tab__dot is-${done ? 'done' : 'open'}`;
|
|
3511
3653
|
// The button the reader is on (a bar drawn again over its own) is found again in the new one.
|
|
3512
3654
|
const ui = captureUi(el);
|
|
3513
|
-
el.innerHTML = done
|
|
3655
|
+
el.innerHTML = faceFor(done ? 'success' : 'warning') + (done
|
|
3514
3656
|
? `<p class="fixbar__done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>Corrected on try ${d.tries} <span>· ${esc(stamp(d.corrected_at))}</span></p>`
|
|
3515
3657
|
: `<div class="fixbar__text"><p class="fixbar__lead">You missed this one.</p>
|
|
3516
3658
|
<p class="fixbar__note">Read it, then correct your answer. Not now? It stays under To correct.</p></div>
|
|
3517
|
-
<button type="button" class="btn" id="fix-open">Correct your answer</button
|
|
3659
|
+
<button type="button" class="btn" id="fix-open">Correct your answer</button>`);
|
|
3518
3660
|
restoreUi(el, ui);
|
|
3519
3661
|
}
|
|
3520
3662
|
|
|
@@ -3540,6 +3682,7 @@ function openFix() {
|
|
|
3540
3682
|
$('fix-opts').innerHTML = shuffled(d.options.length).map((i) =>
|
|
3541
3683
|
`<button type="button" class="fix__opt" data-pick="${esc(d.options[i])}"><span>${esc(d.options[i])}</span>`
|
|
3542
3684
|
+ `${d.option_notes?.[i] ? `<span class="fix__n">${esc(d.option_notes[i])}</span>` : ''}<span class="fix__tag"></span></button>`).join('');
|
|
3685
|
+
stopCount();
|
|
3543
3686
|
fixSay('', '');
|
|
3544
3687
|
$('fix').showModal();
|
|
3545
3688
|
TIP.sync();
|
|
@@ -3557,14 +3700,16 @@ function pickFix(btn) {
|
|
|
3557
3700
|
if (r.corrected) {
|
|
3558
3701
|
btn.classList.add('is-right');
|
|
3559
3702
|
btn.querySelector('.fix__tag').textContent = 'Right answer';
|
|
3560
|
-
fixSay(
|
|
3703
|
+
fixSay(FIXDONE, 'ok');
|
|
3704
|
+
// Read out once, politely, with the message; the ticks below only change Close's label.
|
|
3705
|
+
$('fix-msg').insertAdjacentHTML('beforeend', `<span class="sr"> Closing in ${COUNT} seconds. Press Stay open to cancel.</span>`);
|
|
3561
3706
|
FIX.q = { ...FIX.q, tries: r.tries, corrected_at: new Date().toISOString() };
|
|
3562
3707
|
const row = S.artifacts.find((x) => x.id === FIX.artifact);
|
|
3563
3708
|
if (row) row.correction = 'done';
|
|
3564
3709
|
// Every other count on the page (the tiles, the gallery's tag, the accuracy) follows by itself: the server
|
|
3565
3710
|
// noticed this write and told the page, which reads the payload again and redraws in place.
|
|
3566
3711
|
armLeave(false);
|
|
3567
|
-
|
|
3712
|
+
startCount();
|
|
3568
3713
|
return;
|
|
3569
3714
|
}
|
|
3570
3715
|
btn.classList.add('is-wrong');
|
|
@@ -3590,9 +3735,54 @@ $('fix-opts').addEventListener('keydown', (e) => {
|
|
|
3590
3735
|
const i = live.indexOf(document.activeElement);
|
|
3591
3736
|
live[(i + step + live.length) % live.length]?.focus();
|
|
3592
3737
|
});
|
|
3738
|
+
/**
|
|
3739
|
+
* After a right answer the dialog closes itself, so the reader can read the answer first and still cancel.
|
|
3740
|
+
* One handle, cleared by every way out: Stay open, any close (button, Escape, the countdown) and a re-open.
|
|
3741
|
+
* A hidden tab holds the count and the fill, so the dialog never closes unseen. The message is read out once,
|
|
3742
|
+
* when the count starts; the ticks only change Close's label.
|
|
3743
|
+
*/
|
|
3744
|
+
const FIXDONE = 'Corrected. Your accuracy now counts this as right.';
|
|
3745
|
+
const COUNT = 3;
|
|
3746
|
+
let FIXCD = null;
|
|
3747
|
+
const fixFoot = () => $('fix-close').parentElement;
|
|
3748
|
+
function stopCount() {
|
|
3749
|
+
if (!FIXCD) return;
|
|
3750
|
+
clearTimeout(FIXCD.timer);
|
|
3751
|
+
FIXCD = null;
|
|
3752
|
+
fixFoot().classList.remove('is-closing', 'is-paused');
|
|
3753
|
+
$('fix-close').textContent = 'Close';
|
|
3754
|
+
}
|
|
3755
|
+
function runCount() {
|
|
3756
|
+
fixFoot().classList.toggle('is-paused', document.hidden);
|
|
3757
|
+
if (document.hidden) return;
|
|
3758
|
+
FIXCD.timer = setTimeout(() => {
|
|
3759
|
+
if (--FIXCD.n > 0) { $('fix-close').textContent = `Closing in ${FIXCD.n}…`; return runCount(); }
|
|
3760
|
+
stopCount();
|
|
3761
|
+
$('fix').close();
|
|
3762
|
+
}, 1000);
|
|
3763
|
+
}
|
|
3764
|
+
function startCount() {
|
|
3765
|
+
stopCount();
|
|
3766
|
+
FIXCD = { n: COUNT, timer: 0 };
|
|
3767
|
+
fixFoot().classList.add('is-closing');
|
|
3768
|
+
$('fix-close').textContent = `Closing in ${COUNT}…`;
|
|
3769
|
+
runCount();
|
|
3770
|
+
$('fix-stay').focus();
|
|
3771
|
+
}
|
|
3772
|
+
document.addEventListener('visibilitychange', () => {
|
|
3773
|
+
if (!FIXCD) return;
|
|
3774
|
+
clearTimeout(FIXCD.timer);
|
|
3775
|
+
runCount();
|
|
3776
|
+
});
|
|
3777
|
+
$('fix-stay').addEventListener('click', () => {
|
|
3778
|
+
stopCount();
|
|
3779
|
+
fixSay(FIXDONE, 'ok');
|
|
3780
|
+
$('fix-close').focus();
|
|
3781
|
+
});
|
|
3593
3782
|
$('fix-close').addEventListener('click', () => $('fix').close());
|
|
3594
3783
|
// However it closes -- the button, Escape -- the bar shows where things stand.
|
|
3595
3784
|
$('fix').addEventListener('close', () => {
|
|
3785
|
+
stopCount();
|
|
3596
3786
|
TIP.sync();
|
|
3597
3787
|
const bar = $('fixbar');
|
|
3598
3788
|
const a = FIX && S.artifacts.find((x) => x.id === FIX.artifact);
|
|
@@ -4203,9 +4393,13 @@ function drawSettings(d, scope) {
|
|
|
4203
4393
|
============================================================ */
|
|
4204
4394
|
const FB = {
|
|
4205
4395
|
ruleEmpty: 'Eklavya reviews one prompt at a time. When one is ready it appears here, and the next waits until you press Acknowledge.',
|
|
4206
|
-
rulePending: '
|
|
4396
|
+
rulePending: 'The next review waits until you acknowledge this one. It moves to History.',
|
|
4397
|
+
barRule: 'The next review waits until you acknowledge this one.',
|
|
4207
4398
|
announce: 'Acknowledged. Feedback resumes at your next session.',
|
|
4208
4399
|
areas: { outcome: 'Outcome', context: 'Context', scope: 'Scope', check: 'Check' },
|
|
4400
|
+
hints: { outcome: 'what done looks like', context: 'files, systems, prior facts', scope: 'the limits', check: 'how the agent proves it worked' },
|
|
4401
|
+
// `unknown` is an item from rubric 2 or older, which named only its gaps.
|
|
4402
|
+
status: { left_out: 'Left out', clear: 'Clear', not_needed: 'Not needed', unknown: 'Not flagged' },
|
|
4209
4403
|
counted: false,
|
|
4210
4404
|
observerCmd: 'eklavya config set providers.observer <model>',
|
|
4211
4405
|
about: `Your prompts are already recorded by memory. After a session ends, in the background and never in the middle of your work,
|
|
@@ -4240,16 +4434,17 @@ function fbEmpty(F) {
|
|
|
4240
4434
|
return `<div class="card fb__why" id="fb-empty"><div class="ek-mascot-message"><eklavya-mascot state="${state}" size="72"></eklavya-mascot><div>${body}</div></div></div>`;
|
|
4241
4435
|
}
|
|
4242
4436
|
|
|
4437
|
+
/** With no item, the rule is said above the reason; with one, it sits beside Acknowledge in the rail. */
|
|
4438
|
+
const fbNone = (F) => `<div class="fb__rule" id="fb-rule"><p>${esc(FB.ruleEmpty)}</p></div>${fbEmpty(F)}${fbTip()}`;
|
|
4439
|
+
|
|
4243
4440
|
function viewFeedbackDashboard() {
|
|
4244
4441
|
const F = S.feedback;
|
|
4245
|
-
const item = F.pending ? slot('fb-item', '/api/feedback', (d) => d.item ? drawFbItem(d.item) :
|
|
4442
|
+
const item = F.pending ? slot('fb-item', '/api/feedback', (d) => d.item ? drawFbItem(d.item) : fbNone(S.feedback), { label: 'Loading your feedback…' }) : '';
|
|
4246
4443
|
if (F.pending && !FB.counted) { FB.counted = true; AFTER.push(fbOpened); }
|
|
4247
4444
|
const counts = [F.pending ? '1 waiting' : 'nothing waiting', plural(F.acknowledged, 'acknowledged item')];
|
|
4248
4445
|
return [
|
|
4249
4446
|
pageHead('Feedback', counts, FB.about),
|
|
4250
|
-
|
|
4251
|
-
F.pending ? item : fbEmpty(F),
|
|
4252
|
-
fbTip(),
|
|
4447
|
+
F.pending ? item : fbNone(F),
|
|
4253
4448
|
`<span class="sr" id="copy-live" aria-live="polite"></span>`,
|
|
4254
4449
|
].join('');
|
|
4255
4450
|
}
|
|
@@ -4263,10 +4458,10 @@ function viewFeedbackDashboard() {
|
|
|
4263
4458
|
const FB_TIP_KEY = 'eklavya-dash-feedback-tip';
|
|
4264
4459
|
function fbTip() {
|
|
4265
4460
|
try { if (localStorage.getItem(FB_TIP_KEY) === '1') return ''; } catch (e) { /* shown */ }
|
|
4266
|
-
return `<aside class="
|
|
4267
|
-
<
|
|
4268
|
-
<
|
|
4269
|
-
<
|
|
4461
|
+
return `<aside class="fb__tip" id="fb-tip" aria-labelledby="fb-tip-h">
|
|
4462
|
+
<eklavya-mascot state="info" variant="face" size="28"></eklavya-mascot>
|
|
4463
|
+
<h2 class="sr" id="fb-tip-h">Found a problem with Eklavya?</h2>
|
|
4464
|
+
<p>Found a problem with Eklavya itself? Tell Claude <code>File feedback for Eklavya</code>; it shows you the issue before posting.</p>
|
|
4270
4465
|
<button type="button" class="set__btn" data-fb-tip-dismiss>Dismiss</button>
|
|
4271
4466
|
</aside>`;
|
|
4272
4467
|
}
|
|
@@ -4287,57 +4482,162 @@ function viewFeedbackItem(id) {
|
|
|
4287
4482
|
].join('');
|
|
4288
4483
|
}
|
|
4289
4484
|
|
|
4485
|
+
const fbAck = (id) => `<button type="button" class="btn fb__ack" data-fb-ack="${id}">${svg(ICONS.ack)}Acknowledge</button>`;
|
|
4290
4486
|
const fbActions = (id) => `<div class="fb__actions" id="fb-actions">
|
|
4291
|
-
|
|
4292
|
-
<button type="button" class="
|
|
4487
|
+
${fbAck(id)}
|
|
4488
|
+
<button type="button" class="fb__del" data-fb-del="${id}">Delete this feedback</button>
|
|
4293
4489
|
<p class="fb__msg" id="fb-msg" role="alert"></p></div>`;
|
|
4294
4490
|
|
|
4295
4491
|
const fmtWhen = (ts) => { const d = parse(ts); return d ? d.toLocaleString(undefined, { month:'short', day:'numeric', hour:'numeric', minute:'2-digit' }) : null; };
|
|
4296
4492
|
|
|
4297
|
-
/** Where the prompt was written: project, checkout,
|
|
4493
|
+
/** Where the prompt was written: project, checkout, session, when and from which host, so the note is not blind. */
|
|
4298
4494
|
function fbContext(it) {
|
|
4299
4495
|
const c = it.context ?? {};
|
|
4300
|
-
const
|
|
4301
|
-
|
|
4496
|
+
const line = (html) => `<span class="slug">${html}</span>`;
|
|
4497
|
+
const sent = [fmtWhen(c.prompted_at), c.host].filter(Boolean).join(' · ');
|
|
4302
4498
|
const rows = [
|
|
4303
|
-
['Project', `${esc(pname(it.project))}
|
|
4499
|
+
['Project', `${esc(pname(it.project))}${line(esc(it.project))}`],
|
|
4304
4500
|
c.checkout && c.checkout !== it.project ? ['Checkout', esc(c.checkout)] : null,
|
|
4501
|
+
c.session_id ? ['Session', `<a data-go="${esc(MH('session', c.session_id))}">${esc(c.session_id.slice(0, 8))}</a>`
|
|
4502
|
+
+ (c.prompts ? esc(' · ' + plural(c.prompts, 'prompt')) : '')
|
|
4503
|
+
+ (c.began && c.ended ? line(esc(`${fmtWhen(c.began)} – ${fmtWhen(c.ended)}`)) : '')] : null,
|
|
4504
|
+
sent ? ['Sent', esc(sent)] : null,
|
|
4305
4505
|
c.topic ? ['About', esc(c.topic)] : null,
|
|
4306
|
-
c.host ? ['Host', esc(c.host)] : null,
|
|
4307
|
-
fmtWhen(c.prompted_at) ? ['Sent', esc(fmtWhen(c.prompted_at))] : null,
|
|
4308
|
-
c.session_id ? ['Session', `<a data-go="${esc(MH('session', c.session_id))}">${esc(c.session_id.slice(0, 8))}</a>${esc(sessionTail)}`] : null,
|
|
4309
4506
|
].filter(Boolean);
|
|
4310
|
-
return `<section><h2 class="fb__h">Where you said it</h2
|
|
4507
|
+
return `<section class="card fb__card" aria-labelledby="fb-ctx-h"><h2 class="fb__h" id="fb-ctx-h">Where you said it</h2>
|
|
4508
|
+
<dl class="fb__ctx">${rows.map(([k, v]) => `<div><dt>${k}</dt><dd>${v}</dd></div>`).join('')}</dl></section>`;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
/** The order of the coverage strip. The headline lists gaps in the review's own order, costliest first. */
|
|
4512
|
+
const AREA_ORDER = ['outcome', 'context', 'scope', 'check'];
|
|
4513
|
+
const fbList = (xs) => xs.length < 2 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`;
|
|
4514
|
+
|
|
4515
|
+
/** An area's status: the review's word since rubric 3; before it, a gap is "left out" and anything else was never said. */
|
|
4516
|
+
const fbStatus = (review, a) => review.areas?.[a] ?? (review.gaps.some((g) => g.area === a) ? 'left_out' : 'unknown');
|
|
4517
|
+
const FB_CLS = { left_out: 'is-miss', clear: 'is-ok', not_needed: 'is-na', unknown: 'is-na' };
|
|
4518
|
+
|
|
4519
|
+
/** The conclusion first. Every word of it comes from the gap areas, never from model text, and none is a number. */
|
|
4520
|
+
function fbVerdict(it) {
|
|
4521
|
+
const { worked, gaps } = it.review;
|
|
4522
|
+
const date = it.context?.prompted_at ? fmtDate(it.context.prompted_at) : '';
|
|
4523
|
+
const label = `<p class="fb__h">Review of one prompt${date ? ' · ' + esc(date) : ''}</p>`;
|
|
4524
|
+
// Only an item cleared by migration 029 has no `worked` (parseReview requires one).
|
|
4525
|
+
if (!worked) {
|
|
4526
|
+
return `<section class="card fb__verdict" aria-labelledby="fb-verdict-h"><eklavya-mascot state="info" size="88"></eklavya-mascot>
|
|
4527
|
+
<div>${label}<h2 class="fb__vh fb__vh--old" id="fb-verdict-h">An older review: the prompt, the rewrite and the tips are below.</h2></div></section>`;
|
|
4528
|
+
}
|
|
4529
|
+
const areas = [...new Set(gaps.map((g) => g.area))].map((a) => 'the ' + (FB.areas[a] ?? a).toLowerCase());
|
|
4530
|
+
const head = areas.length ? `The agent had to guess ${fbList(areas)}.` : 'Nothing here left the agent guessing.';
|
|
4531
|
+
const cells = AREA_ORDER.map((a) => {
|
|
4532
|
+
const st = fbStatus(it.review, a);
|
|
4533
|
+
return `<li role="listitem" class="fb__cell ${FB_CLS[st]}">${svg(ICONS[a])}<span class="fb__cn">${esc(FB.areas[a])}</span> <span class="fb__cs">${esc(FB.status[st])}</span></li>`;
|
|
4534
|
+
}).join('');
|
|
4535
|
+
return `<section class="card fb__verdict" aria-labelledby="fb-verdict-h">
|
|
4536
|
+
<eklavya-mascot state="${gaps.length ? 'review' : 'complete'}" size="88"></eklavya-mascot>
|
|
4537
|
+
<div class="fb__vb">${label}<h2 class="fb__vh" id="fb-verdict-h">${esc(head)}</h2>
|
|
4538
|
+
${gaps.length ? '<p class="fb__vs">Of the prompts in that session, this one cost the most rework.</p>' : ''}
|
|
4539
|
+
<ul class="fb__cover" role="list" aria-label="What the prompt covered">${cells}</ul></div></section>`;
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
/** A long text is clamped with its own toggle; `id` names the text the toggle controls. */
|
|
4543
|
+
const FB_CLAMP = { better: 600, mine: 1500 };
|
|
4544
|
+
const fbMore = (key, id, text) => text.length > FB_CLAMP[key]
|
|
4545
|
+
? `<button type="button" class="fb__more" data-fb-more="${id}" data-focus="more:${key}" aria-expanded="false" aria-controls="${id}">Show the whole prompt</button>` : '';
|
|
4546
|
+
|
|
4547
|
+
function fbToggle(btn) {
|
|
4548
|
+
const el = $(btn.dataset.fbMore);
|
|
4549
|
+
const open = btn.getAttribute('aria-expanded') !== 'true';
|
|
4550
|
+
if (open) {
|
|
4551
|
+
el.classList.remove('is-clamped');
|
|
4552
|
+
// A height to move to, so the expansion is a transition; then no cap, so a resize cannot cut it.
|
|
4553
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
4554
|
+
setTimeout(() => { if (!el.classList.contains('is-clamped')) el.style.maxHeight = 'none'; }, 400);
|
|
4555
|
+
} else {
|
|
4556
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
4557
|
+
el.getBoundingClientRect();
|
|
4558
|
+
el.style.maxHeight = '';
|
|
4559
|
+
el.classList.add('is-clamped');
|
|
4560
|
+
}
|
|
4561
|
+
btn.setAttribute('aria-expanded', String(open));
|
|
4562
|
+
btn.textContent = open ? 'Show less' : 'Show the whole prompt';
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
/** The rewrite, escaped first; each [placeholder] it left for a detail only the developer knows is then marked. */
|
|
4566
|
+
const FB_PH = /\[[^\]\n]{2,200}\]/g;
|
|
4567
|
+
|
|
4568
|
+
function fbCompare(it) {
|
|
4569
|
+
const better = esc(it.better);
|
|
4570
|
+
const marked = better.replace(FB_PH, (m) => `<mark class="fb__ph">${m}</mark>`);
|
|
4571
|
+
const clamp = (key, text) => text.length > FB_CLAMP[key] ? ' is-clamped' : '';
|
|
4572
|
+
return `<div class="fb__grid">
|
|
4573
|
+
<section class="fb__mine" aria-labelledby="fb-mine-h"><div class="fb__ch"><h2 class="fb__h" id="fb-mine-h">Your prompt</h2></div>
|
|
4574
|
+
<div class="fb__clamp${clamp('mine', it.prompt)}" id="fb-mine-t"><blockquote class="fb__quote">${esc(it.prompt)}</blockquote></div>${fbMore('mine', 'fb-mine-t', it.prompt)}</section>
|
|
4575
|
+
<section class="fb__better" aria-labelledby="fb-better-h"><div class="fb__ch"><h2 class="fb__h" id="fb-better-h">A better prompt</h2>
|
|
4576
|
+
<button type="button" class="fb__copy" data-copy="fb-better-c" data-focus="copy:better">${svg(ICONS.copy)}<span>Copy</span></button></div>
|
|
4577
|
+
<div class="cmd cmd--block fb__clamp${clamp('better', it.better)}" id="fb-better-t"><code id="fb-better-c">${marked}</code></div>${fbMore('better', 'fb-better-t', it.better)}
|
|
4578
|
+
${marked !== better ? '<p class="fb__cap">Highlighted: details only you know. Fill them in before you reuse it.</p>' : ''}</section>
|
|
4579
|
+
</div>`;
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
/** A section's head: its mascot, its heading and one quiet line. */
|
|
4583
|
+
const fbHead = (state, id, title, cap) => `<div class="fb__sh"><eklavya-mascot state="${state}" variant="face" size="40"></eklavya-mascot>
|
|
4584
|
+
<div><h3 id="${id}">${title}</h3>${cap ? `<p class="fb__cap">${cap}</p>` : ''}</div></div>`;
|
|
4585
|
+
|
|
4586
|
+
function fbGaps(gaps) {
|
|
4587
|
+
if (!gaps.length) {
|
|
4588
|
+
return `<section class="fb__sec" aria-labelledby="fb-gaps-h">${fbHead('success', 'fb-gaps-h', 'What it left out', '')}
|
|
4589
|
+
<p class="fb__note">Nothing the agent had to guess.</p></section>`;
|
|
4590
|
+
}
|
|
4591
|
+
const rows = gaps.map((g) => `<li><span class="fb__tile">${svg(ICONS[g.area] ?? '')}</span><div class="fb__gb">
|
|
4592
|
+
<p class="fb__gt">${esc(FB.areas[g.area] ?? g.area)} <small>${esc(FB.hints[g.area] ?? '')}</small></p>
|
|
4593
|
+
<p class="fb__note">${esc(g.missing)}</p>
|
|
4594
|
+
${g.evidence ? `<p class="fb__ev"><span class="fb__evl" aria-hidden="true">You, later</span><span class="sr">You said later: </span><q>${esc(g.evidence)}</q></p>` : ''}</div></li>`).join('');
|
|
4595
|
+
return `<section class="fb__sec" aria-labelledby="fb-gaps-h">${fbHead('warning', 'fb-gaps-h', 'What it left out', 'What the agent had to guess, costliest first')}
|
|
4596
|
+
<ol class="fb__ds">${rows}</ol></section>`;
|
|
4311
4597
|
}
|
|
4312
4598
|
|
|
4313
|
-
/**
|
|
4599
|
+
/** The decision, beside the item: the rule, Acknowledge and Delete. Only while it is pending. */
|
|
4600
|
+
const fbDecide = (it) => `<section class="card fb__card fb__decide" aria-labelledby="fb-decide-h">
|
|
4601
|
+
<div class="fb__dh"><eklavya-mascot state="info" variant="face" size="32"></eklavya-mascot><h3 id="fb-decide-h">Done reading?</h3></div>
|
|
4602
|
+
<p class="fb__dp">${esc(FB.rulePending)}</p>
|
|
4603
|
+
${fbActions(it.id)}
|
|
4604
|
+
<p class="fb__foot"><span>1 waiting</span><span>${esc(S.feedback.acknowledged + ' in history')}</span></p></section>`;
|
|
4605
|
+
|
|
4606
|
+
/**
|
|
4607
|
+
* One item: the verdict, the two prompts, what worked, what it left out and the
|
|
4608
|
+
* habits for next time, with the decision held in view beside them (a fixed bar
|
|
4609
|
+
* at 900px and below). Acknowledge only while it is pending.
|
|
4610
|
+
*/
|
|
4314
4611
|
function drawFbItem(it) {
|
|
4315
4612
|
const pending = !it.acknowledged_at;
|
|
4316
4613
|
const { worked, gaps } = it.review;
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
return `<article class="card fb" aria-label="Feedback on one of your prompts">
|
|
4329
|
-
${fbContext(it)}
|
|
4330
|
-
<div class="fb__grid">
|
|
4331
|
-
<section><h2 class="fb__h">Your prompt</h2><blockquote class="fb__quote">${esc(it.prompt)}</blockquote></section>
|
|
4332
|
-
<section><h2 class="fb__h">A better prompt</h2>
|
|
4333
|
-
<div class="cmd cmd--block"><code>${esc(it.better)}</code><button type="button" class="cmd__copy" data-copy data-focus="copy:better">Copy</button></div></section>
|
|
4614
|
+
chart('fb-rail', fbRailFit);
|
|
4615
|
+
return `<article class="fb" aria-label="Feedback on one of your prompts">
|
|
4616
|
+
<div class="fb__story">
|
|
4617
|
+
${fbVerdict(it)}
|
|
4618
|
+
${fbCompare(it)}
|
|
4619
|
+
${worked ? `<section class="fb__worked" aria-labelledby="fb-worked-h"><eklavya-mascot state="success" variant="face" size="40"></eklavya-mascot>
|
|
4620
|
+
<div><h3 class="fb__h" id="fb-worked-h">What worked</h3><p>${esc(worked)}</p></div></section>` : ''}
|
|
4621
|
+
${worked ? fbGaps(gaps) : ''}
|
|
4622
|
+
<section class="fb__sec" aria-labelledby="fb-tips-h">${fbHead('tip', 'fb-tips-h', 'Next time', worked ? 'Habits that would have saved those follow-ups' : 'Habits for next time')}
|
|
4623
|
+
<ol class="fb__tips" data-n="${Math.min(it.tips.length, 3)}">${it.tips.map((t) => `<li>${esc(t)}</li>`).join('')}</ol></section>
|
|
4624
|
+
${pending ? fbTip() : ''}
|
|
4334
4625
|
</div>
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4626
|
+
<aside class="fb__rail" id="fb-rail" aria-label="About this prompt">
|
|
4627
|
+
${fbContext(it)}
|
|
4628
|
+
${pending ? fbDecide(it) : `<p class="fb__acked">Acknowledged ${esc(fmtDate(it.acknowledged_at))}</p>`}
|
|
4629
|
+
</aside>
|
|
4630
|
+
${pending ? `<div class="fb__bar" role="region" aria-label="Acknowledge this feedback"><p>${esc(FB.barRule)}</p>${fbAck(it.id)}</div>` : ''}
|
|
4338
4631
|
</article>`;
|
|
4339
4632
|
}
|
|
4340
4633
|
|
|
4634
|
+
/** The rail stays in view while it fits the window; one taller than that would hide its own end, so it scrolls with the page. */
|
|
4635
|
+
function fbRailFit() {
|
|
4636
|
+
const rail = $('fb-rail');
|
|
4637
|
+
if (rail) rail.classList.toggle('is-tall', rail.offsetHeight > innerHeight - 48);
|
|
4638
|
+
}
|
|
4639
|
+
addEventListener('resize', fbRailFit);
|
|
4640
|
+
|
|
4341
4641
|
function viewFeedbackHistory() {
|
|
4342
4642
|
const p = new URLSearchParams();
|
|
4343
4643
|
if (PROJECT !== null) p.set('project', PROJECT);
|
|
@@ -4393,11 +4693,18 @@ function fbFail(e) {
|
|
|
4393
4693
|
|
|
4394
4694
|
function fbAcknowledge(btn) {
|
|
4395
4695
|
if (btn.disabled) return;
|
|
4396
|
-
|
|
4696
|
+
// The rail's button and the bar's are one action: pressing either holds both.
|
|
4697
|
+
const both = [...document.querySelectorAll('[data-fb-ack]')];
|
|
4698
|
+
both.forEach((b) => { b.disabled = true; });
|
|
4397
4699
|
postJson('/api/feedback/acknowledge', { id: +btn.dataset.fbAck })
|
|
4398
4700
|
// The item, its history row and the next one waiting all changed: what was read of them is gone.
|
|
4399
4701
|
.then((d) => { S.feedback = d.state; respForget('/api/feedback'); fbDone(FB.announce); })
|
|
4400
|
-
.catch((e) => {
|
|
4702
|
+
.catch((e) => {
|
|
4703
|
+
both.forEach((b) => { b.disabled = false; });
|
|
4704
|
+
fbFail(e);
|
|
4705
|
+
// Under the bar the message is in the rail card, after the story.
|
|
4706
|
+
if (innerWidth <= 900) $('fb-msg')?.scrollIntoView({ block: 'nearest' });
|
|
4707
|
+
});
|
|
4401
4708
|
}
|
|
4402
4709
|
|
|
4403
4710
|
function fbAskDelete(btn) {
|
|
@@ -4531,6 +4838,13 @@ const ICONS = {
|
|
|
4531
4838
|
history: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
|
4532
4839
|
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
4533
4840
|
project: '<path d="M3 7l9-4 9 4-9 4-9-4z"/><path d="M3 12l9 4 9-4M3 17l9 4 9-4"/>',
|
|
4841
|
+
// The four areas a prompt can leave the agent to guess, and the Feedback item's two actions.
|
|
4842
|
+
outcome: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
|
4843
|
+
context: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
4844
|
+
scope: '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/>',
|
|
4845
|
+
check: '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
|
|
4846
|
+
copy: '<rect width="14" height="14" x="8" y="8"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
|
|
4847
|
+
ack: '<path d="M20 6 9 17l-5-5"/>',
|
|
4534
4848
|
};
|
|
4535
4849
|
const svg = (paths, cls = '') =>
|
|
4536
4850
|
`<svg${cls ? ` class="${cls}"` : ''} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
@@ -4557,7 +4871,7 @@ const WORKFLOWS = {
|
|
|
4557
4871
|
],
|
|
4558
4872
|
pages: {
|
|
4559
4873
|
dashboard: { label: 'Dashboard', view: () => viewLearningDashboard() },
|
|
4560
|
-
concepts: { label: 'Concepts', view: (p) => viewConcepts(p), query: ['domain'], inPage: true },
|
|
4874
|
+
concepts: { label: 'Concepts', view: (p) => viewConcepts(p), query: ['domain', 'tag'], inPage: true },
|
|
4561
4875
|
concept: { parent: 'concepts', view: (p) => viewConcept(p) },
|
|
4562
4876
|
review: { label: 'Review', view: (p) => viewReview(p), inPage: true },
|
|
4563
4877
|
sessions: { label: 'Sessions', view: () => viewSessions() },
|
|
@@ -5624,7 +5938,7 @@ function catchUp() {
|
|
|
5624
5938
|
* opens (`detailSelector`). A control with none is not carried over a redraw, so every control the reader can Tab to has one.
|
|
5625
5939
|
*/
|
|
5626
5940
|
const FOCUS_KEYS = ['data-focus', 'data-tab', 'data-tab-id', 'data-state', 'data-unset', 'data-go',
|
|
5627
|
-
'data-fb-ack', 'data-fb-del', 'data-fb-yes', 'data-fb-no', 'data-fb-tip-dismiss'];
|
|
5941
|
+
'data-fb-ack', 'data-fb-del', 'data-fb-more', 'data-fb-yes', 'data-fb-no', 'data-fb-tip-dismiss'];
|
|
5628
5942
|
function focusKey(el) {
|
|
5629
5943
|
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
5630
5944
|
for (const a of FOCUS_KEYS) {
|
|
@@ -5732,6 +6046,8 @@ function onActivate(e) {
|
|
|
5732
6046
|
if (e.target.closest('[data-slot][aria-busy="true"][data-kept]')) return;
|
|
5733
6047
|
const copy = e.target.closest('[data-copy]');
|
|
5734
6048
|
if (copy) return copyCommand(copy);
|
|
6049
|
+
const more = e.target.closest('[data-fb-more]');
|
|
6050
|
+
if (more) return fbToggle(more);
|
|
5735
6051
|
if (e.target.closest('[data-fb-tip-dismiss]')) return fbTipDismiss();
|
|
5736
6052
|
const ack = e.target.closest('[data-fb-ack]');
|
|
5737
6053
|
if (ack) return fbAcknowledge(ack);
|
|
@@ -5759,7 +6075,7 @@ function onActivate(e) {
|
|
|
5759
6075
|
if (chipState) {
|
|
5760
6076
|
T.concepts.page = 1;
|
|
5761
6077
|
const s = chipState.dataset.state;
|
|
5762
|
-
return goTo(LH('concepts', s === 'all' ? null : s, { domain: CUR.query.get('domain') }));
|
|
6078
|
+
return goTo(LH('concepts', s === 'all' ? null : s, { domain: CUR.query.get('domain'), tag: CUR.query.get('tag') }));
|
|
5763
6079
|
}
|
|
5764
6080
|
const tab = e.target.closest('[data-tab]');
|
|
5765
6081
|
if (tab) { T.review.page = 1; return goTo(LH('review', tab.dataset.tab === 'due' ? null : tab.dataset.tab)); }
|
|
@@ -5833,6 +6149,7 @@ $('view').addEventListener('change', (e) => {
|
|
|
5833
6149
|
return;
|
|
5834
6150
|
}
|
|
5835
6151
|
if (e.target.id === 'dom') { T.concepts.page = 1; setQuery({ domain: e.target.value }); }
|
|
6152
|
+
if (e.target.id === 'ctag') { T.concepts.page = 1; setQuery({ tag: e.target.value }); }
|
|
5836
6153
|
if (e.target.id === 'sort') { T.concepts.sort = e.target.value; T.concepts.page = 1; render(); }
|
|
5837
6154
|
if (e.target.id === 'mtag') { T.memory.page = 1; setQuery({ tag: e.target.value }); }
|
|
5838
6155
|
if (e.target.id === 'mtype') {
|