Files
oh-my-opencode/.opencode/skills/work-with-pr-workspace/iteration-1/review.html
T

1327 lines
331 KiB
HTML
Raw Normal View History

2026-03-14 12:45:53 +09:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>work-with-pr eval review</title>
<style>
:root {
color-scheme: dark;
--background: #060816;
--background-accent: #0a1024;
--panel: rgba(15, 21, 44, 0.86);
--panel-strong: rgba(18, 26, 54, 0.96);
--panel-soft: rgba(14, 18, 34, 0.72);
--border: rgba(148, 163, 184, 0.18);
--border-strong: rgba(148, 163, 184, 0.28);
--text: #eef2ff;
--muted: #98a2c3;
--muted-strong: #c7d2fe;
--accent: #7c8cff;
--accent-strong: #96a5ff;
--success: #2bd576;
--danger: #ff5f7c;
--warning: #ffcc66;
--shadow: 0 24px 80px rgba(2, 6, 23, 0.46);
--radius-xl: 26px;
--radius-lg: 20px;
--radius-md: 14px;
--radius-sm: 10px;
--mono: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace;
--sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at top left, rgba(124, 140, 255, 0.16), transparent 34%),
radial-gradient(circle at top right, rgba(45, 212, 191, 0.12), transparent 28%),
linear-gradient(180deg, var(--background-accent) 0%, var(--background) 55%);
color: var(--text);
font-family: var(--sans);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: radial-gradient(circle at center, black, transparent 85%);
opacity: 0.22;
}
.page {
position: relative;
max-width: 1380px;
margin: 0 auto;
padding: 32px 20px 80px;
}
.hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding: 24px 26px;
border: 1px solid var(--border);
border-radius: var(--radius-xl);
background: linear-gradient(180deg, rgba(18, 26, 54, 0.92), rgba(10, 14, 28, 0.82));
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.hero__title {
margin: 0;
font-size: clamp(1.7rem, 3vw, 2.4rem);
letter-spacing: -0.04em;
}
.hero__subtitle {
margin: 10px 0 0;
max-width: 720px;
color: var(--muted);
line-height: 1.6;
}
.hero__meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
.pill {
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(255, 255, 255, 0.04);
color: var(--muted-strong);
font-size: 0.9rem;
white-space: nowrap;
}
.tab-bar {
display: flex;
gap: 10px;
margin: 22px 0 18px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 18px;
background: rgba(10, 14, 28, 0.74);
backdrop-filter: blur(18px);
}
.tab-button {
border: 0;
border-radius: 12px;
padding: 12px 16px;
font: inherit;
font-weight: 600;
color: var(--muted);
background: transparent;
cursor: pointer;
transition: 160ms ease;
}
.tab-button:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.04);
}
.tab-button.is-active {
color: white;
background: linear-gradient(180deg, rgba(124, 140, 255, 0.42), rgba(124, 140, 255, 0.24));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 12px 32px rgba(57, 72, 157, 0.34);
}
.tab-panel {
display: none;
}
.tab-panel.is-active {
display: block;
animation: tab-fade 220ms ease both;
}
@keyframes tab-fade {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.panel-stack {
display: grid;
gap: 18px;
}
.card {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: linear-gradient(180deg, var(--panel) 0%, rgba(9, 13, 26, 0.88) 100%);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
overflow: hidden;
}
.card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 20px 22px 0;
}
.card__title {
margin: 0;
font-size: 1rem;
letter-spacing: -0.02em;
}
.card__body {
padding: 20px 22px 22px;
}
.nav-shell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
}
.nav-title {
display: grid;
gap: 6px;
}
.nav-title__eyebrow {
color: var(--muted);
font-size: 0.86rem;
text-transform: uppercase;
letter-spacing: 0.14em;
}
.nav-title__name {
font-size: 1.18rem;
font-weight: 700;
letter-spacing: -0.03em;
}
.nav-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.button {
border: 1px solid var(--border-strong);
border-radius: 12px;
padding: 11px 14px;
font: inherit;
font-weight: 600;
color: var(--text);
background: rgba(255, 255, 255, 0.04);
cursor: pointer;
transition: 160ms ease;
}
.button:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(124, 140, 255, 0.52);
background: rgba(124, 140, 255, 0.12);
}
.button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.button--primary {
border-color: rgba(124, 140, 255, 0.44);
background: linear-gradient(180deg, rgba(124, 140, 255, 0.34), rgba(91, 104, 198, 0.28));
}
.button--primary:hover:not(:disabled) {
background: linear-gradient(180deg, rgba(124, 140, 255, 0.44), rgba(91, 104, 198, 0.34));
}
.case-grid {
display: grid;
gap: 18px;
}
.prompt-box {
margin: 0;
padding: 18px 18px;
border: 1px solid rgba(148, 163, 184, 0.14);
border-radius: 16px;
background: rgba(9, 13, 24, 0.88);
color: var(--muted-strong);
white-space: pre-wrap;
word-break: break-word;
line-height: 1.65;
font-family: var(--mono);
font-size: 0.93rem;
}
.section-note {
color: var(--muted);
font-size: 0.94rem;
}
details.collapsible {
border-top: 1px solid rgba(148, 163, 184, 0.08);
}
details.collapsible summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 18px 22px;
font-weight: 650;
}
details.collapsible summary::-webkit-details-marker {
display: none;
}
.summary-copy {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.summary-chevron {
color: var(--muted);
transition: transform 160ms ease;
}
details[open] .summary-chevron {
transform: rotate(90deg);
}
.details-body {
padding: 0 22px 22px;
}
.artifact-list {
display: grid;
gap: 14px;
}
.artifact {
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 18px;
overflow: hidden;
background: rgba(8, 11, 20, 0.84);
}
.artifact__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
background: rgba(255, 255, 255, 0.02);
font-size: 0.9rem;
}
.artifact__path {
font-family: var(--mono);
color: var(--muted-strong);
word-break: break-all;
}
.artifact__kind {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 0.72rem;
white-space: nowrap;
}
.artifact__body {
padding: 18px;
}
.rendered-markdown {
color: var(--muted-strong);
line-height: 1.7;
}
.rendered-markdown h1,
.rendered-markdown h2,
.rendered-markdown h3,
.rendered-markdown h4,
.rendered-markdown h5,
.rendered-markdown h6 {
margin: 1.3em 0 0.55em;
letter-spacing: -0.03em;
color: var(--text);
}
.rendered-markdown h1:first-child,
.rendered-markdown h2:first-child,
.rendered-markdown h3:first-child {
margin-top: 0;
}
.rendered-markdown p,
.rendered-markdown ul,
.rendered-markdown ol,
.rendered-markdown blockquote {
margin: 0 0 1em;
}
.rendered-markdown ul,
.rendered-markdown ol {
padding-left: 1.3rem;
}
.rendered-markdown code:not(.code-block__code) {
padding: 0.18em 0.38em;
border-radius: 8px;
background: rgba(124, 140, 255, 0.12);
color: #e8edff;
font-family: var(--mono);
font-size: 0.92em;
}
.rendered-markdown blockquote {
padding: 0.9rem 1rem;
border-left: 3px solid rgba(124, 140, 255, 0.6);
background: rgba(124, 140, 255, 0.08);
border-radius: 0 14px 14px 0;
}
.rendered-markdown hr {
border: 0;
height: 1px;
background: rgba(148, 163, 184, 0.16);
margin: 1.5rem 0;
}
.rendered-markdown a {
color: #9fb2ff;
text-decoration: none;
}
.rendered-markdown a:hover {
text-decoration: underline;
}
.code-block {
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 16px;
overflow: hidden;
background: rgba(3, 6, 17, 0.95);
}
.code-block__meta {
padding: 10px 14px;
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
color: var(--muted);
font-size: 0.76rem;
font-family: var(--mono);
text-transform: uppercase;
letter-spacing: 0.12em;
}
.code-block pre {
margin: 0;
padding: 16px 18px;
overflow-x: auto;
}
.code-block__code {
display: block;
color: #dfe7ff;
font-family: var(--mono);
font-size: 0.9rem;
line-height: 1.7;
white-space: pre;
}
.token-comment { color: #7082b6; }
.token-string { color: #9effd3; }
.token-number { color: #ffcc85; }
.token-keyword { color: #9fb2ff; }
.token-constant { color: #ff8fb1; }
.image-preview {
margin: 0;
display: flex;
justify-content: center;
background: rgba(2, 6, 23, 0.68);
border-radius: 16px;
padding: 14px;
}
.image-preview img {
max-width: 100%;
height: auto;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.14);
}
.binary-preview {
padding: 16px;
border: 1px dashed rgba(148, 163, 184, 0.22);
border-radius: 14px;
color: var(--muted);
line-height: 1.6;
font-family: var(--mono);
}
.timing-chip,
.status-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(255, 255, 255, 0.04);
}
.status-chip--pass {
color: var(--success);
background: rgba(43, 213, 118, 0.08);
border-color: rgba(43, 213, 118, 0.18);
}
.status-chip--fail {
color: var(--danger);
background: rgba(255, 95, 124, 0.08);
border-color: rgba(255, 95, 124, 0.18);
}
.grade-list {
display: grid;
gap: 12px;
}
.grade-item {
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 16px;
padding: 14px 16px;
background: rgba(8, 11, 20, 0.78);
display: grid;
gap: 10px;
}
.grade-item__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.grade-item__text {
color: var(--muted-strong);
line-height: 1.6;
}
.grade-item__evidence {
color: var(--muted);
line-height: 1.6;
}
.feedback-textarea {
width: 100%;
min-height: 170px;
resize: vertical;
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 16px;
background: rgba(5, 8, 18, 0.94);
color: var(--text);
font: inherit;
line-height: 1.7;
padding: 16px 18px;
outline: none;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.feedback-textarea:focus {
border-color: rgba(124, 140, 255, 0.7);
box-shadow: 0 0 0 4px rgba(124, 140, 255, 0.12);
}
.feedback-meta {
margin-top: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--muted);
font-size: 0.9rem;
flex-wrap: wrap;
}
.feedback-previous {
padding: 14px 16px;
border-radius: 16px;
background: rgba(124, 140, 255, 0.08);
border: 1px solid rgba(124, 140, 255, 0.16);
color: var(--muted-strong);
line-height: 1.65;
white-space: pre-wrap;
}
.table-wrap {
overflow-x: auto;
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 18px;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 700px;
background: rgba(6, 10, 20, 0.8);
}
th,
td {
padding: 14px 16px;
border-bottom: 1px solid rgba(148, 163, 184, 0.08);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.12em;
background: rgba(255, 255, 255, 0.03);
}
td {
color: var(--muted-strong);
}
.benchmark-grid {
display: grid;
gap: 18px;
}
.failed-list,
.observations-list {
display: grid;
gap: 12px;
}
.failed-item,
.observations-list li {
padding: 16px 18px;
border: 1px solid rgba(148, 163, 184, 0.12);
border-radius: 16px;
background: rgba(8, 11, 20, 0.8);
line-height: 1.65;
}
.failed-item__meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.86rem;
}
.empty-state {
padding: 28px;
color: var(--muted);
line-height: 1.7;
}
.mono {
font-family: var(--mono);
}
@media (max-width: 860px) {
.hero,
.nav-shell,
.feedback-meta {
flex-direction: column;
align-items: stretch;
}
.hero__meta {
justify-content: flex-start;
}
.nav-actions {
justify-content: space-between;
}
.page {
padding-inline: 14px;
}
.card__header,
.card__body,
.details-body,
details.collapsible summary {
padding-left: 16px;
padding-right: 16px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<div>
<h1 class="hero__title">work-with-pr eval review</h1>
<p class="hero__subtitle">
Review qualitative outputs, formal grades, and benchmark deltas in one standalone file.
Feedback drafts auto-save locally and export as <span class="mono">feedback.json</span>.
</p>
</div>
<div class="hero__meta" id="hero-meta"></div>
</section>
<nav class="tab-bar" aria-label="Eval viewer tabs">
<button class="tab-button is-active" type="button" data-tab="outputs">Outputs</button>
<button class="tab-button" type="button" data-tab="benchmark">Benchmark</button>
</nav>
<section id="outputs-panel" class="tab-panel is-active"></section>
<section id="benchmark-panel" class="tab-panel"></section>
</main>
<script>
const APP_DATA = {"skill_name": "work-with-pr", "workspace_dir": "/Users/yeongyu/local-workspaces/omo/.opencode/skills/work-with-pr-workspace/iteration-1", "generated_at": "2026-03-13T06:51:22.776914+00:00", "has_previous_workspace": false, "evals": [{"eval_name": "happy-path-feature-config-option", "eval_id": 1, "run_id": "eval-1_with_skill", "prompt": "I need to add a `max_background_agents` config option to oh-my-opencode that limits how many background agents can run simultaneously. It should be in the plugin config schema with a default of 5. Add validation and make sure the background manager respects it. Create a PR for this.", "with_skill": {"outputs": [{"relative_path": "code-changes.md", "kind": "markdown", "language": "markdown", "rendered_html": "<div class=\"rendered-markdown\"><h1>Code Changes: <code>max_background_agents<\/code> Config Option<\/h1><h2>1. <code>src/config/schema/background-task.ts<\/code> — Add schema field<\/h2><div class=\"code-block\"><div class=\"code-block__meta\">typescript<\/div><pre><code class=\"code-block__code\" data-language=\"typescript\">import { z } from &quot;zod&quot;\n\nexport const BackgroundTaskConfigSchema = z.object({\n defaultConcurrency: z.number().min(1).optional(),\n providerConcurrency: z.record(z.string(), z.number().min(0)).optional(),\n modelConcurrency: z.record(z.string(), z.number().min(0)).optional(),\n maxDepth: z.number().int().min(1).optional(),\n maxDescendants: z.number().int().min(1).optional(),\n /** Maximum number of background agents that can run simultaneously across all models/providers (default: 5, minimum: 1) */\n maxBackgroundAgents: z.number().int().min(1).optional(),\n /** Stale timeout in milliseconds - interrupt tasks with no activity for this duration (default: 180000 = 3 minutes, minimum: 60000 = 1 minute) */\n staleTimeoutMs: z.number().min(60000).optional(),\n /** Timeout for tasks that never received any progress update, falling back to startedAt (default: 1800000 = 30 minutes, minimum: 60000 = 1 minute) */\n messageStalenessTimeoutMs: z.number().min(60000).optional(),\n syncPollTimeoutMs: z.number().min(60000).optional(),\n})\n\nexport type BackgroundTaskConfig = z.infer&lt;typeof BackgroundTaskConfigSchema&gt;<\/code><\/pre><\/div><p><strong>Rationale:<\/strong> Follows exact same pattern as <code>maxDepth<\/code> and <code>maxDescendants<\/code> — <code>z.number().int().min(1).optional()<\/code>. The field is optional; runtime default of 5 is applied in <code>ConcurrencyManager<\/code>. No barrel export changes needed since <code>src/config/schema.ts<\/code> already does <code>export * from \"./schema/background-task\"<\/code> and the type is inferred.<\/p><hr><h2>2. <code>src/config/schema/background-task.test.ts<\/code> — Add validation tests<\/h2><p>Append after the existing <code>syncPollTimeoutMs<\/code> describe block (before the closing <code>})<\/code>):<\/p><div class=\"code-block\"><div class=\"code-block__meta\">typescript<\/div><pre><code class=\"code-block__code\" data-language=\"typescript\"> describe(&quot;maxBackgroundAgents&quot;, () =&gt; {\n describe(&quot;#given valid maxBackgroundAgents (10)&quot;, () =&gt; {\n test(&quot;#when parsed #then returns correct value&quot;, () =&gt; {\n const result = BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 10 })\n\n expect(result.maxBackgroundAgents).toBe(10)\n })\n })\n\n describe(&quot;#given maxBackgroundAgents of 1 (minimum)&quot;, () =&gt; {\n test(&quot;#when parsed #then returns correct value&quot;, () =&gt; {\n const result = BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 1 })\n\n expect(result.maxBackgroundAgents).toBe(1)\n })\n })\n\n describe(&quot;#given maxBackgroundAgents below minimum (0)&quot;, () =&gt; {\n test(&quot;#when parsed #then throws ZodError&quot;, () =&gt; {\n let thrownError: unknown\n\n try {\n BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 0 })\n } catch (error) {\n thrownError = error\n
const STORAGE_KEY = `eval-review:${APP_DATA.skill_name}:${APP_DATA.workspace_dir}`;
const state = {
activeTab: 'outputs',
currentIndex: 0,
feedbackByRunId: loadFeedbackState(),
};
function loadFeedbackState() {
try {
const rawValue = window.localStorage.getItem(STORAGE_KEY);
return rawValue ? JSON.parse(rawValue) : {};
} catch (_error) {
return {};
}
}
function persistFeedbackState() {
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state.feedbackByRunId));
} catch (_error) {
// Ignore storage failures.
}
}
function ensureFeedbackRecord(runId) {
if (!state.feedbackByRunId[runId]) {
state.feedbackByRunId[runId] = { feedback: '', timestamp: null };
}
return state.feedbackByRunId[runId];
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function trimNumber(value) {
const absoluteValue = Math.abs(value);
const fractionDigits = absoluteValue >= 100 ? 0 : absoluteValue >= 10 ? 1 : 2;
return value.toFixed(fractionDigits).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1');
}
function asFiniteNumber(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function formatSeconds(value) {
const numericValue = asFiniteNumber(value);
return numericValue === null ? '—' : `${trimNumber(numericValue)}s`;
}
function formatDurationDelta(value) {
const numericValue = asFiniteNumber(value);
if (numericValue === null) {
return '—';
}
const prefix = numericValue > 0 ? '+' : '';
return `${prefix}${trimNumber(numericValue)}s`;
}
function normalizeRatio(value) {
if (value === null) {
return null;
}
return Math.abs(value) > 1 ? value / 100 : value;
}
function formatPercent(value) {
const numericValue = asFiniteNumber(value);
if (numericValue === null) {
return '—';
}
const ratioValue = normalizeRatio(numericValue);
return `${(ratioValue * 100).toFixed(1)}%`;
}
function formatPassRateDelta(value) {
const numericValue = asFiniteNumber(value);
if (numericValue === null) {
return '—';
}
const ratioValue = normalizeRatio(numericValue);
const prefix = ratioValue > 0 ? '+' : '';
return `${prefix}${(ratioValue * 100).toFixed(1)} pp`;
}
function formatPassRateWithCounts(rate, passed, total) {
const percentValue = formatPercent(rate);
if (passed === null || total === null) {
return percentValue;
}
return `${percentValue} (${passed}/${total})`;
}
function formatTimestamp(isoString) {
if (!isoString) {
return 'draft not saved yet';
}
const parsedDate = new Date(isoString);
if (Number.isNaN(parsedDate.getTime())) {
return isoString;
}
return parsedDate.toLocaleString();
}
function renderHeroMeta() {
const heroMeta = document.getElementById('hero-meta');
const pills = [
`<span class="pill">skill · ${escapeHtml(APP_DATA.skill_name)}</span>`,
`<span class="pill">evals · ${APP_DATA.evals.length}</span>`,
`<span class="pill">generated · ${escapeHtml(formatTimestamp(APP_DATA.generated_at))}</span>`,
];
if (APP_DATA.benchmark) {
pills.push('<span class="pill">benchmark loaded</span>');
}
if (APP_DATA.has_previous_workspace) {
pills.push('<span class="pill">previous iteration linked</span>');
}
heroMeta.innerHTML = pills.join('');
}
function setActiveTab(tabName) {
state.activeTab = tabName;
document.querySelectorAll('.tab-button').forEach((button) => {
button.classList.toggle('is-active', button.dataset.tab === tabName);
});
document.getElementById('outputs-panel').classList.toggle('is-active', tabName === 'outputs');
document.getElementById('benchmark-panel').classList.toggle('is-active', tabName === 'benchmark');
}
function renderTimingChip(timing) {
if (!timing) {
return '';
}
const durationSeconds = asFiniteNumber(timing.total_duration_seconds)
?? (asFiniteNumber(timing.duration_ms) !== null ? timing.duration_ms / 1000 : null);
if (durationSeconds === null) {
return '';
}
return `<span class="timing-chip">duration · ${formatSeconds(durationSeconds)}</span>`;
}
function renderArtifactList(artifacts, emptyMessage) {
if (!artifacts || artifacts.length === 0) {
return `<div class="empty-state">${escapeHtml(emptyMessage)}</div>`;
}
return `
<div class="artifact-list">
${artifacts.map((artifact) => `
<article class="artifact">
<div class="artifact__header">
<span class="artifact__path">${escapeHtml(artifact.relative_path)}</span>
<span class="artifact__kind">${escapeHtml(artifact.kind)}</span>
</div>
<div class="artifact__body">${artifact.rendered_html}</div>
</article>
`).join('')}
</div>
`;
}
function renderGrades(grades) {
if (!grades || grades.length === 0) {
return '<div class="empty-state">No grading.json found for this eval.</div>';
}
return `
<div class="grade-list">
${grades.map((grade) => {
const isPassed = grade.passed === true;
const statusClass = isPassed ? 'status-chip status-chip--pass' : 'status-chip status-chip--fail';
const statusLabel = isPassed ? 'PASS' : 'FAIL';
return `
<article class="grade-item">
<div class="grade-item__top">
<div class="grade-item__text">${escapeHtml(grade.text)}</div>
<span class="${statusClass}">${statusLabel}</span>
</div>
<div class="grade-item__evidence">${escapeHtml(grade.evidence || 'No evidence recorded.')}</div>
</article>
`;
}).join('')}
</div>
`;
}
function renderSummaryBadge(grades) {
const passedCount = grades.filter((grade) => grade.passed === true).length;
if (!grades.length) {
return '<span class="timing-chip">no grades</span>';
}
return `<span class="timing-chip">${passedCount}/${grades.length} passed</span>`;
}
function currentEvalCase() {
return APP_DATA.evals[state.currentIndex] || null;
}
function updateFeedback(runId, feedbackText) {
state.feedbackByRunId[runId] = {
feedback: feedbackText,
timestamp: new Date().toISOString(),
};
persistFeedbackState();
const stampElement = document.getElementById('feedback-saved-at');
if (stampElement) {
stampElement.textContent = `Auto-saved · ${formatTimestamp(state.feedbackByRunId[runId].timestamp)}`;
}
}
function renderOutputsPanel() {
const panel = document.getElementById('outputs-panel');
if (APP_DATA.evals.length === 0) {
panel.innerHTML = '<div class="card empty-state">No eval directories were found in this workspace.</div>';
return;
}
const evalCase = currentEvalCase();
const feedbackRecord = ensureFeedbackRecord(evalCase.run_id);
const previousSection = APP_DATA.has_previous_workspace
? `
<details class="card collapsible">
<summary>
<span class="summary-copy">
<span>Previous iteration output</span>
</span>
<span class="summary-chevron"></span>
</summary>
<div class="details-body">
${renderArtifactList(
evalCase.previous_iteration_outputs,
'No previous with_skill outputs found for this eval.',
)}
</div>
</details>
`
: '';
panel.innerHTML = `
<div class="panel-stack">
<section class="card">
<div class="nav-shell">
<div class="nav-title">
<span class="nav-title__eyebrow">Outputs · arrow keys enabled</span>
<span class="nav-title__name">${escapeHtml(evalCase.eval_name)}</span>
</div>
<div class="nav-actions">
<span class="pill">case ${state.currentIndex + 1} / ${APP_DATA.evals.length}</span>
<button class="button" type="button" id="previous-eval" ${state.currentIndex === 0 ? 'disabled' : ''}>← Prev</button>
<button class="button" type="button" id="next-eval" ${state.currentIndex === APP_DATA.evals.length - 1 ? 'disabled' : ''}>Next →</button>
</div>
</div>
</section>
<section class="card">
<div class="card__header">
<h2 class="card__title">Prompt</h2>
</div>
<div class="card__body">
<pre class="prompt-box">${escapeHtml(evalCase.prompt || 'No prompt found in eval_metadata.json.')}</pre>
</div>
</section>
<section class="card">
<div class="card__header">
<h2 class="card__title">with_skill output</h2>
${renderTimingChip(evalCase.with_skill.timing)}
</div>
<div class="card__body">
${renderArtifactList(evalCase.with_skill.outputs, 'No files found in with_skill/outputs/.')}
</div>
</section>
<details class="card collapsible">
<summary>
<span class="summary-copy">
<span>without_skill output</span>
${renderTimingChip(evalCase.without_skill.timing)}
</span>
<span class="summary-chevron"></span>
</summary>
<div class="details-body">
${renderArtifactList(evalCase.without_skill.outputs, 'No files found in without_skill/outputs/.')}
</div>
</details>
${previousSection}
<details class="card collapsible">
<summary>
<span class="summary-copy">
<span>Formal Grades</span>
${renderSummaryBadge(evalCase.with_skill.grades)}
</span>
<span class="summary-chevron"></span>
</summary>
<div class="details-body">
${renderGrades(evalCase.with_skill.grades)}
</div>
</details>
<section class="card">
<div class="card__header">
<h2 class="card__title">Feedback</h2>
</div>
<div class="card__body">
<textarea
class="feedback-textarea"
id="feedback-input"
placeholder="What should change in the next iteration?"
>${escapeHtml(feedbackRecord.feedback || '')}</textarea>
<div class="feedback-meta">
<span id="feedback-saved-at">Auto-saved · ${escapeHtml(formatTimestamp(feedbackRecord.timestamp))}</span>
<span class="section-note mono">run_id · ${escapeHtml(evalCase.run_id)}</span>
</div>
</div>
</section>
${evalCase.previous_feedback ? `
<section class="card">
<div class="card__header">
<h2 class="card__title">Previous feedback</h2>
</div>
<div class="card__body">
<div class="feedback-previous">${escapeHtml(evalCase.previous_feedback)}</div>
</div>
</section>
` : ''}
<section class="card">
<div class="card__body">
<button class="button button--primary" type="button" id="submit-reviews">Submit All Reviews</button>
<p class="section-note">Downloads a standalone <span class="mono">feedback.json</span> covering every eval in this workspace.</p>
</div>
</section>
</div>
`;
document.getElementById('previous-eval')?.addEventListener('click', () => {
state.currentIndex = Math.max(0, state.currentIndex - 1);
renderOutputsPanel();
});
document.getElementById('next-eval')?.addEventListener('click', () => {
state.currentIndex = Math.min(APP_DATA.evals.length - 1, state.currentIndex + 1);
renderOutputsPanel();
});
document.getElementById('feedback-input')?.addEventListener('input', (event) => {
updateFeedback(evalCase.run_id, event.target.value);
});
document.getElementById('submit-reviews')?.addEventListener('click', downloadFeedbackFile);
applySyntaxHighlighting(panel);
}
function renderBenchmarkPanel() {
const panel = document.getElementById('benchmark-panel');
if (!APP_DATA.benchmark) {
panel.innerHTML = '<div class="card empty-state">No benchmark.json was provided for this review.</div>';
return;
}
const benchmark = APP_DATA.benchmark;
const summaryTable = benchmark.summary_rows.length
? `
<section class="card">
<div class="card__header">
<h2 class="card__title">Summary stats</h2>
</div>
<div class="card__body">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Metric</th>
<th>with_skill</th>
<th>without_skill</th>
<th>Delta</th>
</tr>
</thead>
<tbody>
${benchmark.summary_rows.map((row) => {
const withSkillValue = row.unit === 'ratio' ? formatPercent(row.with_skill) : formatSeconds(row.with_skill);
const withoutSkillValue = row.unit === 'ratio' ? formatPercent(row.without_skill) : formatSeconds(row.without_skill);
const deltaValue = row.unit === 'ratio' ? formatPassRateDelta(row.delta) : formatDurationDelta(row.delta);
return `
<tr>
<td>${escapeHtml(row.label)}</td>
<td>${withSkillValue}</td>
<td>${withoutSkillValue}</td>
<td>${deltaValue}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
</section>
`
: '';
const breakdownTable = benchmark.eval_rows.length
? `
<section class="card">
<div class="card__header">
<h2 class="card__title">Per-eval breakdown</h2>
</div>
<div class="card__body">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Eval</th>
<th>with_skill pass</th>
<th>without_skill pass</th>
<th>Pass delta</th>
<th>with_skill time</th>
<th>without_skill time</th>
<th>Time delta</th>
</tr>
</thead>
<tbody>
${benchmark.eval_rows.map((row) => `
<tr>
<td>${escapeHtml(row.eval_name)}</td>
<td>${formatPassRateWithCounts(row.with_skill_pass_rate, row.with_skill_passed, row.with_skill_total)}</td>
<td>${formatPassRateWithCounts(row.without_skill_pass_rate, row.without_skill_passed, row.without_skill_total)}</td>
<td>${formatPassRateDelta(row.pass_rate_delta)}</td>
<td>${formatSeconds(row.with_skill_duration_seconds)}</td>
<td>${formatSeconds(row.without_skill_duration_seconds)}</td>
<td>${formatDurationDelta(row.duration_delta_seconds)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</section>
`
: '';
const failedAssertions = benchmark.failed_assertions.length
? `
<section class="card">
<div class="card__header">
<h2 class="card__title">Failed assertions</h2>
</div>
<div class="card__body">
<div class="failed-list">
${benchmark.failed_assertions.map((item) => `
<article class="failed-item">
<div class="failed-item__meta">
<span class="status-chip status-chip--fail">${escapeHtml(item.configuration)}</span>
<span>${escapeHtml(item.eval_name)}</span>
</div>
<strong>${escapeHtml(item.assertion)}</strong>
<div>${escapeHtml(item.reason || 'No reason recorded.')}</div>
</article>
`).join('')}
</div>
</div>
</section>
`
: `
<section class="card">
<div class="card__header">
<h2 class="card__title">Failed assertions</h2>
</div>
<div class="empty-state">No failed assertions were recorded in benchmark.json.</div>
</section>
`;
const analystObservations = benchmark.analyst_observations.length
? `
<section class="card">
<div class="card__header">
<h2 class="card__title">Analyst observations</h2>
</div>
<div class="card__body">
<ul class="observations-list">
${benchmark.analyst_observations.map((observation) => `<li>${escapeHtml(observation)}</li>`).join('')}
</ul>
</div>
</section>
`
: '';
const rawBenchmark = `
<section class="card">
<details class="collapsible">
<summary>
<span class="summary-copy">
<span>Raw benchmark.json</span>
</span>
<span class="summary-chevron"></span>
</summary>
<div class="details-body">${renderArtifactList([
{
relative_path: 'benchmark.json',
kind: 'code',
rendered_html: '<div class="code-block"><div class="code-block__meta">json</div><pre><code class="code-block__code" data-language="json">' + escapeHtml(benchmark.raw_json) + '</code></pre></div>',
},
], '')}</div>
</details>
</section>
`;
panel.innerHTML = `
<div class="benchmark-grid">
${summaryTable}
${breakdownTable}
${failedAssertions}
${analystObservations}
${rawBenchmark}
</div>
`;
applySyntaxHighlighting(panel);
}
function downloadFeedbackFile() {
const reviews = APP_DATA.evals.map((evalCase) => {
const feedbackRecord = ensureFeedbackRecord(evalCase.run_id);
return {
run_id: evalCase.run_id,
feedback: feedbackRecord.feedback || '',
timestamp: feedbackRecord.timestamp || new Date().toISOString(),
};
});
const payload = { reviews, status: 'complete' };
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json;charset=utf-8' });
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = objectUrl;
anchor.download = 'feedback.json';
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(objectUrl);
}
function highlightCode(rawText) {
let highlighted = escapeHtml(rawText);
const placeholders = [];
const stash = (fragment) => {
const token = `@@CODE_TOKEN_${placeholders.length}@@`;
placeholders.push(fragment);
return token;
};
highlighted = highlighted.replace(/\/\*[\s\S]*?\*\//g, (match) => stash(`<span class="token-comment">${match}</span>`));
highlighted = highlighted.replace(/\/\/.*$/gm, (match) => stash(`<span class="token-comment">${match}</span>`));
highlighted = highlighted.replace(/(^|\s)#.*$/gm, (match) => stash(`<span class="token-comment">${match}</span>`));
highlighted = highlighted.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, (match) => stash(`<span class="token-string">${match}</span>`));
highlighted = highlighted.replace(/\b\d+(?:\.\d+)?\b/g, '<span class="token-number">$&</span>');
highlighted = highlighted.replace(/\b(?:true|false|null|None|True|False)\b/g, '<span class="token-constant">$&</span>');
highlighted = highlighted.replace(/\b(?:def|class|return|if|else|elif|for|while|import|from|try|except|finally|with|as|pass|break|continue|yield|lambda|async|await|function|const|let|var|new|switch|case|default|export|extends|interface|type|public|private|protected|package|func|struct|enum|match|use|SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|JOIN|GROUP|ORDER|BY|LIMIT)\b/g, '<span class="token-keyword">$&</span>');
placeholders.forEach((fragment, index) => {
highlighted = highlighted.replace(`@@CODE_TOKEN_${index}@@`, fragment);
});
return highlighted;
}
function applySyntaxHighlighting(rootElement) {
rootElement.querySelectorAll('.code-block__code').forEach((codeElement) => {
const rawText = codeElement.textContent || '';
codeElement.innerHTML = highlightCode(rawText);
});
}
function bindEvents() {
document.querySelectorAll('.tab-button').forEach((button) => {
button.addEventListener('click', () => {
setActiveTab(button.dataset.tab);
});
});
document.addEventListener('keydown', (event) => {
if (state.activeTab !== 'outputs') {
return;
}
const activeElementTag = document.activeElement?.tagName;
if (activeElementTag === 'TEXTAREA' || activeElementTag === 'INPUT') {
return;
}
if (event.key === 'ArrowLeft' && state.currentIndex > 0) {
state.currentIndex -= 1;
renderOutputsPanel();
}
if (event.key === 'ArrowRight' && state.currentIndex < APP_DATA.evals.length - 1) {
state.currentIndex += 1;
renderOutputsPanel();
}
});
}
renderHeroMeta();
bindEvents();
renderOutputsPanel();
renderBenchmarkPanel();
setActiveTab('outputs');
</script>
</body>
</html>