1327 lines
331 KiB
HTML
1327 lines
331 KiB
HTML
|
|
<!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 "zod"\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<typeof BackgroundTaskConfigSchema><\/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("maxBackgroundAgents", () => {\n describe("#given valid maxBackgroundAgents (10)", () => {\n test("#when parsed #then returns correct value", () => {\n const result = BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 10 })\n\n expect(result.maxBackgroundAgents).toBe(10)\n })\n })\n\n describe("#given maxBackgroundAgents of 1 (minimum)", () => {\n test("#when parsed #then returns correct value", () => {\n const result = BackgroundTaskConfigSchema.parse({ maxBackgroundAgents: 1 })\n\n expect(result.maxBackgroundAgents).toBe(1)\n })\n })\n\n describe("#given maxBackgroundAgents below minimum (0)", () => {\n test("#when parsed #then throws ZodError", () => {\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('&', '&')
|
|||
|
|
.replaceAll('<', '<')
|
|||
|
|
.replaceAll('>', '>')
|
|||
|
|
.replaceAll('"', '"')
|
|||
|
|
.replaceAll("'", ''');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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>
|