@hacksmith/doraval 0.2.46 → 0.2.48
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 +16 -0
- package/bin/doraval.js +1533 -725
- package/bin/ui/index.html +156 -5
- package/package.json +1 -1
package/bin/ui/index.html
CHANGED
|
@@ -249,6 +249,26 @@
|
|
|
249
249
|
</div>
|
|
250
250
|
</div>
|
|
251
251
|
|
|
252
|
+
<!-- Evals / Skill Learnings -->
|
|
253
|
+
<div class="lg:col-span-5 section">
|
|
254
|
+
<div class="flex items-center justify-between mb-3">
|
|
255
|
+
<div>
|
|
256
|
+
<div class="text-sm font-semibold tracking-tight">Skill adherence learnings</div>
|
|
257
|
+
<div class="text-[10px] text-zinc-500 mt-0.5">From <span class="font-mono">dora eval</span> (respects DORAVAL_HOME) • last 25</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="flex items-center gap-2">
|
|
260
|
+
<input id="eval-search" oninput="filterEvals()"
|
|
261
|
+
class="bg-zinc-950 border border-zinc-800 text-xs rounded-2xl px-3 py-1 placeholder:text-zinc-600 w-40 focus:outline-none focus:border-zinc-600"
|
|
262
|
+
placeholder="filter skill...">
|
|
263
|
+
<div class="text-[10px] text-zinc-500 tabular-nums" id="eval-count"></div>
|
|
264
|
+
<button onclick="loadEvals()"
|
|
265
|
+
class="px-3 py-1 text-xs rounded-xl border border-zinc-800 hover:bg-zinc-900 active:scale-[0.97] transition">Refresh</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<div id="evals" class="space-y-1.5 text-sm max-h-[260px] overflow-auto pr-1 -mr-1"></div>
|
|
269
|
+
<div class="mt-2 text-[10px] text-zinc-500">Click a row for full checklist. Run <span class="font-mono">dora eval</span> from the terminal to generate more.</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
252
272
|
<!-- Footer actions -->
|
|
253
273
|
<div class="mt-8 flex items-center gap-2 text-sm">
|
|
254
274
|
<button onclick="doAction('sync')"
|
|
@@ -261,11 +281,11 @@
|
|
|
261
281
|
</button>
|
|
262
282
|
<button onclick="doAction('open-dir')"
|
|
263
283
|
class="px-4 py-2 rounded-2xl border border-zinc-800 hover:bg-zinc-900 active:scale-[0.975] transition text-sm">
|
|
264
|
-
Open
|
|
284
|
+
Open data dir
|
|
265
285
|
</button>
|
|
266
286
|
|
|
267
287
|
<div class="flex-1"></div>
|
|
268
|
-
<div class="text-xs text-zinc-500">CLI is best for heavy lifting. This is for quick capture & review.</div>
|
|
288
|
+
<div class="text-xs text-zinc-500">CLI is best for heavy lifting. This is for quick capture & review. Data location depends on DORAVAL_HOME.</div>
|
|
269
289
|
</div>
|
|
270
290
|
</div>
|
|
271
291
|
|
|
@@ -300,6 +320,7 @@
|
|
|
300
320
|
const $ = (id) => document.getElementById(id);
|
|
301
321
|
|
|
302
322
|
let allEntriesCache = [];
|
|
323
|
+
let allEvalsCache = [];
|
|
303
324
|
let currentProject = null;
|
|
304
325
|
|
|
305
326
|
function showToast(msg, type = 'info') {
|
|
@@ -553,14 +574,143 @@ async function copyContext() {
|
|
|
553
574
|
}
|
|
554
575
|
|
|
555
576
|
async function refreshAll() {
|
|
556
|
-
await Promise.all([loadEntries(), loadContext(), loadHooks()]);
|
|
577
|
+
await Promise.all([loadEntries(), loadContext(), loadHooks(), loadEvals()]);
|
|
557
578
|
showToast('Refreshed');
|
|
558
579
|
}
|
|
559
580
|
|
|
581
|
+
async function loadEvals() {
|
|
582
|
+
try {
|
|
583
|
+
const data = await fetchJSON('/api/evals');
|
|
584
|
+
allEvalsCache = data.evals || [];
|
|
585
|
+
const container = $('evals');
|
|
586
|
+
container.innerHTML = '';
|
|
587
|
+
|
|
588
|
+
if (allEvalsCache.length === 0) {
|
|
589
|
+
$('eval-count').textContent = '0 evals';
|
|
590
|
+
container.innerHTML = '<div class="text-zinc-500 text-sm py-2">No evals yet. Make sure <span class="font-mono">DORAVAL_HOME</span> matches where you ran <span class="font-mono">dora eval</span>, then refresh.</div>';
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
$('eval-count').textContent = `${allEvalsCache.length} evals`;
|
|
595
|
+
allEvalsCache.forEach(e => renderEval(e, container));
|
|
596
|
+
} catch (e) {
|
|
597
|
+
const container = $('evals');
|
|
598
|
+
container.innerHTML = '<div class="text-red-400/70 text-sm py-2">Failed to load evals (check DORAVAL_HOME and restart ui with --force)</div>';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function renderEval(e, container) {
|
|
603
|
+
const div = document.createElement('div');
|
|
604
|
+
div.className = `entry border-l-[3px] bg-zinc-900/50 hover:bg-zinc-900/80 rounded-r-2xl px-3 py-2 cursor-pointer flex items-start gap-3 ${e.verdict === 'PASS' ? 'border-emerald-400' : e.verdict === 'FAIL' ? 'border-red-400' : 'border-yellow-400'}`;
|
|
605
|
+
div.onclick = () => showEvalModal(e);
|
|
606
|
+
|
|
607
|
+
const date = (e.timestamp || '').slice(0, 10);
|
|
608
|
+
const skillShort = (e.skill || '').slice(0, 28);
|
|
609
|
+
const fam = typeof e.userFamiliarity === 'number' ? `${e.userFamiliarity}/10` : '';
|
|
610
|
+
const passed = e.checklist ? e.checklist.filter((c) => c.pass).length : 0;
|
|
611
|
+
const total = e.checklist ? e.checklist.length : 0;
|
|
612
|
+
const score = total > 0 ? `${passed}/${total}` : '';
|
|
613
|
+
|
|
614
|
+
const verdictColor = e.verdict === 'PASS' ? 'text-emerald-400' : e.verdict === 'FAIL' ? 'text-red-400' : 'text-yellow-400';
|
|
615
|
+
|
|
616
|
+
div.innerHTML = `
|
|
617
|
+
<div class="w-14 shrink-0 pt-0.5">
|
|
618
|
+
<div class="${verdictColor} font-semibold text-xs tracking-wider">${e.verdict || 'UNKNOWN'}</div>
|
|
619
|
+
<div class="text-[10px] text-zinc-500 mt-0.5 tabular-nums">${date}</div>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="min-w-0 flex-1">
|
|
622
|
+
<div class="font-medium leading-tight flex items-baseline gap-2">
|
|
623
|
+
<span>${skillShort}</span>
|
|
624
|
+
${score ? `<span class="text-[10px] font-mono text-zinc-400">${score}</span>` : ''}
|
|
625
|
+
</div>
|
|
626
|
+
<div class="text-xs text-zinc-400 mt-0.5 flex gap-x-2 flex-wrap">
|
|
627
|
+
<span>familiarity ${fam || '—'}</span>
|
|
628
|
+
<span class="text-zinc-700">·</span>
|
|
629
|
+
<span>${e.closure || ''}</span>
|
|
630
|
+
${e.sessionTitle ? `<span class="text-zinc-700">·</span> <span class="truncate max-w-[220px]">${e.sessionTitle}</span>` : ''}
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
`;
|
|
634
|
+
container.appendChild(div);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function filterEvals() {
|
|
638
|
+
const q = ($('eval-search')?.value || '').toLowerCase().trim();
|
|
639
|
+
const container = $('evals');
|
|
640
|
+
container.innerHTML = '';
|
|
641
|
+
|
|
642
|
+
let filtered = allEvalsCache;
|
|
643
|
+
if (q) {
|
|
644
|
+
filtered = allEvalsCache.filter((e) =>
|
|
645
|
+
(e.skill || '').toLowerCase().includes(q) ||
|
|
646
|
+
(e.sessionTitle || '').toLowerCase().includes(q) ||
|
|
647
|
+
(e.verdict || '').toLowerCase().includes(q)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
$('eval-count').textContent = `${filtered.length} / ${allEvalsCache.length} evals`;
|
|
652
|
+
|
|
653
|
+
if (filtered.length === 0) {
|
|
654
|
+
container.innerHTML = `<div class="text-zinc-500 text-sm py-2">No matches.</div>`;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
filtered.forEach(e => renderEval(e, container));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function showEvalModal(e) {
|
|
661
|
+
const modal = $('modal');
|
|
662
|
+
modal.classList.remove('hidden');
|
|
663
|
+
modal.classList.add('flex');
|
|
664
|
+
|
|
665
|
+
// Repurpose modal for eval
|
|
666
|
+
$('modal-pb').className = `inline-flex items-center px-3 py-px rounded-full text-[10px] font-semibold tracking-[0.5px] ${e.verdict === 'PASS' ? 'bg-emerald-500 text-black' : e.verdict === 'FAIL' ? 'bg-red-500 text-white' : 'bg-yellow-400 text-black'}`;
|
|
667
|
+
$('modal-pb').textContent = `${e.verdict || 'UNKNOWN'} • ${e.skill || ''}`;
|
|
668
|
+
|
|
669
|
+
$('modal-title').textContent = e.sessionTitle || e.skill || 'Session eval';
|
|
670
|
+
|
|
671
|
+
const metaParts = [];
|
|
672
|
+
if (e.timestamp) metaParts.push(e.timestamp.slice(0,16).replace('T',' '));
|
|
673
|
+
if (typeof e.userFamiliarity === 'number') metaParts.push(`familiarity ${e.userFamiliarity}/10`);
|
|
674
|
+
if (e.closure) metaParts.push(e.closure);
|
|
675
|
+
if (e.agent) metaParts.push(e.agent);
|
|
676
|
+
$('modal-meta').innerHTML = metaParts.join(' · ');
|
|
677
|
+
|
|
678
|
+
const tagsStr = e.model ? `model: ${e.model}` : '';
|
|
679
|
+
$('modal-tags').innerHTML = tagsStr;
|
|
680
|
+
|
|
681
|
+
// Build rich rationale area with checklist
|
|
682
|
+
const checklistHtml = (e.checklist || []).map((item) => {
|
|
683
|
+
const sym = item.pass ? '<span class="text-emerald-400">✓</span>' : '<span class="text-red-400">✗</span>';
|
|
684
|
+
const detail = item.detail ? ` <span class="text-zinc-500 text-xs">— ${item.detail}</span>` : '';
|
|
685
|
+
return `<div class="py-0.5">${sym} ${item.instruction}${detail}</div>`;
|
|
686
|
+
}).join('');
|
|
687
|
+
|
|
688
|
+
const reason = e.verdictReason ? `<div class="mt-3 text-xs text-zinc-400">Reason: ${e.verdictReason}</div>` : '';
|
|
689
|
+
|
|
690
|
+
$('modal-rationale').innerHTML = `
|
|
691
|
+
<div class="text-sm">
|
|
692
|
+
<div class="font-medium mb-1">Adherence checklist</div>
|
|
693
|
+
<div class="space-y-0.5 text-[13px]">${checklistHtml || 'No checklist items.'}</div>
|
|
694
|
+
${reason}
|
|
695
|
+
<div class="mt-3 text-[11px] text-zinc-500">Result: ${e.checklist ? e.checklist.filter((c)=>c.pass).length : 0}/${(e.checklist||[]).length} — ${e.verdict}</div>
|
|
696
|
+
</div>
|
|
697
|
+
`;
|
|
698
|
+
|
|
699
|
+
const actions = $('modal-actions');
|
|
700
|
+
actions.innerHTML = '';
|
|
701
|
+
|
|
702
|
+
const closeBtn = document.createElement('button');
|
|
703
|
+
closeBtn.className = 'ml-auto px-5 py-1.5 text-sm rounded-2xl border border-zinc-700 hover:bg-zinc-800 active:scale-[0.975] transition';
|
|
704
|
+
closeBtn.textContent = 'Close';
|
|
705
|
+
closeBtn.onclick = closeModal;
|
|
706
|
+
actions.appendChild(closeBtn);
|
|
707
|
+
}
|
|
708
|
+
|
|
560
709
|
async function doAction(name) {
|
|
561
710
|
if (name === 'open-dir') {
|
|
562
711
|
const s = await fetchJSON('/api/status');
|
|
563
|
-
|
|
712
|
+
const root = s.doravalRoot || s.doravalDir || '~/.doraval';
|
|
713
|
+
showToast('Data directory: ' + root);
|
|
564
714
|
// optionally try to open, but browser security limits it
|
|
565
715
|
return;
|
|
566
716
|
}
|
|
@@ -697,7 +847,7 @@ async function boot() {
|
|
|
697
847
|
setupCaptureKeys();
|
|
698
848
|
|
|
699
849
|
await loadStatus();
|
|
700
|
-
await Promise.all([loadContext(), loadEntries(), loadHooks()]);
|
|
850
|
+
await Promise.all([loadContext(), loadEntries(), loadHooks(), loadEvals()]);
|
|
701
851
|
|
|
702
852
|
const search = $('search');
|
|
703
853
|
if (search) {
|
|
@@ -714,6 +864,7 @@ async function boot() {
|
|
|
714
864
|
if (!document.hidden) {
|
|
715
865
|
loadEntries().catch(() => {});
|
|
716
866
|
loadContext().catch(() => {});
|
|
867
|
+
loadEvals().catch(() => {});
|
|
717
868
|
}
|
|
718
869
|
}, 9000);
|
|
719
870
|
}
|