eklavya 1.56.0 → 1.57.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 +5 -2
- package/dist/assets/dashboard.html +294 -6
- package/dist/cli.js +59 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-path.js +10 -1
- package/dist/config-path.js.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard.js +69 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/feedback-review.js +177 -0
- package/dist/feedback-review.js.map +1 -0
- package/dist/feedback.js +275 -0
- package/dist/feedback.js.map +1 -0
- package/dist/hooks/prompt-submit-nudge.js +1 -15
- package/dist/hooks/prompt-submit-nudge.js.map +1 -1
- package/dist/hooks/session-start.js +31 -3
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/memory/provider.js +4 -4
- package/dist/memory/provider.js.map +1 -1
- package/dist/migrations/026_feedback.sql +51 -0
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/skills/CLAUDE.md +1 -1
- package/dist/prompt-text.js +28 -0
- package/dist/prompt-text.js.map +1 -0
- package/dist/relocate.js +1 -0
- package/dist/relocate.js.map +1 -1
- package/dist/telemetry-send.js +12 -4
- package/dist/telemetry-send.js.map +1 -1
- package/dist/tools/config_tools.js +4 -0
- package/dist/tools/config_tools.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +71 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -104,12 +104,15 @@ npx eklavya doctor
|
|
|
104
104
|
npx eklavya memory status
|
|
105
105
|
npx eklavya config get
|
|
106
106
|
npx eklavya dashboard
|
|
107
|
+
npx eklavya feedback generate
|
|
107
108
|
npx eklavya db-path
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
The [CLI reference](https://eklavya-run.web.app/docs/cli/) lists all commands and
|
|
111
|
-
flags.
|
|
112
|
-
|
|
112
|
+
flags. `feedback generate` reviews one of your earlier prompts when `feedback.enabled`,
|
|
113
|
+
memory and `providers.observer` are set. The dashboard reads the database and has five guarded writes: a setting
|
|
114
|
+
change, a correction of a missed answer (which can also complete a level), and acknowledging, deleting or counting the opening of a
|
|
115
|
+
feedback item (`POST /api/feedback/acknowledge`, `/delete`, `/opened`; the reads are `GET /api/feedback` and `/api/feedback/list`). An open page polls `/api/cursor`, a
|
|
113
116
|
counter that moves on every write it would show, and offers a refresh. A request
|
|
114
117
|
it cannot parse gets a 400 without stopping the server
|
|
115
118
|
([what it is, underneath](https://eklavya-run.web.app/docs/dashboard/#what-it-is-underneath)).
|
|
@@ -100,7 +100,16 @@
|
|
|
100
100
|
.wf__main svg, .picker svg { width:17px; height:17px; flex:none; color:var(--spot); }
|
|
101
101
|
.wf__caret { flex:none; width:36px; background:none; border:0; border-left:1px solid var(--line-2);
|
|
102
102
|
color:var(--dim); cursor:pointer; display:grid; place-items:center; }
|
|
103
|
+
.wf__caret { position:relative; }
|
|
103
104
|
.wf__caret:hover, .wf__caret[aria-expanded="true"] { background:var(--mass); color:var(--ink); }
|
|
105
|
+
/* The unread count: the attention role (`--warning`) as a fill, `--bg` text on it. The number is
|
|
106
|
+
visible text, so forced-colours mode keeps it; the labels carry it for a screen reader. */
|
|
107
|
+
.wf__badge, .picker .badge { display:inline-grid; place-items:center; min-width:16px; height:16px; padding:0 4px;
|
|
108
|
+
border-radius:50%; background:var(--warning); color:var(--bg); font-style:normal;
|
|
109
|
+
font-family:var(--font-mono); font-size:10px; font-weight:var(--weight-semibold); line-height:1; }
|
|
110
|
+
.wf__badge { position:absolute; top:2px; right:2px; }
|
|
111
|
+
.wf__badge[hidden] { display:none; }
|
|
112
|
+
.picker .badge { flex:none; }
|
|
104
113
|
.wf__caret svg { width:14px; height:14px; transition:transform var(--duration-fast) var(--ease); }
|
|
105
114
|
.wf__caret[aria-expanded="true"] svg { transform:rotate(180deg); }
|
|
106
115
|
|
|
@@ -675,6 +684,40 @@
|
|
|
675
684
|
.cmd__copy:focus-visible { outline:none; box-shadow:var(--ring); }
|
|
676
685
|
.cmd__note { margin:var(--space-2) 0 0; color:var(--dim); font-size:13px; }
|
|
677
686
|
.starts > .cmd__note { margin-top:var(--space-3); }
|
|
687
|
+
/* ---------- Feedback: one reviewed prompt at a time ---------- */
|
|
688
|
+
.fb__rule { border:1px solid var(--line-2); border-left:2px solid var(--spot); background:var(--panel);
|
|
689
|
+
padding:var(--space-4) var(--space-5); }
|
|
690
|
+
.fb__rule p { margin:0; color:var(--ink); font-size:14px; max-width:72ch; }
|
|
691
|
+
.fb__rule--near { margin:0 0 var(--space-4); }
|
|
692
|
+
.fb { display:grid; gap:var(--space-6); }
|
|
693
|
+
.fb__grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:var(--space-5); }
|
|
694
|
+
.fb__h { font-family:var(--font-mono); font-size:10.5px; letter-spacing:var(--tracking-caps); text-transform:uppercase;
|
|
695
|
+
color:var(--faint); font-weight:var(--weight-semibold); margin:0 0 var(--space-2); }
|
|
696
|
+
.fb__quote { margin:0; padding-left:var(--space-4); border-left:2px solid var(--spot); color:var(--ink);
|
|
697
|
+
white-space:pre-wrap; overflow-wrap:anywhere; font-size:14px; }
|
|
698
|
+
.cmd--block { margin-top:0; align-items:flex-start; }
|
|
699
|
+
.cmd--block code { white-space:pre-wrap; overflow-wrap:anywhere; overflow-x:visible; }
|
|
700
|
+
.fb__ds, .fb__tips { list-style:none; margin:0; padding:0; display:grid; gap:var(--space-3); }
|
|
701
|
+
.fb__ds li { display:grid; grid-template-columns:120px max-content minmax(0, 1fr); gap:var(--space-3); align-items:baseline; }
|
|
702
|
+
.fb__dn { font-family:var(--font-mono); font-size:12px; color:var(--dim); }
|
|
703
|
+
.fb__note { color:var(--dim); font-size:13.5px; overflow-wrap:anywhere; }
|
|
704
|
+
.fb__ev { display:block; color:var(--faint); font-size:12.5px; }
|
|
705
|
+
.fb__tips li { color:var(--dim); font-size:13.5px; padding-left:var(--space-4); position:relative; }
|
|
706
|
+
.fb__tips li::before { content:'–'; position:absolute; left:0; color:var(--faint); }
|
|
707
|
+
.fb__actions { display:flex; gap:var(--space-3); flex-wrap:wrap; align-items:center; }
|
|
708
|
+
.fb__ask { display:inline-flex; gap:var(--space-3); flex-wrap:wrap; align-items:center; color:var(--dim); font-size:13.5px; }
|
|
709
|
+
.fb__msg { margin:0; color:var(--warning); font-size:13px; }
|
|
710
|
+
.fb__sts { display:flex; gap:var(--space-2); flex-wrap:wrap; }
|
|
711
|
+
.fb__tip h2 { font-family:var(--font-disp); font-size:var(--text-md); font-weight:var(--weight-semibold); margin:0 0 var(--space-2); }
|
|
712
|
+
.fb__tip p { margin:0 0 var(--space-4); color:var(--dim); max-width:70ch; }
|
|
713
|
+
.fb__tip .fb__quote { margin-bottom:var(--space-4); }
|
|
714
|
+
.fb__why p { margin:0 0 var(--space-3); color:var(--dim); max-width:70ch; }
|
|
715
|
+
@media (max-width:560px) {
|
|
716
|
+
.fb__grid { grid-template-columns:minmax(0, 1fr); }
|
|
717
|
+
.fb__ds li { grid-template-columns:minmax(0, 1fr); gap:var(--space-1); }
|
|
718
|
+
.fb__actions > button, .fb__actions a { flex:1 1 100%; min-height:44px; }
|
|
719
|
+
.fb__ask > button { flex:1 1 40%; min-height:44px; }
|
|
720
|
+
}
|
|
678
721
|
.starts__more > summary { cursor:pointer; color:var(--dim); font-size:13px; padding:var(--space-2) 0; }
|
|
679
722
|
.starts__more > summary:hover { color:var(--spot); }
|
|
680
723
|
.starts__more > summary:focus-visible { outline:none; box-shadow:var(--ring); }
|
|
@@ -850,7 +893,7 @@
|
|
|
850
893
|
<div class="wf" id="wf">
|
|
851
894
|
<a class="wf__main" id="wf-main" href="#/learning/dashboard"></a>
|
|
852
895
|
<button type="button" class="wf__caret" id="wf-caret" aria-label="Switch workflow"
|
|
853
|
-
aria-haspopup="menu" aria-expanded="false" aria-controls="wf-menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button>
|
|
896
|
+
aria-haspopup="menu" aria-expanded="false" aria-controls="wf-menu"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg><span class="wf__badge" id="wf-badge" aria-hidden="true" hidden>1</span></button>
|
|
854
897
|
</div>
|
|
855
898
|
|
|
856
899
|
<nav class="side__nav" id="nav" aria-label="Sections"></nav>
|
|
@@ -890,6 +933,7 @@
|
|
|
890
933
|
<!-- The same markup `loader()` writes; the script has not run yet. -->
|
|
891
934
|
<div class="loader loader--page" role="status" aria-live="polite"><div class="mark" aria-hidden="true"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4c5 2.5 5 13.5 0 16"/><line x1="6" y1="4" x2="6" y2="20"/><line x1="4" y1="12" x2="18" y2="12"/><polyline points="14.5 8.5 18 12 14.5 15.5"/></svg><svg class="loader__trace" viewBox="0 0 40 40" aria-hidden="true"><rect x="0.75" y="0.75" width="38.5" height="38.5" pathLength="160"/></svg></div><span>Loading your history…</span></div>
|
|
892
935
|
</div>
|
|
936
|
+
<span class="sr" id="fb-live" role="status" aria-live="polite"></span>
|
|
893
937
|
<footer class="foot inner">
|
|
894
938
|
<span>Read from <code id="dbline">~/.eklavya/knowledge.db</code> — nothing on this page leaves your machine.<span id="foot-note"></span></span>
|
|
895
939
|
<span id="dials"></span>
|
|
@@ -988,6 +1032,7 @@ const T = {
|
|
|
988
1032
|
memory: { page:1, q:'', tag:'all', since:'', until:'' },
|
|
989
1033
|
reuse: { page:1, only:'all' },
|
|
990
1034
|
artifacts:{ page:1, q:'' },
|
|
1035
|
+
fbhistory:{ page:1 },
|
|
991
1036
|
};
|
|
992
1037
|
const PER = 20;
|
|
993
1038
|
|
|
@@ -2762,7 +2807,7 @@ function viewHealth() {
|
|
|
2762
2807
|
/* ============================================================
|
|
2763
2808
|
Workflows and routes
|
|
2764
2809
|
|
|
2765
|
-
|
|
2810
|
+
Five workflows, one shell. The registry below is the only description of
|
|
2766
2811
|
any of them: the sidebar, the picker, the route resolver and the page renderer all
|
|
2767
2812
|
read it, so the selector, the highlighted item and the screen cannot disagree
|
|
2768
2813
|
about where the reader is.
|
|
@@ -3057,7 +3102,7 @@ for (const ev of ['wheel', 'touchmove', 'keydown']) {
|
|
|
3057
3102
|
registry guard checks every row against the fixture. */
|
|
3058
3103
|
const TIPS = [
|
|
3059
3104
|
{ id: 'wf-switch', where: ['learning/dashboard'], el: '#wf-caret', side: 'right', title: 'Switch workflow',
|
|
3060
|
-
text: 'Eklavya has
|
|
3105
|
+
text: 'Eklavya has five workflows: Learning, Memory, Artifacts, Feedback and Settings. Switch between them here.' },
|
|
3061
3106
|
{ id: 'how-this-works', where: ['learning/dashboard', 'memory/dashboard'], el: '#view details.about > summary', title: 'How this works',
|
|
3062
3107
|
text: 'Every page explains where its numbers come from. Open this to read it.' },
|
|
3063
3108
|
{ id: 'project-scope', where: ['learning/sessions', 'memory/timeline'], el: '#proj', side: 'right', title: 'Project scope',
|
|
@@ -3488,6 +3533,207 @@ function drawSettings(d, scope) {
|
|
|
3488
3533
|
].join('');
|
|
3489
3534
|
}
|
|
3490
3535
|
|
|
3536
|
+
/* ============================================================
|
|
3537
|
+
Feedback: one reviewed prompt at a time
|
|
3538
|
+
|
|
3539
|
+
The rule is the product: nothing new is made until the reader presses
|
|
3540
|
+
Acknowledge, and only a press (a POST the server saw) acknowledges. Opening
|
|
3541
|
+
the page, reloading it or following a link to an item never does. The state
|
|
3542
|
+
payload says whether an item waits (never its text); the page that shows it
|
|
3543
|
+
asks `/api/feedback`.
|
|
3544
|
+
============================================================ */
|
|
3545
|
+
const FB = {
|
|
3546
|
+
ruleEmpty: 'Eklavya reviews one prompt at a time. When one is ready it appears here, and the next waits until you press Acknowledge.',
|
|
3547
|
+
rulePending: 'Feedback pauses until you acknowledge this one. Read it, then press Acknowledge to get the next.',
|
|
3548
|
+
announce: 'Acknowledged. Feedback resumes at your next session.',
|
|
3549
|
+
dims: [['delegation', 'Delegation'], ['description', 'Description'], ['discernment', 'Discernment'], ['diligence', 'Diligence']],
|
|
3550
|
+
status: { strong: 'Strong', mixed: 'Mixed', missing: 'Missing', not_visible: "Can't tell from this prompt" },
|
|
3551
|
+
// `not_visible` is the muted role, never a weakness.
|
|
3552
|
+
pill: { strong: 'pill ok', mixed: 'pill', missing: 'pill due', not_visible: 'pill gone' },
|
|
3553
|
+
counted: false,
|
|
3554
|
+
observerCmd: 'eklavya config set providers.observer <model>',
|
|
3555
|
+
about: `Your prompts are already recorded by memory. After a session ends, in the background and never in the middle of your work,
|
|
3556
|
+
Eklavya asks your observer model (the one in <code>providers.observer</code>, running through Claude Code on your own subscription)
|
|
3557
|
+
to review one of that session's prompts against Anthropic's 4D framework: Delegation, Description, Discernment and Diligence.
|
|
3558
|
+
Only the text of up to eight of your own prompts from that one session goes to that model, and nothing else leaves your machine.
|
|
3559
|
+
Discernment and Diligence can only be seen in what you did after the prompt, so they say "Can't tell from this prompt" when nothing shows them.
|
|
3560
|
+
There are no scores, and nothing here changes your mastery.`,
|
|
3561
|
+
};
|
|
3562
|
+
|
|
3563
|
+
/** A command in a copyable block, as the start-review commands are. */
|
|
3564
|
+
const fbCmd = (text) => `<div class="cmd"><code>${esc(text)}</code><button type="button" class="cmd__copy" data-copy>Copy</button></div>`;
|
|
3565
|
+
|
|
3566
|
+
/** Why there is no item: the one reason, in plain words. */
|
|
3567
|
+
function fbEmpty(F) {
|
|
3568
|
+
let body;
|
|
3569
|
+
if (!F.enabled) {
|
|
3570
|
+
body = `<p>Prompt feedback is off. Turn on <code>feedback.enabled</code> in Settings to start.</p>
|
|
3571
|
+
<a class="btn" data-go="${esc(href('settings', 'user'))}">Open Settings</a>`;
|
|
3572
|
+
} else if (!F.memory) {
|
|
3573
|
+
body = `<p>Prompt feedback needs memory. Turn on <code>memory.enabled</code>.</p>`;
|
|
3574
|
+
} else if (!F.observer) {
|
|
3575
|
+
body = `<p>Prompt feedback needs an observer model. Run <code>eklavya config set providers.observer <model></code>.</p>${fbCmd(FB.observerCmd)}
|
|
3576
|
+
<p class="cmd__note">Put the model's name in place of <model>, for example claude-haiku-4-5. Setting it lets Eklavya send your prompts to that model, through your Claude Code login.</p>`;
|
|
3577
|
+
} else if (F.failed) {
|
|
3578
|
+
body = `<p>Couldn't review the last session. It will try again at a later start.</p>`;
|
|
3579
|
+
} else {
|
|
3580
|
+
body = `<p>Nothing to review yet. When a session ends, the next one you start reviews one of its prompts.</p>`;
|
|
3581
|
+
}
|
|
3582
|
+
return `<div class="card fb__why" id="fb-empty">${body}</div>`;
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
function viewFeedbackDashboard() {
|
|
3586
|
+
const F = S.feedback;
|
|
3587
|
+
if (F.pending) fill('fb-item', '/api/feedback', (d) => d.item ? drawFbItem(d.item) : fbEmpty(S.feedback));
|
|
3588
|
+
if (F.pending && !FB.counted) { FB.counted = true; AFTER.push(fbOpened); }
|
|
3589
|
+
const counts = [F.pending ? '1 waiting' : 'nothing waiting', plural(F.acknowledged, 'acknowledged item')];
|
|
3590
|
+
return [
|
|
3591
|
+
pageHead('Feedback', counts, FB.about),
|
|
3592
|
+
`<div class="fb__rule" id="fb-rule"><p>${esc(F.pending ? FB.rulePending : FB.ruleEmpty)}</p></div>`,
|
|
3593
|
+
F.pending ? `<div id="fb-item">${loader('Loading your feedback…')}</div>` : fbEmpty(F),
|
|
3594
|
+
fbTip(),
|
|
3595
|
+
`<span class="sr" id="copy-live" aria-live="polite"></span>`,
|
|
3596
|
+
].join('');
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
/**
|
|
3600
|
+
* The card that says feedback about Eklavya itself goes through Claude. It is a
|
|
3601
|
+
* card and not a tip bubble: the example and the privacy line run past what a
|
|
3602
|
+
* bubble holds, and it is information rather than a nudge, so storage that
|
|
3603
|
+
* throws means it is shown (the tips engine hides in that case).
|
|
3604
|
+
*/
|
|
3605
|
+
const FB_TIP_KEY = 'eklavya-dash-feedback-tip';
|
|
3606
|
+
function fbTip() {
|
|
3607
|
+
try { if (localStorage.getItem(FB_TIP_KEY) === '1') return ''; } catch (e) { /* shown */ }
|
|
3608
|
+
return `<aside class="card fb__tip" id="fb-tip" aria-labelledby="fb-tip-h">
|
|
3609
|
+
<h2 id="fb-tip-h">Found a problem with Eklavya?</h2>
|
|
3610
|
+
<p>Tell Claude, in plain words. It drafts a GitHub issue, shows you exactly what it will post with paths, project names and code removed, and files it only after you say yes.</p>
|
|
3611
|
+
<blockquote class="fb__quote">That question about retries was wrong. File feedback for Eklavya.</blockquote>
|
|
3612
|
+
<button type="button" class="set__btn" data-fb-tip-dismiss>Dismiss</button>
|
|
3613
|
+
</aside>`;
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
function fbTipDismiss() {
|
|
3617
|
+
try { localStorage.setItem(FB_TIP_KEY, '1'); } catch (e) { /* hidden for now, shown again next time */ }
|
|
3618
|
+
$('fb-tip')?.remove();
|
|
3619
|
+
const h = document.querySelector('#view h1');
|
|
3620
|
+
if (h) { h.tabIndex = -1; h.focus({ preventScroll: true }); }
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
function viewFeedbackItem(id) {
|
|
3624
|
+
fill('fb-item', '/api/feedback?id=' + encodeURIComponent(id), (d) => drawFbItem(d.item));
|
|
3625
|
+
return [
|
|
3626
|
+
pageHead('Feedback item', [], FB.about, `<a class="page__back" data-go="${esc(href('feedback', 'history'))}">← History</a>`),
|
|
3627
|
+
`<div id="fb-item">${loader('Loading your feedback…')}</div>`,
|
|
3628
|
+
`<span class="sr" id="copy-live" aria-live="polite"></span>`,
|
|
3629
|
+
].join('');
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
const fbActions = (id) => `<div class="fb__actions" id="fb-actions">
|
|
3633
|
+
<button type="button" class="btn" data-fb-ack="${id}">Acknowledge</button>
|
|
3634
|
+
<button type="button" class="set__btn" data-fb-del="${id}">Delete</button>
|
|
3635
|
+
<p class="fb__msg" id="fb-msg" role="alert"></p></div>`;
|
|
3636
|
+
|
|
3637
|
+
/** One item: your prompt, a better one, a note per D, tips. Actions only while it is pending. */
|
|
3638
|
+
function drawFbItem(it) {
|
|
3639
|
+
const pending = !it.acknowledged_at;
|
|
3640
|
+
const rows = FB.dims.map(([key, name]) => {
|
|
3641
|
+
const d = it.review[key];
|
|
3642
|
+
return `<li><span class="fb__dn">${esc(name)}</span> <span class="${FB.pill[d.status] ?? 'pill'}">${esc(FB.status[d.status] ?? d.status)}</span> `
|
|
3643
|
+
+ `<span class="fb__note">${d.status === 'not_visible' ? '' : esc(d.note)}`
|
|
3644
|
+
+ `${d.evidence ? `<span class="fb__ev">From your later prompt: “${esc(d.evidence)}”</span>` : ''}</span></li>`;
|
|
3645
|
+
}).join('');
|
|
3646
|
+
// Said again right above the button on the very first item a reader sees.
|
|
3647
|
+
const first = pending && S.feedback.acknowledged === 0
|
|
3648
|
+
? `<div class="fb__rule fb__rule--near"><p>${esc(FB.rulePending)}</p></div>` : '';
|
|
3649
|
+
return `<article class="card fb" aria-label="Feedback on one of your prompts">
|
|
3650
|
+
<div class="fb__grid">
|
|
3651
|
+
<section><h2 class="fb__h">Your prompt</h2><blockquote class="fb__quote">${esc(it.prompt)}</blockquote></section>
|
|
3652
|
+
<section><h2 class="fb__h">A better prompt</h2>
|
|
3653
|
+
<div class="cmd cmd--block"><code>${esc(it.better)}</code><button type="button" class="cmd__copy" data-copy>Copy</button></div></section>
|
|
3654
|
+
</div>
|
|
3655
|
+
<section><h2 class="fb__h">The four Ds</h2><ul class="fb__ds">${rows}</ul></section>
|
|
3656
|
+
<section><h2 class="fb__h">Tips</h2><ul class="fb__tips">${it.tips.map((t) => `<li>${esc(t)}</li>`).join('')}</ul></section>
|
|
3657
|
+
${pending ? `<section>${first}${fbActions(it.id)}</section>` : ''}
|
|
3658
|
+
</article>`;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
function viewFeedbackHistory() {
|
|
3662
|
+
const p = new URLSearchParams();
|
|
3663
|
+
if (PROJECT !== null) p.set('project', PROJECT);
|
|
3664
|
+
p.set('page', T.fbhistory.page);
|
|
3665
|
+
fill('fb-rows', '/api/feedback/list?' + p, drawFbHistory);
|
|
3666
|
+
return [
|
|
3667
|
+
pageHead('History', [plural(S.feedback.acknowledged, 'acknowledged item')], FB.about),
|
|
3668
|
+
`<div class="card card--flush" id="fb-rows">${loader('Loading history…')}</div>`,
|
|
3669
|
+
].join('');
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
function drawFbHistory(d) {
|
|
3673
|
+
T.fbhistory.page = d.page;
|
|
3674
|
+
const pills = (st) => `<span class="fb__sts">${FB.dims.map(([k, n]) =>
|
|
3675
|
+
`<span class="${FB.pill[st[k]] ?? 'pill'}" title="${esc(n)}: ${esc(FB.status[st[k]] ?? st[k])}">${esc(n)} · ${st[k] === 'not_visible' ? '—' : esc(FB.status[st[k]] ?? st[k])}</span>`).join('')}</span>`;
|
|
3676
|
+
const rows = d.items.map((i) => `<tr class="row" data-go="${esc(href('feedback', 'item', i.id))}">
|
|
3677
|
+
<td>${esc(fmtDate(i.acknowledged_at))}</td><td>${esc(pname(i.project))}</td>
|
|
3678
|
+
<td>${esc(i.prompt)}${i.prompt.length >= 80 ? '…' : ''}</td><td>${pills(i.statuses)}</td></tr>`);
|
|
3679
|
+
return table([{ t: 'Acknowledged' }, { t: 'Project' }, { t: 'Your prompt' }, { t: 'The four Ds' }], rows)
|
|
3680
|
+
+ pager('fbhistory', d.total, d.page, d.per);
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
/**
|
|
3684
|
+
* Counted once per opening with an item waiting: a re-render of the same page
|
|
3685
|
+
* (a resize) is not another opening. `via` says how the reader got here
|
|
3686
|
+
* (the greeting link, the badge) and is dropped from the URL once counted, so a
|
|
3687
|
+
* link they share does not carry it. Anything else is a typed or bookmarked link.
|
|
3688
|
+
*/
|
|
3689
|
+
function fbOpened() {
|
|
3690
|
+
const via = CUR.query.get('via');
|
|
3691
|
+
postJson('/api/feedback/opened', { via: via === 'greeting' || via === 'badge' ? via : 'direct' }).catch(() => {});
|
|
3692
|
+
if (via === null) return;
|
|
3693
|
+
const q = new URLSearchParams(CUR.query);
|
|
3694
|
+
q.delete('via');
|
|
3695
|
+
const s = q.toString();
|
|
3696
|
+
history.replaceState(history.state, '', location.pathname + location.search + location.hash.replace(/\?.*$/, '') + (s ? '?' + s : ''));
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
/** After a press: redraw from the new state, put focus on the heading and say so. */
|
|
3700
|
+
function fbDone(text) {
|
|
3701
|
+
render();
|
|
3702
|
+
const h = document.querySelector('#view h1');
|
|
3703
|
+
if (h) { h.tabIndex = -1; h.focus({ preventScroll: true }); }
|
|
3704
|
+
// The region is outside the view, so it exists before the text does.
|
|
3705
|
+
setTimeout(() => { $('fb-live').textContent = text; }, 50);
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
function fbFail(e) {
|
|
3709
|
+
const m = $('fb-msg');
|
|
3710
|
+
if (m) m.textContent = `Couldn't do that: ${e.message}`;
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
function fbAcknowledge(btn) {
|
|
3714
|
+
if (btn.disabled) return;
|
|
3715
|
+
btn.disabled = true;
|
|
3716
|
+
postJson('/api/feedback/acknowledge', { id: +btn.dataset.fbAck })
|
|
3717
|
+
.then((d) => { S.feedback = d.state; fbDone(FB.announce); })
|
|
3718
|
+
.catch((e) => { btn.disabled = false; fbFail(e); });
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
function fbAskDelete(btn) {
|
|
3722
|
+
const id = +btn.dataset.fbDel;
|
|
3723
|
+
$('fb-actions').innerHTML = `<span class="fb__ask" role="group" aria-label="Confirm delete"><span>Delete this feedback for good?</span>
|
|
3724
|
+
<button type="button" class="btn" data-fb-yes="${id}">Delete</button>
|
|
3725
|
+
<button type="button" class="set__btn" data-fb-no="${id}">Cancel</button></span><p class="fb__msg" id="fb-msg" role="alert"></p>`;
|
|
3726
|
+
document.querySelector('[data-fb-no]').focus();
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
function fbDelete(btn) {
|
|
3730
|
+
if (btn.disabled) return;
|
|
3731
|
+
btn.disabled = true;
|
|
3732
|
+
postJson('/api/feedback/delete', { id: +btn.dataset.fbYes })
|
|
3733
|
+
.then((d) => { S.feedback = d.state; fbDone('Deleted.'); })
|
|
3734
|
+
.catch((e) => { btn.disabled = false; fbFail(e); });
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3491
3737
|
function viewSettings(scope) {
|
|
3492
3738
|
fill('settings', settingsUrl(), (d) => drawSettings(d, scope));
|
|
3493
3739
|
return `<div class="stack" id="settings">${loader('Reading your settings…')}</div>`;
|
|
@@ -3595,6 +3841,8 @@ const ICONS = {
|
|
|
3595
3841
|
reuse: '<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>',
|
|
3596
3842
|
health: '<path d="M3 12h4l2-6 3 12 2-6h7"/>',
|
|
3597
3843
|
settings: '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',
|
|
3844
|
+
feedback: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M7 7h10M7 11h6"/>',
|
|
3845
|
+
history: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
|
3598
3846
|
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>',
|
|
3599
3847
|
project: '<path d="M3 7l9-4 9 4-9 4-9-4z"/><path d="M3 12l9 4 9-4M3 17l9 4 9-4"/>',
|
|
3600
3848
|
};
|
|
@@ -3655,6 +3903,15 @@ const WORKFLOWS = {
|
|
|
3655
3903
|
projects: { label: 'Projects', view: () => viewArtifactProjects() },
|
|
3656
3904
|
},
|
|
3657
3905
|
},
|
|
3906
|
+
feedback: {
|
|
3907
|
+
name: 'Feedback',
|
|
3908
|
+
groups: [{ id: 'feedback', label: 'Feedback', items: ['history'] }],
|
|
3909
|
+
pages: {
|
|
3910
|
+
dashboard: { label: 'Dashboard', view: () => viewFeedbackDashboard(), query: ['via'] },
|
|
3911
|
+
history: { label: 'History', view: () => viewFeedbackHistory() },
|
|
3912
|
+
item: { parent: 'history', view: (p) => viewFeedbackItem(p) },
|
|
3913
|
+
},
|
|
3914
|
+
},
|
|
3658
3915
|
settings: {
|
|
3659
3916
|
name: 'Settings',
|
|
3660
3917
|
groups: [
|
|
@@ -3793,6 +4050,8 @@ function navCounts(wf) {
|
|
|
3793
4050
|
};
|
|
3794
4051
|
}
|
|
3795
4052
|
if (wf === 'artifacts') return { projects: new Set(artifactsInScope().map((a) => pid(a.project))).size };
|
|
4053
|
+
// A count of acknowledged items is a tally, not a to-do: the badge is what asks for attention.
|
|
4054
|
+
if (wf === 'feedback') return {};
|
|
3796
4055
|
// No honest count for a settings page: how many keys a file sets is not a to-do.
|
|
3797
4056
|
if (wf === 'settings') return {};
|
|
3798
4057
|
return {
|
|
@@ -3858,6 +4117,19 @@ function renderShell() {
|
|
|
3858
4117
|
$('wf-main').innerHTML = `${svg(ICONS[CUR.wf])}<span>${esc(W.name)}</span>`;
|
|
3859
4118
|
$('wf-main').setAttribute('aria-label', `${W.name} dashboard`);
|
|
3860
4119
|
$('topbar-wf').textContent = W.name;
|
|
4120
|
+
syncBadge();
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
/**
|
|
4124
|
+
* The unread count on the workflow control, on every workflow and not only
|
|
4125
|
+
* Feedback. The labels say it in words (`aria-hidden` badge), and the number is
|
|
4126
|
+
* also visible text, so it survives forced-colours mode.
|
|
4127
|
+
*/
|
|
4128
|
+
const fbUnread = () => (S?.feedback?.notify ? 1 : 0);
|
|
4129
|
+
function syncBadge() {
|
|
4130
|
+
const n = fbUnread();
|
|
4131
|
+
$('wf-badge').hidden = !n;
|
|
4132
|
+
$('wf-caret').setAttribute('aria-label', n ? `Switch workflow, ${n} feedback unread` : 'Switch workflow');
|
|
3861
4133
|
}
|
|
3862
4134
|
|
|
3863
4135
|
/* ---------- the workflow picker ---------- */
|
|
@@ -3884,8 +4156,8 @@ function placePicker(el = menu, at = $('wf')) {
|
|
|
3884
4156
|
|
|
3885
4157
|
function openPicker() {
|
|
3886
4158
|
menu.innerHTML = `<p class="picker__label" aria-hidden="true">Workflow</p>` + Object.entries(WORKFLOWS).map(([key, W]) =>
|
|
3887
|
-
`<button type="button" role="menuitemradio" aria-checked="${key === CUR.wf}" data-wf="${key}" tabindex="-1">
|
|
3888
|
-
${svg(ICONS[key])}<span>${esc(W.name)}</span
|
|
4159
|
+
`<button type="button" role="menuitemradio" aria-checked="${key === CUR.wf}" data-wf="${key}" tabindex="-1"${key === 'feedback' && fbUnread() ? ` aria-label="${esc(W.name)}, ${fbUnread()} unread"` : ''}>
|
|
4160
|
+
${svg(ICONS[key])}<span>${esc(W.name)}</span>${key === 'feedback' && fbUnread() ? `<i class="badge" aria-hidden="true">${fbUnread()}</i>` : ''}
|
|
3889
4161
|
<svg class="check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>
|
|
3890
4162
|
</button>`).join('');
|
|
3891
4163
|
menu.hidden = false;
|
|
@@ -4155,7 +4427,8 @@ addEventListener('resize', () => { if (sOpen) placePicker(smenu, sOpen); });
|
|
|
4155
4427
|
*/
|
|
4156
4428
|
function switchWorkflow(wf) {
|
|
4157
4429
|
closePicker(false);
|
|
4158
|
-
|
|
4430
|
+
// Reached from the unread badge: said so, so the count can tell the three ways in apart.
|
|
4431
|
+
const target = wf === 'feedback' && fbUnread() ? href(wf, 'dashboard', null, { via: 'badge' }) : href(wf, 'dashboard');
|
|
4159
4432
|
closeDrawer(false);
|
|
4160
4433
|
if (location.hash === target) render(); else location.hash = target;
|
|
4161
4434
|
(narrow.matches ? $('menu') : caret).focus();
|
|
@@ -4282,6 +4555,8 @@ function render(keepFocus) {
|
|
|
4282
4555
|
AFTER.length = 0;
|
|
4283
4556
|
LAZY.clear();
|
|
4284
4557
|
HEAT_OFF = 0;
|
|
4558
|
+
// Leaving the Feedback page ends the opening that fbOpened counted.
|
|
4559
|
+
if (!(CUR.wf === 'feedback' && CUR.page === 'dashboard')) FB.counted = false;
|
|
4285
4560
|
// A bubble never survives a navigation.
|
|
4286
4561
|
TIP.hints?.close();
|
|
4287
4562
|
// A screen that still has a correction open re-arms this when it draws its bar.
|
|
@@ -4327,6 +4602,19 @@ function setQuery(changes) {
|
|
|
4327
4602
|
function onActivate(e) {
|
|
4328
4603
|
const copy = e.target.closest('[data-copy]');
|
|
4329
4604
|
if (copy) return copyCommand(copy);
|
|
4605
|
+
if (e.target.closest('[data-fb-tip-dismiss]')) return fbTipDismiss();
|
|
4606
|
+
const ack = e.target.closest('[data-fb-ack]');
|
|
4607
|
+
if (ack) return fbAcknowledge(ack);
|
|
4608
|
+
const ask = e.target.closest('[data-fb-del]');
|
|
4609
|
+
if (ask) return fbAskDelete(ask);
|
|
4610
|
+
const yes = e.target.closest('[data-fb-yes]');
|
|
4611
|
+
if (yes) return fbDelete(yes);
|
|
4612
|
+
const no = e.target.closest('[data-fb-no]');
|
|
4613
|
+
if (no) {
|
|
4614
|
+
const id = +no.dataset.fbNo;
|
|
4615
|
+
$('fb-actions').outerHTML = fbActions(id);
|
|
4616
|
+
return document.querySelector(`[data-fb-del="${id}"]`).focus();
|
|
4617
|
+
}
|
|
4330
4618
|
const arr = e.target.closest('.streak__arr');
|
|
4331
4619
|
if (arr) {
|
|
4332
4620
|
if (arr.disabled) return;
|
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,9 @@ Usage:
|
|
|
46
46
|
e.g. quiz.enabled true|false, quiz.enforced true|false,
|
|
47
47
|
quiz.only_on_changes true|false,
|
|
48
48
|
quiz.panel true|false (experimental side panel),
|
|
49
|
+
feedback.enabled true|false (review one of your prompts per
|
|
50
|
+
session; needs memory and providers.observer),
|
|
51
|
+
providers.observer <model> (a model name, or JSON, or null),
|
|
49
52
|
focus project|concept|learn, cadence as-you-go|end,
|
|
50
53
|
difficulty auto|easy|medium|hard,
|
|
51
54
|
explain_on_wrong true|false, delegate_work true|false
|
|
@@ -60,6 +63,9 @@ Usage:
|
|
|
60
63
|
change the same settings as config set/unset (--no-open just
|
|
61
64
|
prints the URL; --port serves in the foreground on that port)
|
|
62
65
|
eklavya dashboard status|stop Show or stop the background dashboard
|
|
66
|
+
eklavya feedback generate Review one of your prompts from an earlier session against the 4D
|
|
67
|
+
framework (needs feedback.enabled, memory and providers.observer);
|
|
68
|
+
one item at a time, shown on the dashboard's Feedback page
|
|
63
69
|
eklavya artifacts new <title> Start a page under ~/.eklavya/artifacts/<project>/ from the
|
|
64
70
|
Eklavya template and print its path [--description <text>]
|
|
65
71
|
[--kind artifact|explainer] [--concept <slug>]
|
|
@@ -407,6 +413,56 @@ async function telemetryCommand(args) {
|
|
|
407
413
|
`${dim('what is sent: eklavya telemetry show · https://eklavya-run.web.app/docs/usage-analytics/')}\n` +
|
|
408
414
|
`${dim(off ? 'turn on: eklavya telemetry on' : 'turn off: eklavya telemetry off')}\n`);
|
|
409
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* `eklavya feedback generate [--background]`: review one prompt from an earlier
|
|
418
|
+
* session, if nothing is waiting. Exits 0 whatever happens: it runs detached
|
|
419
|
+
* from a session start, and a failure here is only a review that did not
|
|
420
|
+
* happen. `--background` prints nothing.
|
|
421
|
+
*/
|
|
422
|
+
async function feedbackCommand(args) {
|
|
423
|
+
const [sub, ...flags] = args;
|
|
424
|
+
if (sub !== 'generate') {
|
|
425
|
+
process.stderr.write('Usage: eklavya feedback generate [--background]\n');
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
const quiet = flags.includes('--background');
|
|
429
|
+
const say = (text) => {
|
|
430
|
+
if (!quiet)
|
|
431
|
+
process.stdout.write(`${text}\n`);
|
|
432
|
+
};
|
|
433
|
+
const later = "couldn't review right now: it will try again at a later start";
|
|
434
|
+
try {
|
|
435
|
+
const [{ openDb }, { generateFeedback }, { identityFor }, { hostSession }] = await Promise.all([
|
|
436
|
+
import('./db.js'),
|
|
437
|
+
import('./feedback.js'),
|
|
438
|
+
import('./memory/identity.js'),
|
|
439
|
+
import('./session.js'),
|
|
440
|
+
]);
|
|
441
|
+
const db = openDb();
|
|
442
|
+
try {
|
|
443
|
+
const out = await generateFeedback(db, loadConfig().config, {
|
|
444
|
+
project: identityFor({ sessionId: '' }).project,
|
|
445
|
+
currentSession: hostSession(db),
|
|
446
|
+
});
|
|
447
|
+
say({
|
|
448
|
+
off: 'off',
|
|
449
|
+
needs_memory: 'needs memory',
|
|
450
|
+
needs_observer: 'needs providers.observer',
|
|
451
|
+
pending: 'a feedback item is waiting: acknowledge it first',
|
|
452
|
+
nothing: 'nothing to review',
|
|
453
|
+
later,
|
|
454
|
+
failed: "couldn't review that session",
|
|
455
|
+
reviewed: out.status === 'reviewed' ? `reviewed 1 prompt from ${out.date}` : '',
|
|
456
|
+
}[out.status]);
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
db.close();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
say(later);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
410
466
|
/** Never throws: `doctor` is also what someone runs on a half-built database. */
|
|
411
467
|
function safely(fn, fallback) {
|
|
412
468
|
try {
|
|
@@ -1034,7 +1090,7 @@ async function forwardToNewerRuntime() {
|
|
|
1034
1090
|
process.exit(child.status ?? 1);
|
|
1035
1091
|
}
|
|
1036
1092
|
const COUNTED = new Set([
|
|
1037
|
-
'install', 'update', 'export-rules', 'config', 'dashboard', 'memory', 'artifacts', 'doctor', 'db-path', 'telemetry',
|
|
1093
|
+
'install', 'update', 'export-rules', 'config', 'dashboard', 'memory', 'artifacts', 'doctor', 'db-path', 'telemetry', 'feedback',
|
|
1038
1094
|
]);
|
|
1039
1095
|
async function main() {
|
|
1040
1096
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -1109,6 +1165,8 @@ async function main() {
|
|
|
1109
1165
|
return;
|
|
1110
1166
|
case 'telemetry':
|
|
1111
1167
|
return telemetryCommand(rest);
|
|
1168
|
+
case 'feedback':
|
|
1169
|
+
return feedbackCommand(rest);
|
|
1112
1170
|
case undefined:
|
|
1113
1171
|
case '-h':
|
|
1114
1172
|
case '--help':
|