项目文件夹

文件
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

5563 行
205 KiB
HTML

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Run Explorer - 轨迹数据可视化</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #f8fafc;
--bg-card: #ffffff;
--bg-hover: #f1f5f9;
--bg-active: #e0f2fe;
--border: #e2e8f0;
--border-active: #0ea5e9;
--text: #0f172a;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--primary: #0ea5e9;
--primary-light: #e0f2fe;
--primary-dark: #0284c7;
--success: #10b981;
--success-light: #d1fae5;
--error: #ef4444;
--error-light: #fee2e2;
--warning: #f59e0b;
--warning-light: #fef3c7;
--purple: #8b5cf6;
--purple-light: #ede9fe;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
--radius: 12px;
--radius-sm: 8px;
--font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
body {
font-family: var(--font);
background: linear-gradient(135deg, #f0f9ff 0%, #f8fafc 50%, #faf5ff 100%);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
/* ========== Layout ========== */
.app {
display: grid;
grid-template-rows: auto 1fr;
height: 100vh;
overflow: hidden;
}
/* ========== Header ========== */
header {
background: var(--bg-card);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
box-shadow: var(--shadow-sm);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--primary) 0%, var(--purple) 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
.logo h1 {
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, var(--primary) 0%, var(--purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo p {
font-size: 12px;
color: var(--text-muted);
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.btn {
padding: 10px 18px;
border: none;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
box-shadow: 0 2px 4px rgba(14, 165, 233, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(14, 165, 233, 0.4);
}
.btn-secondary {
background: var(--bg-card);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--bg-hover);
border-color: var(--primary);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* ========== Main Content ========== */
main {
display: grid;
grid-template-columns: 380px 1fr;
min-height: 0;
overflow: hidden;
}
/* ========== Sidebar ========== */
.sidebar {
background: var(--bg-card);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h2 {
font-size: 14px;
font-weight: 700;
color: var(--text);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-header h2::before {
content: '';
width: 4px;
height: 16px;
background: linear-gradient(180deg, var(--primary) 0%, var(--purple) 100%);
border-radius: 2px;
}
.run-select-wrap {
position: relative;
}
.run-select-wrap select {
width: 100%;
padding: 12px 16px;
padding-right: 40px;
background: var(--bg);
border: 2px solid var(--border);
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 500;
color: var(--text);
appearance: none;
cursor: pointer;
transition: all 0.2s;
}
.run-select-wrap select:hover {
border-color: var(--primary);
}
.run-select-wrap select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.run-select-wrap::after {
content: '▼';
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
color: var(--text-muted);
pointer-events: none;
}
/* Filters */
.filters {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.filter-group {
margin-bottom: 12px;
}
.filter-group-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.filter-selects {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.filter-selects.single {
grid-template-columns: 1fr;
}
.filter-select {
width: 100%;
padding: 7px 28px 7px 10px;
background: var(--bg-card);
border: 1.5px solid var(--border);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: var(--text);
appearance: none;
cursor: pointer;
transition: all 0.2s;
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
}
.filter-select:hover {
border-color: var(--primary);
}
.filter-select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--primary-light);
}
.filter-select.active-filter {
border-color: var(--primary);
background-color: var(--primary-light);
}
.active-filters-bar {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.active-filter-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: var(--primary-light);
color: var(--primary-dark);
border-radius: 12px;
font-size: 11px;
font-weight: 600;
border: 1px solid var(--primary);
}
.active-filter-tag .remove-filter {
cursor: pointer;
font-size: 14px;
line-height: 1;
opacity: 0.6;
transition: opacity 0.15s;
}
.active-filter-tag .remove-filter:hover {
opacity: 1;
}
.filter-reset-btn {
padding: 3px 8px;
background: var(--bg-card);
color: var(--text-muted);
border: 1px solid var(--border);
border-radius: 12px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.filter-reset-btn:hover {
background: var(--error-light);
color: var(--error);
border-color: var(--error);
}
.search-box {
position: relative;
margin-bottom: 12px;
}
.search-box input {
width: 100%;
padding: 10px 14px 10px 40px;
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: var(--radius-sm);
font-size: 14px;
transition: all 0.2s;
}
.search-box input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.search-box::before {
content: '🔍';
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
}
.filter-chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.chip input {
display: none;
}
.chip.success {
background: var(--success-light);
color: var(--success);
}
.chip.success.active {
border-color: var(--success);
}
.chip.failed {
background: var(--error-light);
color: var(--error);
}
.chip.failed.active {
border-color: var(--error);
}
.chip.error {
background: var(--warning-light);
color: var(--warning);
}
.chip.error.active {
border-color: var(--warning);
}
.chip.sideEffect {
background: #fff7ed;
color: #f97316;
}
.chip.sideEffect.active {
border-color: #f97316;
}
.chip.overdue {
background: #ede9fe;
color: #7c3aed;
}
.chip.overdue.active {
border-color: #7c3aed;
}
.chip.falseComplete {
background: #fce7f3;
color: #be185d;
}
.chip.falseComplete.active {
border-color: #be185d;
}
/* Task List */
.task-list {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.task-item {
padding: 14px 16px;
background: var(--bg);
border: 2px solid transparent;
border-radius: var(--radius);
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.task-item:hover {
background: var(--bg-hover);
border-color: var(--border);
transform: translateX(4px);
}
.task-item.active {
background: var(--primary-light);
border-color: var(--primary);
}
.task-header {
display: flex;
align-items: center;
gap: 10px;
}
.status-badge {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-badge.success { background: var(--success); box-shadow: 0 0 8px var(--success); }
.status-badge.failed { background: var(--error); box-shadow: 0 0 8px var(--error); }
.status-badge.error { background: var(--warning); box-shadow: 0 0 8px var(--warning); }
.status-badge.unknown { background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%); box-shadow: 0 0 8px #9ca3af; }
.task-title {
font-size: 14px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.task-meta {
margin-top: 6px;
font-size: 12px;
color: var(--text-muted);
font-family: var(--mono);
display: flex;
gap: 12px;
}
.task-count {
padding: 2px 0;
text-align: center;
font-size: 12px;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: 8px;
padding-top: 12px;
}
/* ========== Content Area ========== */
.content {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
padding: 16px 24px;
background: var(--bg-card);
border-bottom: 1px solid var(--border);
}
.tab {
padding: 10px 20px;
background: none;
border: none;
border-radius: var(--radius-sm);
font-size: 14px;
font-weight: 600;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.tab.active {
color: var(--primary);
background: var(--primary-light);
}
.tab.active::after {
content: '';
position: absolute;
bottom: -16px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 3px;
background: var(--primary);
border-radius: 3px;
}
/* Tab Content */
.tab-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.panel {
display: none;
animation: fadeIn 0.3s ease;
}
.panel.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ========== Cards ========== */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.card-title {
font-size: 16px;
font-weight: 700;
color: var(--text);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.card-title .icon {
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.card-title .icon.blue { background: var(--primary-light); }
.card-title .icon.green { background: var(--success-light); }
.card-title .icon.purple { background: var(--purple-light); }
/* Stats Grid */
.stats-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 24px;
}
.stats-grid .stat-card {
flex: 1 1 auto;
min-width: 120px;
}
.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
text-align: center;
box-shadow: var(--shadow-sm);
transition: all 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.stat-card.highlight {
background: linear-gradient(135deg, var(--primary) 0%, var(--purple) 100%);
color: white;
}
.stat-card.highlight .stat-label {
color: rgba(255,255,255,0.8);
}
.stat-value {
font-size: 36px;
font-weight: 800;
margin-bottom: 4px;
}
.stat-value.success { color: var(--success); }
.stat-value.error { color: var(--error); }
.stat-label {
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.info-item {
padding: 12px 16px;
background: var(--bg);
border-radius: var(--radius-sm);
}
.info-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.info-value {
font-size: 14px;
font-weight: 500;
color: var(--text);
font-family: var(--mono);
word-break: break-all;
}
/* Judge Section */
.judge-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.judge-stat {
padding: 16px;
background: var(--bg);
border-radius: var(--radius-sm);
text-align: center;
}
.judge-stat-value {
font-size: 18px;
font-weight: 700;
}
.judge-stat-value.pass { color: var(--success); }
.judge-stat-value.fail { color: var(--error); }
.judge-stat-label {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
}
.judge-item {
padding: 16px;
background: var(--bg);
border-radius: var(--radius-sm);
margin-bottom: 10px;
border-left: 4px solid var(--border);
}
.judge-item.passed { border-left-color: var(--success); background: var(--success-light); }
.judge-item.failed { border-left-color: var(--error); background: var(--error-light); }
.judge-field {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
color: var(--text);
margin-bottom: 8px;
}
.judge-values {
display: flex;
gap: 24px;
flex-wrap: wrap;
font-size: 12px;
font-family: var(--mono);
}
.judge-values span {
color: var(--text-muted);
}
/* Warnings */
.warnings-card {
background: var(--warning-light);
border-color: var(--warning);
}
.warning-item {
font-size: 12px;
font-family: var(--mono);
color: #92400e;
padding: 8px 12px;
background: rgba(255,255,255,0.5);
border-radius: var(--radius-sm);
margin-bottom: 6px;
}
/* Collapsible */
details {
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 16px;
overflow: hidden;
}
details summary {
padding: 14px 18px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
background: var(--bg-card);
user-select: none;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.2s;
}
details summary:hover {
background: var(--bg-hover);
}
details summary::before {
content: '▶';
font-size: 10px;
color: var(--text-muted);
transition: transform 0.2s;
}
details[open] summary::before {
transform: rotate(90deg);
}
details .content {
padding: 16px;
background: var(--bg);
}
.code {
font-family: var(--mono);
font-size: 12px;
line-height: 1.6;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 16px;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow: auto;
}
/* ========== Trajectory ========== */
.trajectory-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.trajectory-header h3 {
font-size: 18px;
font-weight: 700;
}
.trajectory-nav {
display: flex;
gap: 8px;
}
.nav-btn {
width: 36px;
height: 36px;
border: 1px solid var(--border);
background: var(--bg-card);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.nav-btn:hover {
background: var(--primary-light);
border-color: var(--primary);
}
.trajectory-strip {
display: flex;
gap: 12px;
overflow-x: auto;
padding: 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 24px;
}
.thumb {
flex-shrink: 0;
width: 90px;
border-radius: var(--radius-sm);
overflow: hidden;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.2s;
position: relative;
box-shadow: var(--shadow-sm);
}
.thumb:hover {
transform: scale(1.05);
border-color: var(--purple);
}
.thumb.active {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.thumb img {
display: block;
width: 90px;
height: 160px;
object-fit: cover;
}
.thumb-label {
position: absolute;
bottom: 6px;
left: 6px;
background: rgba(0,0,0,0.75);
color: white;
font-size: 10px;
font-weight: 600;
padding: 3px 8px;
border-radius: 4px;
font-family: var(--mono);
}
.step-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.step-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: linear-gradient(135deg, var(--primary-light) 0%, var(--purple-light) 100%);
border-bottom: 1px solid var(--border);
}
.step-number {
font-size: 16px;
font-weight: 700;
color: var(--primary-dark);
}
.step-action {
font-size: 12px;
font-weight: 600;
color: var(--purple);
background: white;
padding: 6px 12px;
border-radius: 20px;
font-family: var(--mono);
}
.step-body {
padding: 20px;
display: grid;
grid-template-columns: auto 1fr;
gap: 24px;
}
.step-images {
display: flex;
gap: 12px;
}
.step-img-wrap {
position: relative;
}
.step-img {
width: 220px;
border-radius: var(--radius);
cursor: pointer;
transition: all 0.2s;
border: 2px solid var(--border);
}
.step-img:hover {
transform: scale(1.02);
border-color: var(--primary);
box-shadow: var(--shadow-lg);
}
.img-label {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.75);
color: white;
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
border-radius: 6px;
}
.step-text {
min-width: 0;
}
.step-thought {
font-size: 14px;
line-height: 1.7;
background: var(--bg);
padding: 16px;
border-radius: var(--radius-sm);
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border);
}
.step-route {
font-size: 12px;
font-family: var(--mono);
color: var(--purple);
}
.step-meta-info {
margin-top: 14px;
padding: 12px 16px;
background: var(--purple-light);
border-radius: var(--radius-sm);
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.step-action-data {
font-size: 12px;
font-family: var(--mono);
color: var(--primary-dark);
}
/* Response Section */
.response-section {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.response-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: linear-gradient(135deg, var(--primary-light) 0%, var(--purple-light) 100%);
border-bottom: 1px solid var(--border);
}
.response-title {
font-size: 13px;
font-weight: 600;
color: var(--primary-dark);
}
.response-file {
font-size: 11px;
font-family: var(--mono);
color: var(--text-muted);
background: white;
padding: 3px 8px;
border-radius: 4px;
}
.response-content {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.response-pre {
font-family: var(--mono);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
color: var(--text);
}
.loading-text {
color: var(--text-muted);
font-size: 13px;
}
.no-response {
color: var(--text-muted);
font-size: 13px;
font-style: italic;
}
/* Prompt/Chat Section */
.prompt-section {
margin-top: 16px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.prompt-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
}
.prompt-header:hover {
background: linear-gradient(135deg, #fde68a 0%, #fcd34d 100%);
}
.prompt-title {
font-size: 13px;
font-weight: 600;
color: #92400e;
display: flex;
align-items: center;
gap: 8px;
}
.prompt-toggle {
font-size: 10px;
color: #92400e;
transition: transform 0.2s;
}
.prompt-section.expanded .prompt-toggle {
transform: rotate(90deg);
}
.prompt-content {
display: none;
max-height: 600px;
overflow-y: auto;
}
.prompt-section.expanded .prompt-content {
display: block;
}
.chat-messages {
padding: 16px;
}
.chat-message {
margin-bottom: 16px;
padding: 14px 16px;
border-radius: var(--radius-sm);
position: relative;
}
.chat-message:last-child {
margin-bottom: 0;
}
.chat-message.system {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-left: 4px solid var(--primary);
}
.chat-message.user {
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border-left: 4px solid var(--success);
}
.chat-message.assistant {
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
border-left: 4px solid var(--purple);
}
.chat-role {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 6px;
}
.chat-message.system .chat-role { color: var(--primary-dark); }
.chat-message.user .chat-role { color: var(--success); }
.chat-message.assistant .chat-role { color: var(--purple); }
.chat-text {
font-size: 13px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
color: var(--text);
}
/* Collapsible system message */
.chat-message.system .chat-text {
max-height: 150px;
overflow: hidden;
position: relative;
}
.chat-message.system .chat-text.expanded {
max-height: none;
}
.chat-message.system .chat-text:not(.expanded)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(transparent, #e0f2fe);
}
.expand-btn {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 8px;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
color: var(--primary-dark);
background: rgba(14, 165, 233, 0.1);
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.expand-btn:hover {
background: rgba(14, 165, 233, 0.2);
}
.chat-image-placeholder {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(0,0,0,0.05);
border-radius: 6px;
font-size: 12px;
color: var(--text-secondary);
margin: 4px 0;
}
.chat-content-item {
margin-bottom: 8px;
}
.chat-content-item:last-child {
margin-bottom: 0;
}
/* Full History Button */
.full-history-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 1px solid #f59e0b;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 600;
color: #92400e;
cursor: pointer;
transition: all 0.2s;
margin-left: 10px;
}
.full-history-btn:hover {
background: linear-gradient(135deg, #fde68a 0%, #fcd34d 100%);
transform: translateY(-1px);
}
/* Prompt Modal */
.prompt-modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 9998;
backdrop-filter: blur(5px);
}
.prompt-modal.open {
display: flex;
}
.prompt-modal-content {
background: var(--bg-card);
border-radius: var(--radius);
width: 90%;
max-width: 900px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
}
.prompt-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-bottom: 1px solid var(--border);
}
.prompt-modal-title {
font-size: 16px;
font-weight: 700;
color: #92400e;
}
.prompt-modal-close {
width: 32px;
height: 32px;
border: none;
background: rgba(0,0,0,0.1);
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.prompt-modal-close:hover {
background: rgba(0,0,0,0.2);
}
.prompt-modal-body {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.prompt-stats {
display: flex;
gap: 16px;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--bg);
border-radius: var(--radius-sm);
}
.prompt-stat {
font-size: 12px;
color: var(--text-secondary);
}
.prompt-stat strong {
color: var(--text);
font-weight: 600;
}
/* Return Value */
.return-value {
margin-top: 14px;
padding: 14px 16px;
background: var(--success-light);
border: 1px solid var(--success);
border-radius: var(--radius-sm);
}
.return-label {
font-size: 11px;
font-weight: 600;
color: var(--success);
text-transform: uppercase;
margin-bottom: 6px;
}
.return-content {
font-size: 13px;
line-height: 1.6;
color: var(--text);
white-space: pre-wrap;
}
/* ========== All Images Grid ========== */
.images-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.images-header h3 {
font-size: 18px;
font-weight: 700;
}
.images-count {
font-size: 14px;
color: var(--text-muted);
background: var(--bg-card);
padding: 6px 14px;
border-radius: 20px;
border: 1px solid var(--border);
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}
.image-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
box-shadow: var(--shadow-sm);
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary);
}
.image-card img {
width: 100%;
display: block;
}
.image-card-info {
padding: 12px 14px;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
border-top: 1px solid var(--border);
background: var(--bg);
}
/* ========== Lightbox ========== */
.lightbox {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.9);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
backdrop-filter: blur(10px);
}
.lightbox.open {
display: flex;
}
.lightbox img {
max-width: 92%;
max-height: 92%;
border-radius: var(--radius);
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
}
.lightbox-close {
position: absolute;
top: 20px;
right: 20px;
width: 48px;
height: 48px;
background: rgba(255,255,255,0.1);
border: none;
border-radius: 50%;
color: white;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.lightbox-close:hover {
background: rgba(255,255,255,0.2);
transform: scale(1.1);
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 56px;
height: 56px;
background: rgba(255,255,255,0.1);
border: none;
border-radius: 50%;
color: white;
font-size: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.lightbox-nav:hover {
background: rgba(255,255,255,0.2);
transform: translateY(-50%) scale(1.1);
}
.lightbox-prev { left: 24px; }
.lightbox-next { right: 24px; }
.lightbox-info {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 10px 24px;
border-radius: 30px;
font-size: 14px;
font-weight: 500;
}
/* ========== Empty State ========== */
.empty {
text-align: center;
padding: 60px 30px;
}
.empty-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
background: var(--bg);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
}
.empty h3 {
font-size: 18px;
color: var(--text);
margin-bottom: 8px;
}
.empty p {
font-size: 14px;
color: var(--text-muted);
}
/* ========== Badge ========== */
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
}
.badge.success {
background: var(--success-light);
color: var(--success);
}
.badge.failed {
background: var(--error-light);
color: var(--error);
}
/* ========== Pass@K Stats ========== */
.pass-k-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.pass-k-item {
text-align: center;
padding: 16px 12px;
background: linear-gradient(135deg, var(--primary-light) 0%, var(--purple-light) 100%);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.pass-k-label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 4px;
}
.pass-k-value {
font-size: 24px;
font-weight: 800;
color: var(--primary-dark);
}
/* ========== Per Task Pass@K Table ========== */
.task-pass-k-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.task-pass-k-table th,
.task-pass-k-table td {
padding: 12px 14px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.task-pass-k-table th {
background: var(--bg);
font-weight: 600;
color: var(--text-secondary);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
}
.task-pass-k-table tbody tr:hover {
background: var(--bg-hover);
}
.task-pass-k-table .task-name-cell {
font-weight: 600;
font-family: var(--mono);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pass-k-cell {
text-align: center;
font-family: var(--mono);
}
.pass-k-cell.full {
color: var(--success);
font-weight: 700;
}
.pass-k-cell.zero {
color: var(--error);
}
.pass-k-cell.partial {
color: var(--warning);
}
.success-bar {
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
}
.success-bar-track {
width: 60px;
min-width: 60px;
height: 8px;
background: var(--bg);
border-radius: 4px;
overflow: hidden;
}
.success-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--success) 0%, #059669 100%);
border-radius: 4px;
transition: width 0.3s ease;
}
.success-bar-label {
font-size: 13px;
font-weight: 600;
color: var(--text);
min-width: 55px;
}
/* ========== Distribution Charts ========== */
.distribution-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.bar-chart {
margin-top: 12px;
}
.bar-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.bar-label {
min-width: 100px;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
font-family: var(--mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bar-track {
flex: 1;
height: 24px;
background: var(--bg);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
font-size: 11px;
font-weight: 600;
color: white;
min-width: fit-content;
}
.bar-fill.primary {
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-dark) 100%);
}
.bar-fill.success {
background: linear-gradient(90deg, var(--success) 0%, #059669 100%);
}
.bar-fill.purple {
background: linear-gradient(90deg, var(--warning) 0%, #d97706 100%);
}
.bar-fill.warning {
background: linear-gradient(90deg, var(--warning) 0%, #d97706 100%);
}
.bar-value {
font-size: 12px;
font-weight: 600;
color: var(--text);
min-width: 50px;
text-align: right;
}
/* ========== Trial Info ========== */
.trial-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 8px;
}
.trial-item {
text-align: center;
padding: 10px 8px;
background: var(--bg);
border-radius: var(--radius-sm);
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
}
.trial-item:hover {
transform: scale(1.05);
box-shadow: var(--shadow);
}
.trial-item.success {
background: var(--success-light);
}
.trial-item.success:hover {
border-color: var(--success);
}
.trial-item.failed {
background: var(--error-light);
}
.trial-item.failed:hover {
border-color: var(--error);
}
.trial-item.error {
background: var(--warning-light);
}
.trial-item.error:hover {
border-color: var(--warning);
}
.trial-item.unknown {
background: #f3f4f6;
}
.trial-item.unknown:hover {
border-color: #6b7280;
}
.trial-item.current {
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--primary-light);
}
.trial-num {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 2px;
}
.trial-status {
font-size: 16px;
}
/* ========== Table Container ========== */
.table-container {
max-height: 400px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
/* ========== Task Stats Table ========== */
.task-stats-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.task-stats-table th,
.task-stats-table td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
}
.task-stats-table th {
background: var(--bg);
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
position: sticky;
top: 0;
z-index: 1;
}
.task-stats-table tbody tr:hover {
background: var(--bg-hover);
}
.task-stats-table .task-name-cell {
font-family: var(--mono);
max-width: 220px;
}
.task-stats-table .center-cell {
text-align: center;
font-family: var(--mono);
}
.task-stats-table .actions-cell {
font-size: 11px;
color: var(--text-secondary);
font-family: var(--mono);
}
.success-rate-cell {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 80px;
}
.success-rate-value {
font-size: 14px;
font-weight: 700;
}
.success-rate-bar {
height: 6px;
background: var(--bg);
border-radius: 3px;
overflow: hidden;
}
.success-rate-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
/* ========== Task Group (for repeat_n > 1) ========== */
.task-group {
background: var(--bg);
border: 2px solid transparent;
border-radius: var(--radius);
margin-bottom: 8px;
overflow: hidden;
transition: all 0.2s;
}
.task-group:hover {
background: var(--bg-hover);
border-color: var(--border);
}
.task-group.active {
background: var(--primary-light);
border-color: var(--primary);
}
.task-group-header {
padding: 12px 14px;
cursor: pointer;
}
.trial-indicators {
display: flex;
gap: 4px;
padding: 0 14px 12px;
flex-wrap: wrap;
}
.trial-indicator {
position: relative;
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
border: 2px solid transparent;
}
.trial-indicator.success {
background: var(--success-light);
color: var(--success);
}
.trial-indicator.failed {
background: var(--error-light);
color: var(--error);
}
.trial-indicator.error {
background: var(--warning-light);
color: var(--warning);
}
.trial-indicator.unknown {
background: #f3f4f6;
color: #6b7280;
}
.trial-indicator-tags {
position: absolute;
bottom: -2px;
right: -2px;
display: flex;
gap: 1px;
font-size: 8px;
line-height: 1;
pointer-events: none;
}
.trial-indicator:hover {
transform: scale(1.1);
}
.trial-indicator.active {
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--primary-light);
}
.status-badge.partial {
background: var(--warning);
box-shadow: 0 0 8px var(--warning);
}
/* ========== Scrollbar ========== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="logo">
<div class="logo-icon">📊</div>
<div>
<h1>Run Explorer</h1>
<p>轨迹数据可视化工具</p>
</div>
</div>
<div class="header-actions">
<button class="btn btn-secondary" id="btnRefresh" onclick="loadRunsViaHttp()">
<span>🔄</span> 刷新
</button>
</div>
</header>
<main>
<aside class="sidebar">
<div class="sidebar-header">
<h2>选择运行记录</h2>
<div class="run-select-wrap">
<select id="runSelect" disabled>
<option value="">加载中...</option>
</select>
</div>
</div>
<div class="filters">
<div class="search-box">
<input type="text" id="taskFilter" placeholder="搜索任务..." disabled>
</div>
<div id="activeFiltersBar" class="active-filters-bar" style="display: none;"></div>
<div class="filter-group">
<div class="filter-selects single">
<select id="fSuite" class="filter-select" disabled>
<option value="">全部 Suite</option>
</select>
</div>
</div>
<div class="filter-group">
<div class="filter-selects">
<select id="fDifficulty" class="filter-select" disabled>
<option value="">难度</option>
</select>
<select id="fScope" class="filter-select" disabled>
<option value="">范围</option>
</select>
<select id="fObjective" class="filter-select" disabled>
<option value="">目标</option>
</select>
<select id="fComposition" class="filter-select" disabled>
<option value="">组合</option>
</select>
</div>
</div>
<div class="filter-chips">
<label class="chip success active" title="完美成功:目标达成 + 无副作用 + 主动 COMPLETE">
<input type="checkbox" id="fSuccess" checked> ✓ 成功
</label>
<label class="chip failed active" title="非完美成功(含副作用、超步数、早终止、目标未达成等)">
<input type="checkbox" id="fFailed" checked> ✗ 失败
</label>
<label class="chip error active" title="execution.error / judge_error:系统/判定异常">
<input type="checkbox" id="fError" checked> ⚠ 错误
</label>
</div>
<div class="filter-chips" style="margin-top: 6px;">
<label class="chip sideEffect" title="勾选后仅显示带副作用的 trialUSE: judge.clean=false">
<input type="checkbox" id="fSideEffect"> 🔧 副作用
</label>
<label class="chip overdue" title="勾选后仅显示超步数的 trialOT: truncated=true 且 judge.success=true">
<input type="checkbox" id="fOverdue"> ⏱ 超步数
</label>
<label class="chip falseComplete" title="勾选后仅显示 false-complete 的 trialFC: stop_reason=COMPLETE 但 is_success=false">
<input type="checkbox" id="fFalseComplete"> 🚫 错误完成
</label>
</div>
</div>
<div class="task-list" id="taskList">
<div class="empty">
<div class="empty-icon">📋</div>
<h3>加载中...</h3>
<p>正在加载任务列表</p>
</div>
</div>
</aside>
<div class="content">
<div class="tabs">
<button class="tab active" data-tab="overview">📊 概览</button>
<button class="tab" data-tab="detail">📝 任务详情</button>
<button class="tab" data-tab="trajectory">🎯 轨迹查看</button>
<button class="tab" data-tab="all-images">🖼️ 全部截图</button>
</div>
<div class="tab-content">
<!-- Overview Panel -->
<div class="panel active" id="panel-overview">
<div class="empty" id="overviewEmpty">
<div class="empty-icon">📊</div>
<h3>加载中...</h3>
<p>正在从服务器加载运行记录</p>
</div>
<div id="overviewContent" style="display: none;"></div>
</div>
<!-- Detail Panel -->
<div class="panel" id="panel-detail">
<div class="empty" id="detailEmpty">
<div class="empty-icon">📝</div>
<h3>选择任务</h3>
<p>从左侧列表选择一个任务来查看详情</p>
</div>
<div id="detailContent" style="display: none;"></div>
</div>
<!-- Trajectory Panel -->
<div class="panel" id="panel-trajectory">
<div class="empty" id="trajectoryEmpty">
<div class="empty-icon">🎯</div>
<h3>选择任务</h3>
<p>从左侧列表选择一个任务来查看轨迹</p>
</div>
<div id="trajectoryContent" style="display: none;"></div>
</div>
<!-- All Images Panel -->
<div class="panel" id="panel-all-images">
<div class="empty" id="allImagesEmpty">
<div class="empty-icon">🖼️</div>
<h3>选择任务</h3>
<p>从左侧列表选择一个任务来查看所有截图</p>
</div>
<div id="allImagesContent" style="display: none;"></div>
</div>
</div>
</div>
</main>
</div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox">
<button class="lightbox-close" onclick="closeLightbox()">×</button>
<button class="lightbox-nav lightbox-prev" onclick="navigateLightbox(-1)"></button>
<img id="lightboxImg" src="" alt="">
<button class="lightbox-nav lightbox-next" onclick="navigateLightbox(1)"></button>
<div class="lightbox-info" id="lightboxInfo"></div>
</div>
<!-- Prompt Modal -->
<div class="prompt-modal" id="promptModal">
<div class="prompt-modal-content">
<div class="prompt-modal-header">
<span class="prompt-modal-title">📝 完整对话历史</span>
<button class="prompt-modal-close" onclick="closePromptModal()">×</button>
</div>
<div class="prompt-modal-body" id="promptModalBody">
<!-- Content will be injected here -->
</div>
</div>
</div>
<script>
// =========== Helpers ===========
const el = id => document.getElementById(id);
const API_BASE = '/api/runs';
async function fetchJson(urlPath, timeout = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(API_BASE + urlPath, { signal: controller.signal });
clearTimeout(timeoutId);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
} catch (e) {
clearTimeout(timeoutId);
if (e.name === 'AbortError') throw new Error('Request timeout');
throw e;
}
}
async function fetchText(urlPath, timeout = 30000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(API_BASE + urlPath, { signal: controller.signal });
clearTimeout(timeoutId);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
} catch (e) {
clearTimeout(timeoutId);
if (e.name === 'AbortError') throw new Error('Request timeout');
throw e;
}
}
function imageUrl(urlPath) {
return API_BASE + urlPath;
}
function escapeHtml(s) {
if (!s) return '';
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function formatValue(v) {
if (v === null) return 'null';
if (v === undefined) return 'undefined';
if (typeof v === 'boolean') return v ? 'true' : 'false';
if (typeof v === 'string') return v.length > 120 ? `"${v.slice(0, 120)}..."` : `"${v}"`;
if (typeof v === 'object') return JSON.stringify(v).slice(0, 120);
return String(v);
}
function formatTime(t) {
if (!t) return '-';
try { return new Date(t).toLocaleString('zh-CN'); } catch { return t; }
}
function debounce(fn, ms = 150) {
let t = null;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}
// =========== Taxonomy Filter Logic ===========
// Populate suite + taxonomy filter dropdowns from loaded results
function populateFilterOptions() {
const allRes = state.allResults || [];
const suites = new Set();
const difficulties = new Set();
const scopes = new Set();
const objectives = new Set();
const compositions = new Set();
for (const r of allRes) {
if (r.suite) suites.add(r.suite);
if (r.difficulty) difficulties.add(r.difficulty);
if (r.scope) scopes.add(r.scope);
if (r.objective) objectives.add(r.objective);
if (r.composition) compositions.add(r.composition);
}
const fillSelect = (id, defaultLabel, values, sortFn) => {
const sel = el(id);
const curVal = sel.value;
sel.innerHTML = `<option value="">${defaultLabel}</option>`;
const sorted = [...values].sort(sortFn || ((a, b) => a.localeCompare(b)));
sorted.forEach(v => sel.appendChild(new Option(v, v)));
sel.disabled = sorted.length === 0;
if (curVal && sorted.includes(curVal)) sel.value = curVal;
};
fillSelect('fSuite', `全部 Suite (${suites.size})`, suites);
fillSelect('fDifficulty', '难度', difficulties);
fillSelect('fScope', '范围', scopes);
fillSelect('fObjective', '目标', objectives);
fillSelect('fComposition', '组合', compositions);
updateActiveFiltersBar();
}
// Get the taxonomy dimensions for a particular task (from its first result)
function getResultTaxonomy(taskId) {
const trials = state.resultsByTask?.get(taskId);
if (!trials || trials.length === 0) return null;
const r = trials[0];
return {
suite: r.suite || null,
difficulty: r.difficulty || null,
scope: r.scope || null,
objective: r.objective || null,
composition: r.composition || null,
};
}
// Check if a task passes all taxonomy filters
function taskPassesTaxonomyFilters(taskId) {
const fSuite = el('fSuite').value;
const fDifficulty = el('fDifficulty').value;
const fScope = el('fScope').value;
const fObjective = el('fObjective').value;
const fComposition = el('fComposition').value;
// No filter active = pass
if (!fSuite && !fDifficulty && !fScope && !fObjective && !fComposition) return true;
const tax = getResultTaxonomy(taskId);
if (!tax) return false;
if (fSuite && tax.suite !== fSuite) return false;
if (fDifficulty && tax.difficulty !== fDifficulty) return false;
if (fScope && tax.scope !== fScope) return false;
if (fObjective && tax.objective !== fObjective) return false;
if (fComposition && tax.composition !== fComposition) return false;
return true;
}
// Get results filtered by all taxonomy filters
function getFilteredResults() {
const allRes = state.allResults || [];
const fSuite = el('fSuite').value;
const fDifficulty = el('fDifficulty').value;
const fScope = el('fScope').value;
const fObjective = el('fObjective').value;
const fComposition = el('fComposition').value;
if (!fSuite && !fDifficulty && !fScope && !fObjective && !fComposition) return allRes;
return allRes.filter(r => {
if (fSuite && r.suite !== fSuite) return false;
if (fDifficulty && r.difficulty !== fDifficulty) return false;
if (fScope && r.scope !== fScope) return false;
if (fObjective && r.objective !== fObjective) return false;
if (fComposition && r.composition !== fComposition) return false;
return true;
});
}
// Check if any taxonomy filter is active
function hasTaxonomyFilters() {
return !!(el('fSuite').value || el('fDifficulty').value || el('fScope').value ||
el('fObjective').value || el('fComposition').value);
}
// Render the active filters tags bar
function updateActiveFiltersBar() {
const bar = el('activeFiltersBar');
const filters = [
{ id: 'fSuite', label: 'Suite' },
{ id: 'fDifficulty', label: '难度' },
{ id: 'fScope', label: '范围' },
{ id: 'fObjective', label: '目标' },
{ id: 'fComposition', label: '组合' },
];
const active = filters.filter(f => el(f.id).value);
if (active.length === 0) {
bar.style.display = 'none';
return;
}
bar.style.display = 'flex';
bar.innerHTML = active.map(f =>
`<span class="active-filter-tag">${f.label}: ${el(f.id).value} <span class="remove-filter" onclick="el('${f.id}').value=''; onTaxonomyFilterChange()">×</span></span>`
).join('') + `<button class="filter-reset-btn" onclick="resetTaxonomyFilters()">清除全部</button>`;
// Update select highlight
filters.forEach(f => {
el(f.id).classList.toggle('active-filter', !!el(f.id).value);
});
}
function resetTaxonomyFilters() {
['fSuite', 'fDifficulty', 'fScope', 'fObjective', 'fComposition'].forEach(id => { el(id).value = ''; });
onTaxonomyFilterChange();
}
function onTaxonomyFilterChange() {
updateActiveFiltersBar();
renderTaskList();
renderOverview();
}
// Render chat messages from prompt array
function renderChatMessages(messages, uniquePrefix = '') {
if (!messages || !Array.isArray(messages)) return '<div class="no-response">无对话数据</div>';
return messages.map((msg, msgIdx) => {
const role = msg.role || 'unknown';
const roleIcon = role === 'system' ? '⚙️' : role === 'user' ? '👤' : role === 'assistant' ? '🤖' : '❓';
const roleName = role === 'system' ? 'System' : role === 'user' ? 'User' : role === 'assistant' ? 'Assistant' : role;
const textId = `${uniquePrefix}_msg_${msgIdx}`;
let contentHtml = '';
let isLongSystemMsg = false;
if (typeof msg.content === 'string') {
// Simple string content
isLongSystemMsg = role === 'system' && msg.content.length > 500;
contentHtml = `<div class="chat-text" id="${textId}">${escapeHtml(msg.content)}</div>`;
if (isLongSystemMsg) {
contentHtml += `<button class="expand-btn" onclick="toggleSystemMsg('${textId}', this)">▼ 展开完整内容</button>`;
}
} else if (Array.isArray(msg.content)) {
// Array of content items (multimodal)
contentHtml = msg.content.map(item => {
if (item.type === 'text') {
return `<div class="chat-content-item chat-text">${escapeHtml(item.text || '')}</div>`;
} else if (item.type === 'image_url') {
// Show placeholder for image (data stripped)
const url = item.image_url?.url || '';
const isStripped = url.includes('STRIPPED') || url.startsWith('[');
return `<div class="chat-content-item">
<div class="chat-image-placeholder">
🖼️ ${isStripped ? '图片 (数据已省略)' : '图片'}
</div>
</div>`;
}
return '';
}).join('');
}
return `
<div class="chat-message ${role}">
<div class="chat-role">${roleIcon} ${roleName}</div>
${contentHtml}
</div>
`;
}).join('');
}
function toggleSystemMsg(textId, btn) {
const textEl = document.getElementById(textId);
if (!textEl) return;
if (textEl.classList.contains('expanded')) {
textEl.classList.remove('expanded');
btn.innerHTML = '▼ 展开完整内容';
} else {
textEl.classList.add('expanded');
btn.innerHTML = '▲ 收起';
}
}
// Get prompt stats
function getPromptStats(messages) {
if (!messages || !Array.isArray(messages)) return { total: 0, system: 0, user: 0, assistant: 0 };
return {
total: messages.length,
system: messages.filter(m => m.role === 'system').length,
user: messages.filter(m => m.role === 'user').length,
assistant: messages.filter(m => m.role === 'assistant').length
};
}
// =========== State ===========
const state = {
runsList: [],
runDirName: '',
meta: null,
summary: null,
results: new Map(),
allResults: [],
resultsByTask: new Map(),
tasks: [],
taskGroups: new Map(),
selectedTask: null,
trajectory: [],
selectedStepIdx: 0,
mediaCache: new Map(),
actionTypesCache: null,
perTaskActionTypes: {},
actionTypesLoading: false,
isLoading: false, // Prevent duplicate operations
};
let lightboxImages = [];
let lightboxIndex = 0;
// =========== Data Loading ===========
async function loadRunsViaHttp() {
state.runDirName = '';
try {
const data = await fetchJson('');
state.runsList = data.runs || [];
} catch (e) {
console.error('Failed to load runs:', e);
state.runsList = [];
}
await refreshRunsDropdown();
}
async function refreshRunsDropdown() {
const sel = el('runSelect');
sel.innerHTML = '';
if (state.runsList.length === 0) {
sel.disabled = true;
sel.innerHTML = '<option value="">runs 目录下没有子目录</option>';
return;
}
sel.disabled = false;
sel.innerHTML = '<option value="">选择一个 run...</option>';
state.runsList.forEach(name => sel.appendChild(new Option(name, name)));
// Auto select first
if (!state.runDirName && state.runsList.length > 0) {
sel.value = state.runsList[0];
await loadRunDir(state.runsList[0]);
}
}
async function loadRunDir(runName) {
if (state.isLoading) return;
state.isLoading = true;
try {
// Show loading state
el('overviewEmpty').innerHTML = `
<div class="empty-icon">⏳</div>
<h3>加载中...</h3>
<p>正在加载 ${runName}</p>
`;
el('overviewEmpty').style.display = 'block';
el('overviewContent').style.display = 'none';
state.runDirName = runName;
state.selectedTask = null;
state.trajectory = [];
state.selectedStepIdx = 0;
state.actionTypesCache = null;
state.perTaskActionTypes = {};
state.actionTypesLoading = false;
clearMediaCache();
// Load meta
try {
state.meta = await fetchJson(`/${runName}/meta.json`);
} catch { state.meta = null; }
// Load summary
try {
state.summary = await fetchJson(`/${runName}/summary.json`);
} catch { state.summary = null; }
// Load results.jsonl
state.results = new Map();
state.allResults = [];
state.resultsByTask = new Map();
try {
const text = await fetchText(`/${runName}/results.jsonl`);
for (const line of text.split('\n')) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
if (obj.id) {
obj.task_id = obj.id;
state.allResults.push(obj);
if (!state.resultsByTask.has(obj.id)) {
state.resultsByTask.set(obj.id, []);
}
state.resultsByTask.get(obj.id).push(obj);
const key = obj.trial_id !== undefined ? `${obj.id}_trial_${obj.trial_id}` : obj.id;
state.results.set(key, obj);
if (!state.results.has(obj.id)) {
state.results.set(obj.id, obj);
}
}
} catch {}
}
for (const [taskId, trials] of state.resultsByTask) {
trials.sort((a, b) => (a.trial_id ?? 0) - (b.trial_id ?? 0));
}
} catch {}
// Load tasks - parse from directory names
state.tasks = [];
state.taskGroups = new Map();
try {
const trajData = await fetchJson(`/${runName}/trajectory`);
const taskDirs = trajData.tasks || [];
// Helper: convert dir name to result id format
// e.g., wechat_OpenNewFriends_i0 -> wechat.OpenNewFriends_i0
// Only replace the first underscore (between app and task name) with a dot
const dirNameToResultId = (name) => {
// Find the FIRST underscore followed by an uppercase letter — that's the suite/task boundary
// e.g. x1_X1_SendDm -> x1.X1_SendDm, tencent_meeting_Check -> tencent_meeting.Check
const match = name.match(/^(.*?)_([A-Z].*)$/);
if (match) {
return `${match[1]}.${match[2]}`;
}
return name;
};
for (const dirName of taskDirs) {
const trialMatch = dirName.match(/^(.+)_t(\d+)$/);
let baseTaskId, trialId;
if (trialMatch) {
baseTaskId = dirNameToResultId(trialMatch[1]);
trialId = parseInt(trialMatch[2]);
} else {
baseTaskId = dirNameToResultId(dirName);
trialId = 0;
}
const resultData = state.resultsByTask.get(baseTaskId)?.[0];
const task_name = resultData?.task_name || '';
const taskEntry = {
dirName,
task_id: baseTaskId,
baseTaskId,
trialId,
task_name,
meta: null
};
state.tasks.push(taskEntry);
if (!state.taskGroups.has(baseTaskId)) {
state.taskGroups.set(baseTaskId, []);
}
state.taskGroups.get(baseTaskId).push(taskEntry);
}
state.tasks.sort((a, b) => {
const cmp = a.baseTaskId.localeCompare(b.baseTaskId, 'zh-CN');
if (cmp !== 0) return cmp;
return (a.trialId ?? 0) - (b.trialId ?? 0);
});
for (const [taskId, trials] of state.taskGroups) {
trials.sort((a, b) => (a.trialId ?? 0) - (b.trialId ?? 0));
}
} catch {}
el('taskFilter').disabled = false;
populateFilterOptions();
renderAll();
// 自动加载动作统计(后台异步,不阻塞界面)
loadActionTypes();
// Select first task without blocking
if (state.tasks.length > 0) {
state.isLoading = false; // Reset before selectTask
selectTask(state.tasks[0]); // Don't await
return;
}
} catch (e) {
console.error('loadRunDir error:', e);
el('overviewEmpty').innerHTML = `
<div class="empty-icon">❌</div>
<h3>加载失败</h3>
<p style="color: var(--error);">${e.message || '未知错误'}</p>
<button class="btn btn-secondary" onclick="loadRunDir('${runName}')" style="margin-top: 12px;">
重试
</button>
`;
} finally {
state.isLoading = false;
}
}
async function loadTrajectory(task) {
state.trajectory = [];
state.selectedStepIdx = 0;
clearMediaCache();
try {
state.trajectory = await fetchJson(`/${state.runDirName}/trajectory/${task.dirName}/trajectory.json`);
if (!Array.isArray(state.trajectory)) state.trajectory = [];
} catch { state.trajectory = []; }
}
function clearMediaCache() {
// Simply clear the cache - URLs are from API, not blob URLs
state.mediaCache = new Map();
}
async function getMedia(idx) {
if (state.mediaCache.has(idx)) return state.mediaCache.get(idx);
const step = state.trajectory[idx];
const media = { rawUrl: '', annotUrl: '', responseText: '', prompt: null };
if (step && state.selectedTask) {
const taskBasePath = `/${state.runDirName}/trajectory/${state.selectedTask.dirName}`;
if (step.screenshot) {
media.rawUrl = imageUrl(`${taskBasePath}/${step.screenshot}`);
}
if (step.screenshot_annotated) {
media.annotUrl = imageUrl(`${taskBasePath}/${step.screenshot_annotated}`);
}
// Load model response text
if (step.model_response_path) {
try {
media.responseText = await fetchText(`${taskBasePath}/${step.model_response_path}`);
} catch {}
}
// Load prompt JSON
const stepNum = step.step ?? (idx + 1);
const promptFileName = `step_${String(stepNum).padStart(3, '0')}_prompt.json`;
try {
media.prompt = await fetchJson(`${taskBasePath}/${promptFileName}`);
} catch {}
}
state.mediaCache.set(idx, media);
return media;
}
// =========== Task Selection ===========
async function selectTask(task) {
console.log('selectTask called:', task?.dirName, 'isLoading:', state.isLoading);
if (state.isLoading) {
console.log('selectTask blocked by isLoading');
return;
}
if (state.selectedTask?.dirName === task.dirName) {
console.log('selectTask skipped - same task');
return;
}
state.isLoading = true;
console.log('selectTask starting for:', task?.dirName);
try {
state.selectedTask = task;
renderTaskList();
el('trajectoryContent').innerHTML = `
<div class="empty" style="padding: 40px;">
<div class="empty-icon">⏳</div>
<h3>加载轨迹...</h3>
</div>
`;
el('trajectoryContent').style.display = 'block';
el('trajectoryEmpty').style.display = 'none';
await loadTrajectory(task);
console.log('selectTask trajectory loaded');
renderDetail();
renderTrajectory();
renderAllImages();
console.log('selectTask completed');
} catch (e) {
console.error('selectTask error:', e);
} finally {
state.isLoading = false;
console.log('selectTask isLoading reset to false');
}
}
function getTaskStatus(task) {
return getTaskStatusForTrial(task);
}
// Get all trials for a specific task
function getTrialsForTask(taskId) {
return state.resultsByTask?.get(taskId) || [];
}
// Select a specific trial for viewing
async function selectTrial(taskId, trialIdx) {
if (state.isLoading) return;
const taskGroup = state.taskGroups.get(taskId);
if (taskGroup && trialIdx >= 0 && trialIdx < taskGroup.length) {
const task = taskGroup[trialIdx];
state.isLoading = true;
try {
state.selectedTask = task;
renderTaskList();
await loadTrajectory(task);
renderDetail();
renderTrajectory();
renderAllImages();
} catch (e) {
console.error('selectTrial error:', e);
} finally {
state.isLoading = false;
}
}
}
// =========== Rendering ===========
function renderAll() {
renderOverview();
renderTaskList();
renderDetail();
renderTrajectory();
renderAllImages();
}
function renderOverview() {
if (!state.runDirName) {
el('overviewEmpty').style.display = 'block';
el('overviewContent').style.display = 'none';
return;
}
el('overviewEmpty').style.display = 'none';
el('overviewContent').style.display = 'block';
const sum = state.summary || {};
const meta = state.meta || {};
const successRate = ((sum.success_rate ?? 0) * 100).toFixed(1);
const hasPassK = sum.pass_at_k && Object.keys(sum.pass_at_k).length > 0;
const hasRepeat = (sum.repeat_n ?? meta.repeat_n ?? 1) > 1;
// Get filtered results for overview stats
const filteredRes = getFilteredResults();
const isFiltered = hasTaxonomyFilters();
const filterBadge = isFiltered ? ` <span style="font-size: 11px; font-weight: 500; color: var(--primary); background: var(--primary-light); padding: 2px 8px; border-radius: 10px; margin-left: 8px;">已筛选: ${filteredRes.length} 条</span>` : '';
// Calculate additional statistics (with filtered data)
const statsData = calculateDetailedStats(isFiltered ? filteredRes : undefined);
// Pre-calculate per-trial stats for header display (with filtered data)
const preTrialStats = calculatePerTrialStatsQuick(isFiltered ? filteredRes : undefined);
// 使用重新计算的值(基于 clean && finished)而不是 summary.json 中的旧值
const actualSuccess = preTrialStats ? preTrialStats.perfectSuccess : (sum.success ?? 0);
const actualTotal = preTrialStats ? preTrialStats.totalResults : (sum.total_tasks ?? sum.total ?? state.tasks.length);
const actualError = preTrialStats ? preTrialStats.errorCount : 0;
const actualValid = actualTotal - actualError;
const actualFailed = actualValid - actualSuccess;
const actualSuccessRate = actualValid > 0 ? (actualSuccess / actualValid * 100).toFixed(1) : '0.0';
const filteredEpisodes = isFiltered ? filteredRes.length : (sum.total_episodes ?? '-');
let html = `
${filterBadge}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${actualTotal}</div>
<div class="stat-label">任务数</div>
</div>
${hasRepeat ? `
<div class="stat-card">
<div class="stat-value">${filteredEpisodes}</div>
<div class="stat-label">总运行次数</div>
</div>
` : ''}
<div class="stat-card">
<div class="stat-value success">${actualSuccess}</div>
<div class="stat-label">成功</div>
</div>
<div class="stat-card">
<div class="stat-value error">${actualFailed}</div>
<div class="stat-label">失败</div>
</div>
${actualError > 0 ? `
<div class="stat-card" style="border-left: 4px solid var(--warning);" title="评判/执行阶段内部错误,非 Agent 行为导致">
<div class="stat-value" style="color: var(--warning);">${actualError}</div>
<div class="stat-label">错误</div>
</div>
` : ''}
<div class="stat-card highlight">
<div class="stat-value">${actualSuccessRate}%</div>
<div class="stat-label">成功率</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">${actualSuccess}/${actualValid}</div>
</div>
${preTrialStats ? `
<div class="stat-card" style="border-left: 4px solid #60a5fa;" title="目标达成率 = judge.success=true 的比例(排除 error,不考虑副作用和是否主动终止)">
<div class="stat-value" style="color: #60a5fa;">${(preTrialStats.goalSuccessRate * 100).toFixed(1)}%</div>
<div class="stat-label">目标达成率</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">${preTrialStats.totalGoalSuccess}/${preTrialStats.validResults}</div>
</div>
` : ''}
${hasRepeat && preTrialStats ? `
<div class="stat-card" style="border-left: 4px solid var(--success);">
<div class="stat-value success">${preTrialStats.tasksWithSuccess}</div>
<div class="stat-label">有成功的任务</div>
</div>
<div class="stat-card" style="border-left: 4px solid var(--error);">
<div class="stat-value error">${preTrialStats.tasksAllFailed}</div>
<div class="stat-label">全部失败的任务</div>
</div>
<div class="stat-card" style="border-left: 4px solid var(--primary);">
<div class="stat-value" style="color: var(--primary);">${preTrialStats.tasksAllSuccess}</div>
<div class="stat-label">全部成功的任务</div>
</div>
` : ''}
</div>
`;
// 论文 §3.5 评估指标(PR / FC / OT / USE
if (preTrialStats) {
const pr = (preTrialStats.progressRate * 100).toFixed(1);
const fc = (preTrialStats.falseCompleteRate * 100).toFixed(1);
const otr = (preTrialStats.overdueRate * 100).toFixed(1);
const use = (preTrialStats.sideEffectRate * 100).toFixed(1);
html += `
<div class="card" style="border-left: 4px solid var(--purple);">
<div class="card-title">
<div class="icon purple">📐</div>
多维评估指标
<span style="font-size: 11px; font-weight: 400; color: var(--text-muted); margin-left: auto;">论文 §3.5 评估协议</span>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 16px;">
<div style="padding: 16px; border-radius: var(--radius-sm); text-align: center; background: linear-gradient(135deg, var(--primary-light) 0%, var(--purple-light) 100%); border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: 800; color: var(--primary-dark);">${pr}%</div>
<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-top: 4px;">Progress Rate</div>
<div style="font-size: 10px; color: var(--text-muted);">平均目标完成进度</div>
</div>
<div style="padding: 16px; border-radius: var(--radius-sm); text-align: center; background: ${preTrialStats.falseCompleteCount > 0 ? 'var(--error-light)' : 'var(--success-light)'}; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: 800; color: ${preTrialStats.falseCompleteCount > 0 ? 'var(--error)' : 'var(--success)'};">${fc}%</div>
<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-top: 4px;">False Complete</div>
<div style="font-size: 10px; color: var(--text-muted);">声称完成但未真正成功 (${preTrialStats.falseCompleteCount})</div>
</div>
<div style="padding: 16px; border-radius: var(--radius-sm); text-align: center; background: ${preTrialStats.overdueCount > 0 ? 'var(--warning-light)' : 'var(--success-light)'}; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: 800; color: ${preTrialStats.overdueCount > 0 ? 'var(--warning)' : 'var(--success)'};">${otr}%</div>
<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-top: 4px;">Overdue Term.</div>
<div style="font-size: 10px; color: var(--text-muted);">达成目标却未主动结束 (${preTrialStats.overdueCount})</div>
</div>
<div style="padding: 16px; border-radius: var(--radius-sm); text-align: center; background: ${preTrialStats.uncleanCount > 0 ? '#fff7ed' : 'var(--success-light)'}; border: 1px solid var(--border);">
<div style="font-size: 24px; font-weight: 800; color: ${preTrialStats.uncleanCount > 0 ? '#f97316' : 'var(--success)'};">${use}%</div>
<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-top: 4px;">Side Effects</div>
<div style="font-size: 10px; color: var(--text-muted);">意外状态变更 (${preTrialStats.uncleanCount})</div>
</div>
</div>
<div style="padding: 12px 16px; background: var(--bg); border-radius: var(--radius-sm); font-size: 12px; color: var(--text-secondary);">
<div style="margin-bottom: 6px;"><strong style="color: var(--text);">📌 指标定义(论文 §3.5):</strong></div>
<div style="display: grid; gap: 3px;">
<div><strong>PR (Progress Rate)</strong>:平均 check_goals 完成进度,mean(passed_checks / total_checks)</div>
<div><strong>FC (False Complete)</strong>Agent 声明完成但 episode 未 fully successful (stop_reason=COMPLETE 且 is_success=false)</div>
<div><strong>OT (Overdue Termination)</strong>:达成 goal state 但未主动 COMPLETE,被 step budget / loop detection 截断 (truncated=true 且 judge.success=true)</div>
<div><strong>USE (Side Effects)</strong>:产生非预期状态变更 (judge.clean=false),分母为全体 episode,与 SR/FC/OT 互不互斥</div>
</div>
</div>
</div>
`;
}
// 目标达成分析(如果有目标达成但不是完美成功的情况)
if (preTrialStats?.totalGoalSuccess > preTrialStats?.perfectSuccess) {
const goalSuccessPct = (preTrialStats.goalSuccessRate * 100).toFixed(1);
const successRatePct = successRate;
// 计算各类别占目标达成的比例
const total = preTrialStats.totalGoalSuccess;
const perfectPct = total > 0 ? (preTrialStats.perfectSuccess / total * 100).toFixed(1) : 0;
const unawarePct = total > 0 ? (preTrialStats.passedButUnaware / total * 100).toFixed(1) : 0;
const sideEffectFinishedPct = total > 0 ? (preTrialStats.sideEffectFinished / total * 100).toFixed(1) : 0;
const sideEffectUnawarePct = total > 0 ? (preTrialStats.sideEffectUnaware / total * 100).toFixed(1) : 0;
html += `
<div class="card" style="border-left: 4px solid #60a5fa;">
<div class="card-title">
<div class="icon" style="background: #dbeafe;">📊</div>
目标达成分析
</div>
<div style="margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
<span style="font-size: 24px; font-weight: 800; color: #60a5fa;">${preTrialStats.totalGoalSuccess}</span>
<span style="color: var(--text-secondary);">次目标达成 (${goalSuccessPct}%)</span>
</div>
<div style="display: flex; height: 24px; border-radius: 4px; overflow: hidden; background: var(--bg);">
${preTrialStats.perfectSuccess > 0 ? `<div style="width: ${perfectPct}%; background: var(--success); display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600;" title="完美成功: ${preTrialStats.perfectSuccess}">✓</div>` : ''}
${preTrialStats.passedButUnaware > 0 ? `<div style="width: ${unawarePct}%; background: var(--warning); display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600;" title="达成但未意识: ${preTrialStats.passedButUnaware}">⚠</div>` : ''}
${preTrialStats.sideEffectFinished > 0 ? `<div style="width: ${sideEffectFinishedPct}%; background: #f97316; display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600;" title="有副作用(主动终止): ${preTrialStats.sideEffectFinished}">🔧</div>` : ''}
${preTrialStats.sideEffectUnaware > 0 ? `<div style="width: ${sideEffectUnawarePct}%; background: #c2410c; display: flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 600;" title="有副作用(未终止): ${preTrialStats.sideEffectUnaware}">⚠🔧</div>` : ''}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px;">
<div style="text-align: center; padding: 12px; background: var(--success-light); border-radius: var(--radius-sm); border: 2px solid var(--success);">
<div style="font-size: 20px; font-weight: 800; color: var(--success);">${preTrialStats.perfectSuccess}</div>
<div style="font-size: 11px; color: var(--text-secondary);">✓ 完美成功</div>
<div style="font-size: 10px; color: var(--text-muted);">${perfectPct}%</div>
</div>
${preTrialStats.passedButUnaware > 0 ? `
<div style="text-align: center; padding: 12px; background: var(--warning-light); border-radius: var(--radius-sm); border: 2px solid var(--warning);">
<div style="font-size: 20px; font-weight: 800; color: var(--warning);">${preTrialStats.passedButUnaware}</div>
<div style="font-size: 11px; color: var(--text-secondary);">⚠️ 未意识</div>
<div style="font-size: 10px; color: var(--text-muted);">${unawarePct}%</div>
</div>
` : ''}
${preTrialStats.sideEffectFinished > 0 ? `
<div style="text-align: center; padding: 12px; background: #fff7ed; border-radius: var(--radius-sm); border: 2px solid #f97316;">
<div style="font-size: 20px; font-weight: 800; color: #f97316;">${preTrialStats.sideEffectFinished}</div>
<div style="font-size: 11px; color: var(--text-secondary);">🔧 有副作用</div>
<div style="font-size: 10px; color: var(--text-muted);">${sideEffectFinishedPct}%</div>
</div>
` : ''}
${preTrialStats.sideEffectUnaware > 0 ? `
<div style="text-align: center; padding: 12px; background: #fed7aa; border-radius: var(--radius-sm); border: 2px solid #c2410c;">
<div style="font-size: 20px; font-weight: 800; color: #c2410c;">${preTrialStats.sideEffectUnaware}</div>
<div style="font-size: 11px; color: var(--text-secondary);">⚠️🔧 双重问题</div>
<div style="font-size: 10px; color: var(--text-muted);">${sideEffectUnawarePct}%</div>
</div>
` : ''}
</div>
<div style="padding: 12px 16px; background: var(--bg); border-radius: var(--radius-sm); font-size: 12px; color: var(--text-secondary);">
<div style="margin-bottom: 8px;"><strong style="color: var(--text);">📌 指标说明:</strong></div>
<div style="display: grid; gap: 4px;">
<div><span style="color: var(--success);">✓ 完美成功</span>:目标达成 + 无副作用 + 主动终止</div>
<div><span style="color: var(--warning);">⚠️ 未意识</span>:目标达成 + 无副作用,但未主动终止</div>
<div><span style="color: #f97316;">🔧 有副作用</span>:目标达成 + 主动终止,但有意外状态变化</div>
<div><span style="color: #c2410c;">⚠️🔧 双重问题</span>:目标达成,但有副作用且未主动终止</div>
</div>
</div>
</div>
`;
}
// Taxonomy Distribution (Difficulty / Scope / Objective / Composition breakdown)
(() => {
const allRes = filteredRes;
const hasTaxonomy = allRes.some(r => r.difficulty || r.scope);
if (hasTaxonomy) {
const buildBreakdown = (dimKey, label, colorMap) => {
const groups = {};
allRes.forEach(r => {
const val = r[dimKey];
if (!val) return;
if (!groups[val]) groups[val] = { total: 0, success: 0, goalSuccess: 0 };
groups[val].total++;
if (r.is_success) groups[val].success++;
if (r.judge?.success) groups[val].goalSuccess++;
});
if (Object.keys(groups).length === 0) return '';
const sorted = Object.entries(groups).sort(([a], [b]) => a.localeCompare(b));
return `
<div style="margin-bottom: 16px;">
<div style="font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 8px;">${label}</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px;">
${sorted.map(([k, v]) => {
const color = colorMap[k] || '#64748b';
const sr = v.total > 0 ? (v.success / v.total * 100).toFixed(0) : 0;
return `
<div style="padding: 10px; border-radius: var(--radius-sm); background: ${color}10; border: 1px solid ${color}30; text-align: center;">
<div style="font-size: 12px; font-weight: 700; color: ${color}; margin-bottom: 4px;">${k}</div>
<div style="font-size: 18px; font-weight: 800; color: ${color};">${sr}%</div>
<div style="font-size: 10px; color: var(--text-muted);">${v.success}/${v.total}</div>
</div>`;
}).join('')}
</div>
</div>`;
};
const dc = { L1: '#10b981', L2: '#3b82f6', L3: '#f59e0b', L4: '#ef4444' };
const sc = { S1: '#10b981', S2: '#3b82f6', S3: '#8b5cf6' };
const oc = { operate: '#3b82f6', query: '#10b981', hybrid: '#f59e0b', vague: '#94a3b8', safety: '#ef4444' };
const cc = { atomic: '#94a3b8', sequential: '#3b82f6', transfer: '#10b981', deep_dive: '#8b5cf6' };
html += `
<div class="card" style="border-left: 4px solid var(--primary);">
<div class="card-title">
<div class="icon blue">🏷️</div>
按分类维度统计
</div>
${buildBreakdown('difficulty', '📊 按难度 (Difficulty)', dc)}
${buildBreakdown('scope', '📐 按范围 (Scope)', sc)}
${buildBreakdown('objective', '🎯 按目标 (Objective)', oc)}
${buildBreakdown('composition', '🧩 按组合 (Composition)', cc)}
</div>
`;
}
})();
// 运行配置 (放在最上面)
html += `
<div class="card">
<div class="card-title">
<div class="icon blue">⚙️</div>
运行配置
</div>
<div class="info-grid">
${[
['开始时间', formatTime(meta.start_time)],
['Agent', meta.agent],
['模型', meta.model_name],
['坐标空间', meta.coord_space],
['最大步数', meta.max_steps],
['重复次数', meta.repeat_n ?? 1],
['应用', meta.app || (meta.apps?.join(', '))],
['环境URL', meta.env_url],
['并行度', meta.parallel],
['隔离模式', meta.isolation],
['截图缩放', meta.screenshot_scale],
['Pass@K 值', meta.pass_k?.join(', ')],
].filter(([k, v]) => v != null && v !== '').map(([k, v]) => `
<div class="info-item">
<div class="info-label">${k}</div>
<div class="info-value">${v ?? '-'}</div>
</div>
`).join('')}
</div>
</div>
<details>
<summary>查看完整 meta.json</summary>
<div class="content">
<div class="code">${JSON.stringify(meta, null, 2)}</div>
</div>
</details>
`;
// Pass@K Statistics (if available)
const hasFilteredPassK = isFiltered && preTrialStats?.passAtKSuccess && Object.keys(preTrialStats.passAtKSuccess).length > 0;
if (hasPassK || hasFilteredPassK) {
const passKSource = hasFilteredPassK ? preTrialStats.passAtKSuccess : sum.pass_at_k;
const passKKeys = Object.keys(passKSource).sort((a, b) => parseInt(a) - parseInt(b));
const hasPassedPassK = preTrialStats?.passAtKPassed && Object.keys(preTrialStats.passAtKPassed).length > 0;
html += `
<div class="card">
<div class="card-title">
<div class="icon purple">🎯</div>
Pass@K 统计 ${hasRepeat ? `(每任务重复 ${sum.repeat_n ?? meta.repeat_n} 次)` : ''}${hasFilteredPassK ? ' <span style="font-size: 11px; color: var(--primary);">(已筛选)</span>' : ''}
</div>
<div style="display: flex; gap: 16px; margin-bottom: 12px; font-size: 12px;">
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 12px; height: 12px; background: var(--success); border-radius: 2px;"></span>
任务成功 (success+clean+finished)
</span>
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 12px; height: 12px; background: var(--warning); border-radius: 2px;"></span>
目标达成 (judge.success)
</span>
</div>
<div class="pass-k-grid">
${passKKeys.map(k => {
const successVal = ((passKSource[k] ?? 0) * 100).toFixed(1);
const passedVal = hasPassedPassK ? ((preTrialStats.passAtKPassed[k] ?? 0) * 100).toFixed(1) : successVal;
const hasGap = hasPassedPassK && parseFloat(passedVal) > parseFloat(successVal);
return `
<div class="pass-k-item" style="position: relative;">
<div class="pass-k-label">Pass@${k}</div>
<div style="display: flex; flex-direction: column; gap: 4px; align-items: center;">
${hasGap ? `<div style="font-size: 16px; font-weight: 700; color: var(--warning);">${passedVal}%</div>` : ''}
<div class="pass-k-value" style="color: var(--success); ${hasGap ? 'font-size: 14px;' : ''}">${successVal}%</div>
</div>
${hasGap ? `<div style="font-size: 10px; color: var(--warning); margin-top: 2px;">+${(parseFloat(passedVal) - parseFloat(successVal)).toFixed(1)}%</div>` : ''}
</div>
`;
}).join('')}
</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
💡 Pass@K 表示在 K 次尝试中至少成功/达成一次的概率
</div>
</div>
`;
}
// Per-Trial Statistics and Task Success Summary
if (hasRepeat && statsData.perTrialStats) {
const pts = statsData.perTrialStats;
html += `
<div class="card">
<div class="card-title">
<div class="icon green">📊</div>
分 Trial 统计
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px;">
<div class="info-item" style="text-align: center; padding: 16px;">
<div class="info-label">有成功的任务</div>
<div style="font-size: 28px; font-weight: 800; color: var(--success);">${pts.tasksWithSuccess}</div>
<div style="font-size: 12px; color: var(--text-muted);">至少成功一次</div>
</div>
<div class="info-item" style="text-align: center; padding: 16px;">
<div class="info-label">全部失败的任务</div>
<div style="font-size: 28px; font-weight: 800; color: var(--error);">${pts.tasksAllFailed}</div>
<div style="font-size: 12px; color: var(--text-muted);">所有 trial 都失败</div>
</div>
<div class="info-item" style="text-align: center; padding: 16px;">
<div class="info-label">全部成功的任务</div>
<div style="font-size: 28px; font-weight: 800; color: var(--primary);">${pts.tasksAllSuccess}</div>
<div style="font-size: 12px; color: var(--text-muted);">所有 trial 都成功</div>
</div>
</div>
<div style="display: flex; gap: 16px; margin-bottom: 12px; font-size: 12px;">
<span class="info-label" style="margin: 0;">各 Trial 统计</span>
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 12px; height: 12px; background: var(--warning); border-radius: 2px;"></span>
目标达成率
</span>
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 12px; height: 12px; background: var(--success); border-radius: 2px;"></span>
任务成功率
</span>
</div>
<div class="bar-chart">
${pts.trialSuccessRates.map((rate, idx) => {
const successPct = (rate * 100).toFixed(1);
const passedRate = pts.trialPassedRates?.[idx] ?? rate;
const passedPct = (passedRate * 100).toFixed(1);
const passedCount = pts.trialPassedCounts?.[idx] ?? pts.trialSuccessCounts[idx];
const hasGap = passedRate > rate;
return `
<div class="bar-item">
<div class="bar-label">Trial ${idx}</div>
<div class="bar-track" style="position: relative;">
<!-- 目标达成率(紫色底层) -->
<div class="bar-fill purple" style="width: ${Math.max(parseFloat(passedPct), 3)}%; position: absolute; left: 0; top: 0; height: 100%; opacity: ${hasGap ? 1 : 0.3};">
${hasGap ? `<span style="position: absolute; right: 8px; color: white; font-size: 11px;">${passedCount}/${pts.totalTasks}</span>` : ''}
</div>
<!-- 任务成功率(绿色顶层) -->
<div class="bar-fill success" style="width: ${Math.max(parseFloat(successPct), 3)}%; position: relative; z-index: 1;">
${pts.trialSuccessCounts[idx]}/${pts.totalTasks}
</div>
</div>
<div class="bar-value" style="min-width: 90px;">
${hasGap ? `<span style="color: var(--warning);">${passedCount}/${pts.totalTasks}</span> / ` : ''}
<span style="color: var(--success);">${pts.trialSuccessCounts[idx]}/${pts.totalTasks}</span>
</div>
</div>
`;
}).join('')}
</div>
${pts.trialSuccessRates.length > 1 ? `
<div style="margin-top: 16px; padding: 12px; background: var(--bg); border-radius: var(--radius-sm);">
<div style="display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px;">
<span style="color: var(--success);">成功率: <strong>${(pts.trialSuccessRates.reduce((a,b) => a+b, 0) / pts.trialSuccessRates.length * 100).toFixed(1)}%</strong> (平均)</span>
<span style="color: var(--warning);">达成率: <strong>${(pts.trialPassedRates.reduce((a,b) => a+b, 0) / pts.trialPassedRates.length * 100).toFixed(1)}%</strong> (平均)</span>
<span>差距: <strong style="color: var(--warning);">${((pts.trialPassedRates.reduce((a,b) => a+b, 0) - pts.trialSuccessRates.reduce((a,b) => a+b, 0)) / pts.trialPassedRates.length * 100).toFixed(1)}%</strong></span>
</div>
</div>
` : ''}
</div>
`;
}
// 运行总结
html += `
<div class="card">
<div class="card-title">
<div class="icon green">📈</div>
运行总结
</div>
<div class="info-grid">
${[
['结束时间', formatTime(sum.end_time)],
['总运行时间', sum.start_time && sum.end_time ? formatDuration(new Date(sum.end_time) - new Date(sum.start_time)) : null],
['平均步数', sum.avg_steps?.toFixed(2)],
['平均耗时', sum.avg_runtime_s ? `${sum.avg_runtime_s.toFixed(1)}s` : null],
['错误任务数', sum.error],
].filter(([k, v]) => v != null).map(([k, v]) => `
<div class="info-item">
<div class="info-label">${k}</div>
<div class="info-value">${v ?? '-'}</div>
</div>
`).join('')}
</div>
</div>
`;
// Steps Statistics
if (statsData.stepsStats) {
const ss = statsData.stepsStats;
html += `
<div class="card">
<div class="card-title">
<div class="icon blue">📊</div>
步数统计
</div>
<div class="info-grid">
${[
['最小步数', ss.min],
['最大步数', ss.max],
['平均步数', ss.avg.toFixed(2)],
['中位数', ss.median],
['标准差', ss.std.toFixed(2)],
].map(([k, v]) => `
<div class="info-item">
<div class="info-label">${k}</div>
<div class="info-value">${v}</div>
</div>
`).join('')}
</div>
${ss.distribution ? `
<div style="margin-top: 16px;">
<div class="info-label" style="margin-bottom: 12px;">步数分布</div>
<div class="bar-chart">
${Object.entries(ss.distribution).map(([range, count]) => {
const pct = (count / ss.total * 100).toFixed(1);
return `
<div class="bar-item">
<div class="bar-label">${range} 步</div>
<div class="bar-track">
<div class="bar-fill primary" style="width: ${Math.max(pct, 2)}%">${count}</div>
</div>
<div class="bar-value">${pct}%</div>
</div>
`;
}).join('')}
</div>
</div>
` : ''}
</div>
`;
}
// Action Type Distribution (only show when data is loaded)
if (state.actionTypesCache && Object.keys(state.actionTypesCache).length > 0) {
const totalActions = Object.values(state.actionTypesCache).reduce((a, b) => a + b, 0);
const sortedActions = Object.entries(state.actionTypesCache).sort((a, b) => b[1] - a[1]);
html += `
<div class="card">
<div class="card-title">
<div class="icon purple">🎬</div>
动作类型分布 (共 ${totalActions} 个动作)
</div>
<div class="bar-chart">
${sortedActions.map(([action, count], idx) => {
const pct = (count / totalActions * 100).toFixed(1);
const colors = ['primary', 'success', 'purple', 'warning'];
const color = colors[idx % colors.length];
return `
<div class="bar-item">
<div class="bar-label">${action}</div>
<div class="bar-track">
<div class="bar-fill ${color}" style="width: ${Math.max(pct, 3)}%">${count}</div>
</div>
<div class="bar-value">${pct}%</div>
</div>
`;
}).join('')}
</div>
</div>
`;
}
// Per-App Success Rate
if (statsData.perApp && Object.keys(statsData.perApp).length > 0) {
html += `
<div class="card">
<div class="card-title">
<div class="icon green">📱</div>
分应用成功率
</div>
<div class="bar-chart">
${Object.entries(statsData.perApp).map(([app, data]) => {
const pct = (data.successRate * 100).toFixed(1);
return `
<div class="bar-item">
<div class="bar-label" title="${app}">${app}</div>
<div class="bar-track">
<div class="bar-fill success" style="width: ${Math.max(pct, 2)}%">${data.success}/${data.total}</div>
</div>
<div class="bar-value">${pct}%</div>
</div>
`;
}).join('')}
</div>
</div>
`;
}
// Task Complexity Analysis
if (statsData.complexity && statsData.complexity.length > 0) {
html += `
<div class="card">
<div class="card-title">
<div class="icon blue">🧩</div>
任务复杂度分析 (按步数)
</div>
<div class="info-grid">
${statsData.complexity.map(c => `
<div class="info-item">
<div class="info-label">${c.level}</div>
<div class="info-value">${c.count} 任务 (${c.successRate.toFixed(1)}% 成功)</div>
</div>
`).join('')}
</div>
</div>
`;
}
// Per-Task Statistics Table (detailed view with actions and Pass@K)
if (statsData.perTaskStats && Object.keys(statsData.perTaskStats).length > 0) {
const sortedTasks = Object.entries(statsData.perTaskStats)
.sort((a, b) => b[1].successRate - a[1].successRate);
const hasActionData = state.actionTypesCache && Object.keys(state.actionTypesCache).length > 0;
// 检查是否有 Pass@K 数据
const hasPassK = sum.per_task_pass_k && Object.keys(sum.per_task_pass_k).length > 0;
let passKKeys = [];
if (hasPassK) {
passKKeys = Object.keys(Object.values(sum.per_task_pass_k)[0] || {})
.filter(k => k.startsWith('pass@'))
.sort((a, b) => parseInt(a.split('@')[1]) - parseInt(b.split('@')[1]));
}
html += `
<div class="card">
<div class="card-title" style="justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 10px;">
<div class="icon purple">🏷️</div>
按任务类型统计
</div>
${!hasActionData && !state.actionTypesLoading ? `
<button class="btn btn-secondary" onclick="loadActionTypes()" style="font-size: 12px; padding: 6px 12px;">
📊 加载动作统计
</button>
` : ''}
${state.actionTypesLoading ? `
<span style="font-size: 12px; color: var(--primary);">⏳ 加载中...</span>
` : ''}
</div>
<div class="table-container" style="max-height: 500px;">
<table class="task-stats-table">
<thead>
<tr>
<th>任务类型</th>
<th title="任务成功率 (success && clean && finished)">
<span style="color: var(--success);">✓</span> 成功率
</th>
<th title="目标达成率 (judge.success=true)">
<span style="color: var(--warning);">🎯</span> 达成率
</th>
<th>成功/达成/总数</th>
<th title="Progress Rate: 平均 check_goals 完成进度">PR</th>
${hasPassK ? passKKeys.map(k => `<th style="text-align: center;">${k}</th>`).join('') : ''}
<th>平均步数</th>
<th>主要动作</th>
</tr>
</thead>
<tbody>
${sortedTasks.map(([taskName, data]) => {
const successPct = (data.successRate * 100).toFixed(0);
const goalSuccessPct = ((data.goalSuccessRate ?? data.successRate) * 100).toFixed(0);
const hasGap = (data.goalSuccessRate ?? 0) > data.successRate;
const successBarColor = data.successRate >= 0.8 ? 'var(--success)' :
data.successRate >= 0.5 ? 'var(--warning)' : 'var(--error)';
const goalSuccessBarColor = (data.goalSuccessRate ?? 0) >= 0.8 ? 'var(--warning)' :
(data.goalSuccessRate ?? 0) >= 0.5 ? 'var(--purple)' : 'var(--purple)';
// Re-calculate actions with latest data
const actionCounts = getActionCountsForTask(taskName);
const topActions = Object.entries(actionCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([type, count]) => `${type}:${count}`)
.join(', ');
// 获取该任务的 Pass@K 数据
const taskPassK = hasPassK ? sum.per_task_pass_k[taskName] : null;
const passKCells = hasPassK ? passKKeys.map(k => {
const val = taskPassK?.[k] ?? 0;
const pct = (val * 100).toFixed(0);
let cellClass = 'pass-k-cell';
if (val >= 1) cellClass += ' full';
else if (val === 0) cellClass += ' zero';
else cellClass += ' partial';
return `<td class="${cellClass}">${pct}%</td>`;
}).join('') : '';
return `
<tr>
<td class="task-name-cell" title="${taskName}">
<strong>${taskName.split('.').pop() || taskName}</strong>
</td>
<td>
<div class="success-rate-cell">
<span class="success-rate-value" style="color: ${successBarColor}">${successPct}%</span>
<div class="success-rate-bar">
<div class="success-rate-fill" style="width: ${successPct}%; background: ${successBarColor}"></div>
</div>
</div>
</td>
<td>
<div class="success-rate-cell">
<span class="success-rate-value" style="color: var(--purple);">${goalSuccessPct}%</span>
<div class="success-rate-bar">
<div class="success-rate-fill" style="width: ${goalSuccessPct}%; background: var(--purple);"></div>
</div>
${hasGap ? `<span style="font-size: 10px; color: var(--warning);">+${(parseFloat(goalSuccessPct) - parseFloat(successPct)).toFixed(0)}%</span>` : ''}
</div>
</td>
<td class="center-cell">
<span style="color: var(--success);">${data.success}</span> /
<span style="color: var(--purple);">${data.goalSuccess ?? data.success}</span> /
${data.total}
</td>
<td class="center-cell" style="font-family: var(--mono); color: ${(data.progressRate ?? 0) >= 0.8 ? 'var(--success)' : (data.progressRate ?? 0) >= 0.5 ? 'var(--warning)' : 'var(--error)'};">${((data.progressRate ?? 0) * 100).toFixed(0)}%</td>
${passKCells}
<td class="center-cell">${data.avgSteps.toFixed(1)}</td>
<td class="actions-cell">${topActions || (hasActionData ? '-' : '<span style="color: var(--text-muted);">点击加载</span>')}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
</div>
`;
}
// 收集各类任务列表(基于新规则重新计算)
const realSuccessTasks = []; // success && clean && finished (完美成功)
const passedButUnawareTasks = []; // success && clean && !finished (未意识)
const sideEffectFinishedTasks = []; // success && !clean && finished (有副作用)
const sideEffectUnawareTasks = []; // success && !clean && !finished (双重问题)
const realFailedTasks = []; // !success (真正失败)
const resultsToCheck = filteredRes;
for (const r of resultsToCheck) {
const success = r.judge?.success === true;
const clean = r.judge?.clean === true;
const finished = r.execution?.finished === true;
const taskId = r.task_id || r.id;
const trialId = r.trial_id;
const label = trialId !== undefined ? `${taskId} (T${trialId})` : taskId;
if (success && clean && finished) {
realSuccessTasks.push(label);
} else if (success && clean && !finished) {
passedButUnawareTasks.push(label);
} else if (success && !clean && finished) {
sideEffectFinishedTasks.push(label);
} else if (success && !clean && !finished) {
sideEffectUnawareTasks.push(label);
} else {
realFailedTasks.push(label);
}
}
// Success/Failed task lists (collapsible) - 使用新规则重新计算的列表
html += `
<details ${realSuccessTasks.length && realSuccessTasks.length <= 20 ? 'open' : ''}>
<summary style="color: var(--success);">✓ 成功任务列表 (${realSuccessTasks.length} 个)</summary>
<div class="content">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;">
这些任务目标达成(judge.success=true)、无副作用(judge.clean=true)且主动完成(stop_reason=COMPLETE)。
</p>
${realSuccessTasks.length ? `
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${realSuccessTasks.map(t => `<span class="badge success">${t}</span>`).join('')}
</div>
` : '<p style="color: var(--text-muted);">无成功任务</p>'}
</div>
</details>
${passedButUnawareTasks.length > 0 ? `
<details ${passedButUnawareTasks.length <= 20 ? 'open' : ''}>
<summary style="color: var(--warning);">⚠️ 达成但未意识任务列表 (${passedButUnawareTasks.length} 个)</summary>
<div class="content">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;">
这些任务的目标已达成(judge.success=true)且无副作用(judge.clean=true),但模型没有主动以 COMPLETE 结束任务(stop_reason≠COMPLETE),最终被判定为失败。
</p>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${passedButUnawareTasks.map(t => `<span class="badge" style="background: var(--warning-light); color: var(--warning);">${t}</span>`).join('')}
</div>
</div>
</details>
` : ''}
${sideEffectFinishedTasks.length > 0 ? `
<details ${sideEffectFinishedTasks.length <= 20 ? 'open' : ''}>
<summary style="color: #f97316;">🔧 达成但有副作用任务列表 (${sideEffectFinishedTasks.length} 个)</summary>
<div class="content">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;">
这些任务的目标已达成(judge.success=true)且模型主动完成(stop_reason=COMPLETE),但产生了意外的副作用(judge.clean=false),最终被判定为失败。
</p>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${sideEffectFinishedTasks.map(t => `<span class="badge" style="background: #fff7ed; color: #f97316;">${t}</span>`).join('')}
</div>
</div>
</details>
` : ''}
${sideEffectUnawareTasks.length > 0 ? `
<details ${sideEffectUnawareTasks.length <= 20 ? 'open' : ''}>
<summary style="color: #c2410c;">⚠️🔧 双重问题任务列表 (${sideEffectUnawareTasks.length} 个)</summary>
<div class="content">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;">
这些任务的目标已达成(judge.success=true),但既有副作用(judge.clean=false)又未以 COMPLETE 结束(stop_reason≠COMPLETE),最终被判定为失败。
</p>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${sideEffectUnawareTasks.map(t => `<span class="badge" style="background: #fed7aa; color: #c2410c;">${t}</span>`).join('')}
</div>
</div>
</details>
` : ''}
<details ${realFailedTasks.length && realFailedTasks.length <= 20 ? 'open' : ''}>
<summary style="color: var(--error);">✗ 失败任务列表 (${realFailedTasks.length} 个)</summary>
<div class="content">
<p style="font-size: 13px; color: var(--text-secondary); margin-bottom: 12px;">
这些任务目标未达成(judge.success=false)。
</p>
${realFailedTasks.length ? `
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${realFailedTasks.map(t => `<span class="badge failed">${t}</span>`).join('')}
</div>
` : '<p style="color: var(--text-muted);">无失败任务</p>'}
</div>
</details>
<details>
<summary>查看完整 summary.json</summary>
<div class="content">
<div class="code">${JSON.stringify(sum, null, 2)}</div>
</div>
</details>
`;
el('overviewContent').innerHTML = html;
}
// Calculate detailed statistics from results
function calculateDetailedStats(filteredResults) {
const stats = {
stepsStats: null,
actionTypes: {},
perApp: {},
complexity: [],
taskTypes: {},
perTaskStats: {},
perTrialStats: null
};
if (state.results.size === 0 && state.allResults.length === 0) return stats;
// Use filteredResults if provided, otherwise fallback to allResults
const resultsToAnalyze = filteredResults || (state.allResults.length > 0 ? state.allResults : Array.from(state.results.values()));
// Collect steps data
const stepsArr = [];
const tasksBySteps = { simple: [], medium: [], complex: [] };
// Per-task detailed stats collection
const perTaskData = {};
for (const r of resultsToAnalyze) {
const taskId = r.task_id || r.id;
const steps = r.execution?.steps;
if (typeof steps === 'number' && steps > 0) {
stepsArr.push(steps);
// Categorize by complexity
if (steps <= 5) tasksBySteps.simple.push(r);
else if (steps <= 12) tasksBySteps.medium.push(r);
else tasksBySteps.complex.push(r);
}
// Per-app stats
const appMatch = taskId?.match(/^([^.]+)\./);
if (appMatch) {
const app = appMatch[1];
if (!stats.perApp[app]) {
stats.perApp[app] = { total: 0, success: 0, successRate: 0 };
}
stats.perApp[app].total++;
if (isRealSuccess(r)) stats.perApp[app].success++;
}
// Task type stats (extract verb from task name like ReadMyWxid -> Read)
const taskTypeMatch = taskId?.match(/^[^.]+\.([A-Z][a-z]+)/);
if (taskTypeMatch) {
const taskType = taskTypeMatch[1];
if (!stats.taskTypes[taskType]) {
stats.taskTypes[taskType] = { total: 0, success: 0 };
}
stats.taskTypes[taskType].total++;
if (isRealSuccess(r)) stats.taskTypes[taskType].success++;
}
// Per-task detailed stats
if (taskId) {
if (!perTaskData[taskId]) {
perTaskData[taskId] = {
total: 0,
success: 0,
goalSuccess: 0,
errorCount: 0,
stepsArr: [],
actionCounts: {},
progressSum: 0,
falseCompleteCount: 0,
overdueCount: 0,
};
}
const td = perTaskData[taskId];
td.total++;
const realSuccess = isRealSuccess(r);
if (realSuccess) td.success++;
const goalOk = r.judge?.success === true;
if (goalOk) td.goalSuccess++;
td.progressSum += (r.progress ?? r.judge?.progress ?? (goalOk ? 1.0 : 0.0));
const isErr = r.is_error || r.execution?.error || r.judge?.judge_error
|| r.judge?.issues?.some(i => 'error' in i);
if (isErr) td.errorCount++;
// FC: stop_reason=COMPLETE 且 episode 未 fully successful (论文 §3.5)。
// 新字段 false_complete 存在时优先使用;历史 run 没有该字段时按 raw fields 重算,
// 避免旧 premature_termination 口径漏掉「goal 达成但有副作用」一类。
const fcFromRaw = (r.execution?.stop_reason === 'COMPLETE' && !realSuccess);
const fcFlag = r.false_complete === undefined ? fcFromRaw : r.false_complete === true;
if (!isErr && fcFlag) td.falseCompleteCount++;
// OT: 统一按 raw fields 重算 (truncated && goal_success),与 overview /
// getTaskIndicators / METRICS_RECALC_NOTES 对齐。旧 run 中 overdue_termination
// 字段曾表示「所有 truncated」,不可信,必须忽略。
if (r.execution?.truncated === true && goalOk) td.overdueCount++;
if (typeof steps === 'number') {
td.stepsArr.push(steps);
}
}
}
// Calculate per-app success rates
for (const app of Object.keys(stats.perApp)) {
const data = stats.perApp[app];
data.successRate = data.total > 0 ? data.success / data.total : 0;
}
// Calculate per-task stats with action types from trajectories
for (const [taskId, data] of Object.entries(perTaskData)) {
const avgSteps = data.stepsArr.length > 0
? data.stepsArr.reduce((a, b) => a + b, 0) / data.stepsArr.length
: 0;
// Get action counts from cached action types or trajectory data
const actionCounts = getActionCountsForTask(taskId);
const topActions = Object.entries(actionCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([type, count]) => ({ type, count }));
const validCount = data.total - (data.errorCount || 0);
stats.perTaskStats[taskId] = {
total: data.total,
success: data.success,
goalSuccess: data.goalSuccess,
errorCount: data.errorCount || 0,
successRate: validCount > 0 ? data.success / validCount : 0,
goalSuccessRate: validCount > 0 ? data.goalSuccess / validCount : 0,
progressRate: data.total > 0 ? data.progressSum / data.total : 0,
falseCompleteRate: data.total > 0 ? data.falseCompleteCount / data.total : 0,
overdueRate: data.total > 0 ? data.overdueCount / data.total : 0,
avgSteps,
topActions
};
}
// Per-Trial Statistics (for repeat_n > 1)
const meta = state.meta || {};
const sum = state.summary || {};
const repeatN = sum.repeat_n ?? meta.repeat_n ?? 1;
if (repeatN > 1) {
// Re-group resultsToAnalyze by taskId for filtered per-trial stats
const filteredByTask = new Map();
for (const r of resultsToAnalyze) {
const id = r.id || r.task_id;
if (!id) continue;
if (!filteredByTask.has(id)) filteredByTask.set(id, []);
filteredByTask.get(id).push(r);
}
if (filteredByTask.size > 0) {
const trialSuccessCounts = new Array(repeatN).fill(0);
const trialPassedCounts = new Array(repeatN).fill(0); // 目标达成数
let tasksWithSuccess = 0;
let tasksAllFailed = 0;
let tasksAllSuccess = 0;
let validTaskCount = 0; // 排除全 error 的任务
const totalTasks = filteredByTask.size;
for (const [taskId, trials] of filteredByTask) {
const errorCount = trials.filter(t =>
t.is_error || t.execution?.error || t.judge?.judge_error
|| t.judge?.issues?.some(i => 'error' in i)).length;
const validN = trials.length - errorCount;
const successCount = trials.filter(t => isRealSuccess(t)).length;
if (validN > 0) {
validTaskCount++;
// Count tasks by success pattern
if (successCount > 0) tasksWithSuccess++;
if (successCount === 0) tasksAllFailed++;
if (successCount === validN) tasksAllSuccess++;
}
// Count success and passed per trial index
for (const trial of trials) {
const trialIdx = trial.trial_id ?? 0;
if (trialIdx < repeatN) {
if (isRealSuccess(trial)) {
trialSuccessCounts[trialIdx]++;
}
// 目标达成:judge.success=true
if (trial.judge?.success === true) {
trialPassedCounts[trialIdx]++;
}
}
}
}
const trialSuccessRates = trialSuccessCounts.map(count =>
validTaskCount > 0 ? count / validTaskCount : 0
);
const trialPassedRates = trialPassedCounts.map(count =>
validTaskCount > 0 ? count / validTaskCount : 0
);
stats.perTrialStats = {
totalTasks: validTaskCount,
repeatN,
trialSuccessCounts,
trialSuccessRates,
trialPassedCounts,
trialPassedRates,
tasksWithSuccess,
tasksAllFailed,
tasksAllSuccess
};
} // end if filteredByTask.size > 0
} // end if repeatN > 1
// Steps statistics
if (stepsArr.length > 0) {
stepsArr.sort((a, b) => a - b);
const sum = stepsArr.reduce((a, b) => a + b, 0);
const avg = sum / stepsArr.length;
const median = stepsArr.length % 2 === 0
? (stepsArr[stepsArr.length / 2 - 1] + stepsArr[stepsArr.length / 2]) / 2
: stepsArr[Math.floor(stepsArr.length / 2)];
const variance = stepsArr.reduce((acc, v) => acc + Math.pow(v - avg, 2), 0) / stepsArr.length;
const std = Math.sqrt(variance);
// Distribution buckets
const distribution = {};
const buckets = [[1, 5], [6, 10], [11, 15], [16, 20], [21, Infinity]];
for (const [min, max] of buckets) {
const label = max === Infinity ? `${min}+` : `${min}-${max}`;
distribution[label] = stepsArr.filter(s => s >= min && s <= max).length;
}
stats.stepsStats = {
min: stepsArr[0],
max: stepsArr[stepsArr.length - 1],
avg,
median,
std,
total: stepsArr.length,
distribution
};
}
// Complexity analysis
const complexityLevels = [
{ level: '简单 (1-5步)', tasks: tasksBySteps.simple },
{ level: '中等 (6-12步)', tasks: tasksBySteps.medium },
{ level: '复杂 (13+步)', tasks: tasksBySteps.complex }
];
stats.complexity = complexityLevels.map(c => ({
level: c.level,
count: c.tasks.length,
successRate: c.tasks.length > 0 ?
c.tasks.filter(t => isRealSuccess(t)).length / c.tasks.length * 100 : 0
})).filter(c => c.count > 0);
// Action types will be collected async when available
// They're stored in state.actionTypesCache after loadActionTypes() is called
return stats;
}
function formatDuration(ms) {
if (!ms || ms < 0) return '-';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}小时 ${minutes % 60}分钟`;
} else if (minutes > 0) {
return `${minutes}分钟 ${seconds % 60}秒`;
} else {
return `${seconds}秒`;
}
}
function calculateStd(arr) {
if (!arr || arr.length === 0) return 0;
const avg = arr.reduce((a, b) => a + b, 0) / arr.length;
const variance = arr.reduce((acc, v) => acc + Math.pow(v - avg, 2), 0) / arr.length;
return Math.sqrt(variance);
}
// Pass@K 无偏估计器计算
function calculatePassAtK(n, c, k) {
if (n <= 0 || k <= 0) return 0;
if (c >= n) return 1; // 全部成功
if (k > n) k = n;
if (n - c < k) return 1; // 失败数小于 k,必然至少成功一次
// Pass@k = 1 - C(n-c, k) / C(n, k)
// = 1 - product((n-c-i)/(n-i) for i in 0..k-1)
let prob = 1;
for (let i = 0; i < k; i++) {
prob *= (n - c - i) / (n - i);
}
return 1 - prob;
}
// Quick calculation for header stats
function calculatePerTrialStatsQuick(filteredResults) {
// When filteredResults provided, re-group by taskId
const taskMap = filteredResults ? new Map() : state.resultsByTask;
if (filteredResults) {
for (const r of filteredResults) {
const id = r.id || r.task_id;
if (!id) continue;
if (!taskMap.has(id)) taskMap.set(id, []);
taskMap.get(id).push(r);
}
}
if (taskMap.size === 0) return null;
let tasksWithSuccess = 0;
let tasksAllFailed = 0;
let tasksAllSuccess = 0;
// 收集每个任务的成功数和达成数,用于计算 Pass@K
const taskSuccessCounts = [];
const taskPassedCounts = [];
for (const [taskId, trials] of taskMap) {
// 排除 error episodes
const errorTrials = trials.filter(t =>
t.is_error || t.execution?.error || t.judge?.judge_error
|| t.judge?.issues?.some(i => 'error' in i));
const validN = trials.length - errorTrials.length;
// 使用新规则计算成功数
const successCount = trials.filter(t => isRealSuccess(t)).length;
// 目标达成数:judge.success=true(不考虑 clean
const goalSuccessCount = trials.filter(t => t.judge?.success === true).length;
taskSuccessCounts.push({ n: validN, c: successCount });
taskPassedCounts.push({ n: validN, c: goalSuccessCount });
if (successCount > 0) tasksWithSuccess++;
if (successCount === 0 && validN > 0) tasksAllFailed++;
if (successCount === validN && validN > 0) tasksAllSuccess++;
}
// 统计目标达成后的各种情况
const resultsToAnalyze = filteredResults || (state.allResults.length > 0 ? state.allResults : Array.from(state.results.values()));
let totalResults = resultsToAnalyze.length;
// 目标达成相关统计 (基于 judge.success)
let totalGoalSuccess = 0; // judge.success=true 的总数
let perfectSuccess = 0; // is_success=true (success + clean + stop_reason=COMPLETE)
let passedButUnaware = 0; // success=true, clean=true, stop_reason≠COMPLETE (达成但未以 COMPLETE 结束)
let sideEffectFinished = 0; // success=true, clean=false, stop_reason=COMPLETE (达成但有副作用,主动完成)
let sideEffectUnaware = 0; // success=true, clean=false, stop_reason≠COMPLETE (达成但有副作用,未以 COMPLETE 结束)
// 旧的 passed 统计 (基于 judge.passed = success AND clean),保留用于兼容
let totalPassed = 0;
// 论文 §3.5 聚合指标
let progressSum = 0; // 用于计算 Progress Rate (PR)
let falseCompleteCount = 0; // FC: stop_reason=COMPLETE 但 episode 未 fully successful
let overdueCount = 0; // OT: truncated=true 且 goal_success(达成目标却未主动结束)
let uncleanCount = 0; // USE: judge.clean=false(独立诊断,与 SR/FC/OT 不互斥)
let errorCount = 0; // Judge/Exec errors (not agent's fault)
for (const r of resultsToAnalyze) {
const success = r.judge?.success === true;
const clean = r.judge?.clean === true;
const finished = r.execution?.finished === true;
const isCompleted = r.execution?.stop_reason === 'COMPLETE';
const realSuccess = isRealSuccess(r);
if (success) {
totalGoalSuccess++;
if (clean && isCompleted) {
perfectSuccess++; // is_success = true
} else if (clean && !isCompleted) {
passedButUnaware++; // 达成但未以 COMPLETE 结束
} else if (!clean && isCompleted) {
sideEffectFinished++; // 达成但有副作用(主动完成)
} else {
sideEffectUnaware++; // 达成但有副作用(未以 COMPLETE 结束)
}
}
if (r.judge?.passed === true) {
totalPassed++;
}
// PR: 收集 progress(优先用顶层字段,回退到 judge.progress
const prog = r.progress ?? r.judge?.progress ?? (success ? 1.0 : 0.0);
progressSum += prog;
// Error detection (exec or judge)
const isError = r.is_error || r.execution?.error || r.judge?.judge_error
|| r.judge?.issues?.some(i => 'error' in i);
if (isError) errorCount++;
// FC (论文 §3.5): stop_reason=COMPLETE 但 episode 未 fully successful。
// 新字段 false_complete 存在时优先使用;历史 run 没有该字段时按 raw fields 重算。
const fcFromRaw = isCompleted && !realSuccess;
const fcFlag = r.false_complete === undefined ? fcFromRaw : r.false_complete === true;
if (!isError && fcFlag) {
falseCompleteCount++;
}
// OT (论文 §3.5): 达成 goal 但未主动 COMPLETE,被 step budget / loop 截断。
// NOTES 提示老 run 的 overdue_termination 字段语义曾经是「所有 truncation」,
// 不可信,统一按 raw fields 重算 (truncated=true 且 judge.success=true)。
if (r.execution?.truncated === true && success) {
overdueCount++;
}
// USE: 有意外副作用(分母为全体 episode,与 SR/FC/OT 不互斥)
if (!clean && r.judge != null) {
uncleanCount++;
}
}
// 有效 episode 数(排除 error
const validResults = totalResults - errorCount;
// 目标达成率 = goal_success / valid (基于 judge.success,排除 error)
const goalSuccessRate = validResults > 0 ? totalGoalSuccess / validResults : 0;
// 旧的 passed 率 (基于 judge.passed = success AND clean),保留用于兼容
const passedRate = validResults > 0 ? totalPassed / validResults : 0;
// 论文 §3.5 聚合指标
const progressRate = totalResults > 0 ? progressSum / totalResults : 0;
const falseCompleteRate = totalResults > 0 ? falseCompleteCount / totalResults : 0;
const overdueRate = totalResults > 0 ? overdueCount / totalResults : 0;
const sideEffectRate = totalResults > 0 ? uncleanCount / totalResults : 0;
// 计算基于 judge.success 的 Pass@K(目标达成)
const meta = state.meta || {};
const sum = state.summary || {};
const passKValues = meta.pass_k || [1, 2, 4, 8];
// 只有 n > 0 的任务才计入 Pass@K 平均
const validPassedTasks = taskPassedCounts.filter(t => t.n > 0);
const validSuccessTasks = taskSuccessCounts.filter(t => t.n > 0);
const numPassedTasks = validPassedTasks.length || 1;
const numSuccessTasks = validSuccessTasks.length || 1;
const passAtKPassed = {}; // 基于 judge.success 的 Pass@K(目标达成)
const passAtKSuccess = {}; // 基于 isRealSuccess 的 Pass@K(完美成功)
for (const k of passKValues) {
passAtKPassed[k] = validPassedTasks.reduce((acc, { n, c }) => acc + calculatePassAtK(n, c, k), 0) / numPassedTasks;
passAtKSuccess[k] = validSuccessTasks.reduce((acc, { n, c }) => acc + calculatePassAtK(n, c, k), 0) / numSuccessTasks;
}
return {
tasksWithSuccess, tasksAllFailed, tasksAllSuccess,
// 目标达成分解
totalGoalSuccess, goalSuccessRate,
perfectSuccess, passedButUnaware, sideEffectFinished, sideEffectUnaware,
// 旧的 passed 统计
totalPassed, totalResults, passedRate, passAtKPassed, passAtKSuccess,
// 论文 §3.5 聚合指标
progressRate, falseCompleteRate, falseCompleteCount, overdueRate, overdueCount, sideEffectRate, uncleanCount,
errorCount, validResults,
};
}
// Load action types from all trajectories
async function loadActionTypes() {
if (state.actionTypesLoading) return;
state.actionTypesLoading = true;
state.actionTypesCache = {};
state.perTaskActionTypes = {}; // Store per-task action types
renderOverview();
try {
for (const task of state.tasks) {
try {
const trajectory = await fetchJson(`/${state.runDirName}/trajectory/${task.dirName}/trajectory.json`);
const baseTaskId = task.baseTaskId || task.task_id;
if (!state.perTaskActionTypes[baseTaskId]) {
state.perTaskActionTypes[baseTaskId] = {};
}
if (Array.isArray(trajectory)) {
for (const step of trajectory) {
const actionType = step.action_type || 'UNKNOWN';
// Global action types
state.actionTypesCache[actionType] = (state.actionTypesCache[actionType] || 0) + 1;
// Per-task action types
state.perTaskActionTypes[baseTaskId][actionType] =
(state.perTaskActionTypes[baseTaskId][actionType] || 0) + 1;
}
}
} catch {}
}
} catch (e) {
console.error('Failed to load action types:', e);
}
state.actionTypesLoading = false;
renderOverview();
}
// Get action counts for a specific task
function getActionCountsForTask(taskId) {
if (state.perTaskActionTypes && state.perTaskActionTypes[taskId]) {
return state.perTaskActionTypes[taskId];
}
return {};
}
function renderTaskList() {
const wrap = el('taskList');
if (!state.runDirName) {
wrap.innerHTML = `
<div class="empty">
<div class="empty-icon">📋</div>
<h3>暂无数据</h3>
<p>正在加载...</p>
</div>
`;
return;
}
const filter = el('taskFilter').value.toLowerCase();
const showSuccess = el('fSuccess').checked;
const showFailed = el('fFailed').checked;
const showError = el('fError').checked;
const filterSideEffect = el('fSideEffect').checked;
const filterOverdue = el('fOverdue').checked;
const filterFalseComplete = el('fFalseComplete').checked;
// Update chip active states
document.querySelectorAll('.chip').forEach(chip => {
const input = chip.querySelector('input');
chip.classList.toggle('active', input.checked);
});
// 主结果白名单 + 问题标签正向 OR 过滤:
// - 标签全不选:不按标签过滤
// - 勾选标签:trial 命中任意一个已选标签即可保留
function trialPasses(t) {
const status = getTaskStatusForTrial(t);
if (status === 'success' && !showSuccess) return false;
if (status === 'failed' && !showFailed) return false;
if (status === 'error' && !showError) return false;
if (status === 'unknown' && !showError) return false;
const tags = getTaskIndicators(t);
const selectedIssueTags = [];
if (filterSideEffect) selectedIssueTags.push('sideEffect');
if (filterOverdue) selectedIssueTags.push('overdue');
if (filterFalseComplete) selectedIssueTags.push('falseComplete');
if (selectedIssueTags.length > 0 && !selectedIssueTags.some(tag => tags.includes(tag))) {
return false;
}
return true;
}
const sum = state.summary || {};
const meta = state.meta || {};
const hasPassK = sum.per_task_pass_k && Object.keys(sum.per_task_pass_k).length > 0;
const repeatN = sum.repeat_n ?? meta.repeat_n ?? 1;
// Determine if we should show grouped view (when repeat_n > 1)
const showGrouped = repeatN > 1 && state.taskGroups.size > 0;
let html = '';
if (showGrouped) {
// Grouped view - show tasks with their trials
const filteredGroups = [];
for (const [baseTaskId, trials] of state.taskGroups) {
// Taxonomy filter check
if (!taskPassesTaxonomyFilters(baseTaskId)) continue;
// Text filter check
const text = `${baseTaskId} ${trials[0]?.task_name || ''}`.toLowerCase();
if (filter && !text.includes(filter)) continue;
const visibleTrials = trials.filter(trialPasses);
if (visibleTrials.length === 0) continue;
filteredGroups.push({ baseTaskId, visibleTrials });
}
if (filteredGroups.length === 0) {
wrap.innerHTML = `
<div class="empty">
<div class="empty-icon">🔍</div>
<h3>未找到任务</h3>
<p>尝试调整筛选条件</p>
</div>
`;
return;
}
const visibleTrialCount = filteredGroups.reduce((acc, { visibleTrials }) => acc + visibleTrials.length, 0);
html = filteredGroups.map(({ baseTaskId, visibleTrials }) => {
const taskPassK = sum.per_task_pass_k?.[baseTaskId];
const successCount = visibleTrials.filter(t => getTaskStatusForTrial(t) === 'success').length;
// 目标达成数:judge.success === true(无论是否完美)
const passedCount = visibleTrials.filter(t => getResultForTask(t)?.judge?.success === true).length;
const totalTrials = visibleTrials.length;
const isAnyActive = visibleTrials.some(t => state.selectedTask?.dirName === t.dirName);
const taskName = visibleTrials[0]?.task_name || baseTaskId;
const hasPassedButUnaware = passedCount > successCount;
// Pass@k info (只显示 K>1 的情况,Pass@1 和成功率重复)
let passKHtml = '';
if (taskPassK) {
// 不显示 Pass@1,因为和成功率重复
}
// Trial indicators
const trialIndicators = visibleTrials.map((t, idx) => {
const status = getTaskStatusForTrial(t);
const tags = getTaskIndicators(t);
const isActive = state.selectedTask?.dirName === t.dirName;
let statusText, statusIcon;
switch (status) {
case 'success': statusText = '完美成功'; statusIcon = '✓'; break;
case 'error': statusText = '错误'; statusIcon = '⚠'; break;
case 'unknown': statusText = '未知'; statusIcon = '?'; break;
default: statusText = '失败'; statusIcon = '✗';
}
const tagText = tags.length ? ' · ' + tags.map(k => INDICATOR_LABELS[k].text).join('/') : '';
const tagBadges = tags.length
? `<span class="trial-indicator-tags">${tags.map(k => INDICATOR_LABELS[k].icon).join('')}</span>`
: '';
return `<div class="trial-indicator ${status} ${isActive ? 'active' : ''}"
data-dir="${t.dirName}"
title="Trial ${idx}: ${statusText}${tagText}">
${statusIcon}${tagBadges}
</div>`;
}).join('');
const r0 = getResultForTask(visibleTrials[0]);
const diff0 = r0?.difficulty;
const diffColorMap0 = { L1: '#10b981', L2: '#3b82f6', L3: '#f59e0b', L4: '#ef4444' };
const diffBadge0 = diff0 ? `<span style="padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; background: ${diffColorMap0[diff0] || '#64748b'}20; color: ${diffColorMap0[diff0] || '#64748b'}; flex-shrink: 0;">${diff0}</span>` : '';
return `
<div class="task-group ${isAnyActive ? 'active' : ''}" data-base-id="${baseTaskId}">
<div class="task-group-header" data-dir="${visibleTrials[0].dirName}">
<div class="task-header">
<div class="status-badge ${successCount === totalTrials ? 'success' : successCount > 0 ? 'partial' : 'failed'}"></div>
<div class="task-title" title="${taskName}">${taskName}</div>
${diffBadge0}
</div>
<div class="task-meta">
<span title="任务成功数 (success && clean && finished)" style="color: var(--success);">✓ ${successCount}/${totalTrials}</span>
${hasPassedButUnaware ? `<span title="目标达成数 (judge.success=true)" style="color: var(--warning);">🎯 ${passedCount}/${totalTrials}</span>` : ''}
</div>
</div>
<div class="trial-indicators">
${trialIndicators}
</div>
</div>
`;
}).join('');
html += `<div class="task-count">共 ${filteredGroups.length} 个任务,${visibleTrialCount} 次运行</div>`;
} else {
// Simple view - single trial per task
const filtered = state.tasks.filter(t => {
// Taxonomy filter
if (!taskPassesTaxonomyFilters(t.baseTaskId || t.task_id)) return false;
// Text filter
const text = `${t.task_id} ${t.task_name} ${t.dirName}`.toLowerCase();
if (filter && !text.includes(filter)) return false;
return trialPasses(t);
});
if (filtered.length === 0) {
wrap.innerHTML = `
<div class="empty">
<div class="empty-icon">🔍</div>
<h3>未找到任务</h3>
<p>尝试调整筛选条件</p>
</div>
`;
return;
}
html = filtered.map(t => {
const status = getTaskStatusForTrial(t);
const r = getResultForTask(t);
const steps = r?.execution?.steps ?? '-';
const runtime = r?.execution?.runtime_s?.toFixed(1) ?? '-';
const isActive = state.selectedTask?.dirName === t.dirName;
const diff = r?.difficulty;
const diffColorMap = { L1: '#10b981', L2: '#3b82f6', L3: '#f59e0b', L4: '#ef4444' };
const diffBadge = diff ? `<span style="padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; background: ${diffColorMap[diff] || '#64748b'}20; color: ${diffColorMap[diff] || '#64748b'};">${diff}</span>` : '';
return `
<div class="task-item ${isActive ? 'active' : ''}" data-dir="${t.dirName}">
<div class="task-header">
<div class="status-badge ${status}"></div>
<div class="task-title" title="${t.task_name || t.task_id}">${t.task_name || t.task_id}</div>
${diffBadge}
</div>
<div class="task-meta">
<span>${steps} 步</span>
<span>${runtime}s</span>
</div>
</div>
`;
}).join('');
html += `<div class="task-count">共 ${filtered.length} 个任务</div>`;
}
wrap.innerHTML = html;
// Bind click for task items (simple view)
wrap.querySelectorAll('.task-item').forEach(item => {
item.onclick = function() {
const dirName = this.dataset.dir;
console.log('task-item clicked:', dirName);
const task = state.tasks.find(t => t.dirName === dirName);
if (task) selectTask(task);
};
});
// Bind click for task group headers (grouped view)
wrap.querySelectorAll('.task-group-header').forEach(header => {
header.onclick = function() {
const dirName = this.dataset.dir;
console.log('task-group-header clicked:', dirName);
const task = state.tasks.find(t => t.dirName === dirName);
if (task) selectTask(task);
};
});
// Bind click for trial indicators
wrap.querySelectorAll('.trial-indicator').forEach(indicator => {
indicator.onclick = function(e) {
e.stopPropagation();
const dirName = this.dataset.dir;
console.log('trial-indicator clicked:', dirName);
const task = state.tasks.find(t => t.dirName === dirName);
if (task) selectTask(task);
};
});
}
// 辅助函数:判断是否为真正的成功
// 优先使用后端 is_success(已包含 ABORT 排除逻辑),回退到前端计算
function isRealSuccess(r) {
if (!r) return false;
if (r.is_success !== undefined) return r.is_success === true;
return r.judge?.success === true && r.judge?.clean === true
&& r.execution?.stop_reason === 'COMPLETE';
}
// Get task primary outcome for a specific trial.
// 三类互斥:success(完美成功)/ error(系统/判定异常)/ failed(其余一切,含副作用、超步数、错误完成、ABORT 等)。
// 副作用、超步数、错误完成 等正交细节由 getTaskIndicators 返回。
function getTaskStatusForTrial(task) {
const r = getResultForTask(task);
if (!r) return 'unknown';
const goalSuccess = r.judge?.success === true;
const clean = r.judge?.clean === true;
const isCompleted = r.execution?.stop_reason === 'COMPLETE';
const isAbort = r.execution?.stop_reason === 'ABORT';
const hasSysError = r.execution?.error || r.is_error || r.judge?.judge_error
|| r.judge?.issues?.some(i => 'error' in i);
if (!isAbort && goalSuccess && clean && isCompleted) return 'success';
if (hasSysError) return 'error';
return 'failed';
}
// 任务的正交问题指标,与 overview 三指标 (FC / OT / USE) 对齐(论文 §3.5)。
function getTaskIndicators(task) {
const r = getResultForTask(task);
if (!r) return [];
const tags = [];
const hasSysError = r.execution?.error || r.is_error || r.judge?.judge_error
|| r.judge?.issues?.some(i => 'error' in i);
if (r.judge?.clean === false) tags.push('sideEffect');
// OT: 重算 (truncated && goal_success),避免老 run 中 overdue_termination 字段语义漂移。
const goalSuccess = r.judge?.success === true;
if (r.execution?.truncated === true && goalSuccess) tags.push('overdue');
// FC: stop_reason=COMPLETE 且 episode 未 fully successful。
// 新字段 false_complete 存在时优先使用;历史 run 没有该字段时按 raw fields 重算。
const realSuccess = isRealSuccess(r);
const fcFromRaw = r.execution?.stop_reason === 'COMPLETE' && !realSuccess;
const fcFlag = r.false_complete === undefined ? fcFromRaw : r.false_complete === true;
if (!hasSysError && fcFlag) {
tags.push('falseComplete');
}
return tags;
}
const INDICATOR_LABELS = {
sideEffect: { icon: '🔧', text: '副作用' },
overdue: { icon: '⏱', text: '超步数' },
falseComplete: { icon: '🚫', text: '错误完成' },
};
// Get result for a specific task/trial
function getResultForTask(task) {
// Try to find by task_id + trial_id first
const trialKey = `${task.baseTaskId || task.task_id}_trial_${task.trialId ?? 0}`;
let r = state.results.get(trialKey);
if (r) return r;
// Normalize task_id: convert wechat_OpenNewFriends_i0 to wechat.OpenNewFriends_i0
// and vice versa for matching
const normalizeId = (id) => {
if (!id) return id;
// Pattern: app_TaskName_i0 -> app.TaskName_i0
const match = id.match(/^([a-z]+)_([A-Z].*)$/);
if (match) {
return `${match[1]}.${match[2]}`;
}
return id;
};
const taskIdNormalized = normalizeId(task.task_id);
const baseTaskIdNormalized = normalizeId(task.baseTaskId);
// Fallback to looking through allResults with normalized IDs
r = state.allResults.find(res => {
const resIdNormalized = normalizeId(res.id);
const resTaskIdNormalized = normalizeId(res.task_id);
const idMatches =
res.task_id === task.baseTaskId ||
res.task_id === task.task_id ||
res.id === task.task_id ||
resIdNormalized === taskIdNormalized ||
resIdNormalized === baseTaskIdNormalized ||
resTaskIdNormalized === taskIdNormalized ||
resTaskIdNormalized === baseTaskIdNormalized;
const trialMatches =
res.trial_id === task.trialId ||
(res.trial_id === undefined && task.trialId === 0) ||
(res.trial_id === 0 && task.trialId === undefined);
return idMatches && trialMatches;
});
if (r) return r;
// Final fallback
return state.results.get(task.task_id) || state.results.get(task.baseTaskId);
}
function renderDetail() {
if (!state.selectedTask) {
el('detailEmpty').style.display = 'block';
el('detailContent').style.display = 'none';
return;
}
el('detailEmpty').style.display = 'none';
el('detailContent').style.display = 'block';
const task = state.selectedTask;
const r = getResultForTask(task);
const isSuccess = isRealSuccess(r);
const sum = state.summary || {};
const meta = state.meta || {};
const taskPassK = sum.per_task_pass_k?.[task.baseTaskId || task.task_id];
const repeatN = sum.repeat_n ?? meta.repeat_n ?? 1;
const trialLabel = repeatN > 1 ? ` (Trial ${task.trialId ?? 0})` : '';
// 检查是否是"达成但未意识"的情况(success && clean && !finished
const isPassedButUnaware = r?.judge?.success === true && r?.judge?.clean === true && r?.execution?.finished === false;
let html = `
<div class="card">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<div>
<h2 style="font-size: 20px; font-weight: 700; margin-bottom: 4px;">
${task.task_name || task.task_id}${trialLabel}
</h2>
<p style="font-size: 13px; color: var(--text-muted); font-family: var(--mono);">
${task.baseTaskId || task.task_id}
${repeatN > 1 ? `<span style="color: var(--primary);"> • Trial ${task.trialId ?? 0}</span>` : ''}
</p>
</div>
<span class="badge ${isSuccess ? 'success' : isPassedButUnaware ? '' : 'failed'}" style="font-size: 14px; padding: 8px 18px; ${isPassedButUnaware ? 'background: var(--warning-light); color: var(--warning);' : ''}">
${isSuccess ? '✓ 成功' : isPassedButUnaware ? '! 达成但未意识' : '✗ 失败'}
</span>
</div>
${isPassedButUnaware ? `
<div style="margin-top: 12px; padding: 12px 16px; background: var(--warning-light); border: 1px solid var(--warning); border-radius: var(--radius-sm);">
<div style="display: flex; align-items: center; gap: 8px; color: var(--warning); font-weight: 600; margin-bottom: 4px;">
<span>⚠️</span> 任务目标已达成,但模型未意识到完成
</div>
<div style="font-size: 13px; color: var(--text-secondary);">
judge.success=true 且 judge.clean=true 表示任务目标已达成且无副作用,但 stop_reason=${r?.execution?.stop_reason || 'MAX_STEPS'}(非 COMPLETE),表示模型没有主动以 COMPLETE 结束任务。
</div>
</div>
` : ''}
</div>
`;
// Pass@K for this task
if (taskPassK) {
const passKKeys = Object.keys(taskPassK).filter(k => k.startsWith('pass@')).sort((a, b) => {
return parseInt(a.split('@')[1]) - parseInt(b.split('@')[1]);
});
html += `
<div class="card">
<div class="card-title">
<div class="icon purple">🎯</div>
任务 Pass@K 统计
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">试验次数</div>
<div class="info-value">${taskPassK.trials}</div>
</div>
<div class="info-item">
<div class="info-label">成功次数</div>
<div class="info-value">${taskPassK.successes}</div>
</div>
<div class="info-item">
<div class="info-label">成功率</div>
<div class="info-value">${(taskPassK.successes / taskPassK.trials * 100).toFixed(1)}%</div>
</div>
</div>
<div class="pass-k-grid" style="margin-top: 16px;">
${passKKeys.map(k => {
const val = (taskPassK[k] * 100).toFixed(1);
return `
<div class="pass-k-item">
<div class="pass-k-label">${k}</div>
<div class="pass-k-value">${val}%</div>
</div>
`;
}).join('')}
</div>
</div>
`;
// Show individual trial results - get from task groups for trajectory access
const taskGroup = state.taskGroups.get(task.baseTaskId || task.task_id) || [];
const baseTaskId = task.baseTaskId || task.task_id;
if (taskGroup.length > 1) {
html += `
<div class="card">
<div class="card-title">
<div class="icon green">📊</div>
分 Trial 结果 (${taskGroup.length} 次运行)
</div>
<div style="display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; font-size: 11px;">
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 10px; height: 10px; background: var(--success); border-radius: 2px;"></span>
完美成功
</span>
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 10px; height: 10px; background: var(--error); border-radius: 2px;"></span>
失败
</span>
<span style="display: flex; align-items: center; gap: 4px;">
<span style="width: 10px; height: 10px; background: var(--warning); border-radius: 2px;"></span>
错误
</span>
<span style="color: var(--text-muted);">指标:🔧 副作用 / ⏱ 超步数 / 🚫 错误完成</span>
</div>
<div class="trial-grid">
${taskGroup.map((t, idx) => {
const trialResult = getResultForTask(t);
const status = getTaskStatusForTrial(t);
const tags = getTaskIndicators(t);
const isCurrentTrial = task.dirName === t.dirName;
let statusEmoji, statusText;
switch (status) {
case 'success': statusEmoji = '✅'; statusText = '完美成功'; break;
case 'error': statusEmoji = '⚠️'; statusText = '错误'; break;
case 'unknown': statusEmoji = '?'; statusText = '未知'; break;
default: statusEmoji = '❌'; statusText = '失败';
}
const tagSuffix = tags.length ? ' · ' + tags.map(k => INDICATOR_LABELS[k].text).join('/') : '';
const tagIcons = tags.map(k => INDICATOR_LABELS[k].icon).join('');
return `
<div class="trial-item ${status} ${isCurrentTrial ? 'current' : ''}"
onclick="selectTrial('${baseTaskId}', ${idx})"
title="Trial ${idx}: ${statusText}${tagSuffix}&#10;步数: ${trialResult?.execution?.steps ?? '-'}&#10;耗时: ${trialResult?.execution?.runtime_s?.toFixed(1) ?? '-'}s${isCurrentTrial ? '&#10;(当前查看)' : ''}">
<div class="trial-num">Trial ${idx}</div>
<div class="trial-status">${statusEmoji}${tagIcons ? `<span style="font-size: 10px; margin-left: 2px;">${tagIcons}</span>` : ''}</div>
${isCurrentTrial ? '<div style="font-size: 10px; color: var(--primary);">当前</div>' : ''}
</div>
`;
}).join('')}
</div>
<div style="margin-top: 16px; font-size: 12px; color: var(--text-muted);">
💡 点击查看具体 Trial 的轨迹和详细信息
</div>
</div>
`;
// Trial statistics summary - use results from resultsByTask
const trialResults = getTrialsForTask(baseTaskId);
if (trialResults.length > 0) {
const successTrials = trialResults.filter(t => isRealSuccess(t));
const goalSuccessTrials = trialResults.filter(t => t.judge?.success === true);
const passedButUnawareTrials = goalSuccessTrials.filter(t => t.judge?.clean === true && t.execution?.finished !== true);
const sideEffectTrials = goalSuccessTrials.filter(t => t.judge?.clean !== true && t.execution?.finished === true);
const doubleIssueTrials = goalSuccessTrials.filter(t => t.judge?.clean !== true && t.execution?.finished !== true);
const avgSteps = trialResults.reduce((sum, t) => sum + (t.execution?.steps || 0), 0) / trialResults.length;
const avgTime = trialResults.reduce((sum, t) => sum + (t.execution?.runtime_s || 0), 0) / trialResults.length;
// Calculate step variance
const stepsArr = trialResults.map(t => t.execution?.steps || 0);
const minSteps = Math.min(...stepsArr);
const maxSteps = Math.max(...stepsArr);
html += `
<div class="card">
<div class="card-title">
<div class="icon blue">📈</div>
Trial 统计汇总
</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label" style="color: var(--success);">✓ 任务成功</div>
<div class="info-value" style="color: var(--success);">${successTrials.length}</div>
</div>
<div class="info-item">
<div class="info-label" style="color: #60a5fa;">🎯 目标达成</div>
<div class="info-value" style="color: #60a5fa;">${goalSuccessTrials.length}</div>
</div>
${passedButUnawareTrials.length > 0 ? `
<div class="info-item">
<div class="info-label" style="color: var(--warning);">⚠️ 未意识</div>
<div class="info-value" style="color: var(--warning);">${passedButUnawareTrials.length}</div>
</div>
` : ''}
${sideEffectTrials.length > 0 ? `
<div class="info-item">
<div class="info-label" style="color: #f97316;">🔧 有副作用</div>
<div class="info-value" style="color: #f97316;">${sideEffectTrials.length}</div>
</div>
` : ''}
${doubleIssueTrials.length > 0 ? `
<div class="info-item">
<div class="info-label" style="color: #c2410c;">⚠🔧 双重问题</div>
<div class="info-value" style="color: #c2410c;">${doubleIssueTrials.length}</div>
</div>
` : ''}
<div class="info-item">
<div class="info-label">失败 Trial</div>
<div class="info-value" style="color: var(--error);">${trialResults.filter(t => !t.judge?.success).length}</div>
</div>
<div class="info-item">
<div class="info-label">平均步数</div>
<div class="info-value">${avgSteps.toFixed(1)}</div>
</div>
<div class="info-item">
<div class="info-label">步数范围</div>
<div class="info-value">${minSteps} - ${maxSteps}</div>
</div>
<div class="info-item">
<div class="info-label">平均耗时</div>
<div class="info-value">${avgTime.toFixed(1)}s</div>
</div>
</div>
${successTrials.length > 0 ? `
<div style="margin-top: 16px;">
<div class="info-label" style="margin-bottom: 8px; color: var(--success);">✓ 成功 Trial 详情</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${successTrials.map((t, i) => `
<span class="badge success" title="Trial ${t.trial_id ?? i}">
T${t.trial_id ?? i}: ${t.execution?.steps ?? '-'}
</span>
`).join('')}
</div>
</div>
` : ''}
${passedButUnawareTrials.length > 0 ? `
<div style="margin-top: 16px;">
<div class="info-label" style="margin-bottom: 8px; color: var(--warning);">⚠️ 未意识 Trial 详情</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${passedButUnawareTrials.map((t, i) => `
<span class="badge" style="background: var(--warning-light); color: var(--warning);" title="Trial ${t.trial_id ?? i}: 目标达成但未主动终止 (${t.execution?.stop_reason || 'MAX_STEPS'})">
T${t.trial_id ?? i}: ${t.execution?.steps ?? '-'}
</span>
`).join('')}
</div>
</div>
` : ''}
${sideEffectTrials.length > 0 ? `
<div style="margin-top: 16px;">
<div class="info-label" style="margin-bottom: 8px; color: #f97316;">🔧 有副作用 Trial 详情</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${sideEffectTrials.map((t, i) => `
<span class="badge" style="background: #fff7ed; color: #f97316;" title="Trial ${t.trial_id ?? i}: 目标达成但有副作用 (warnings: ${t.judge?.warnings?.length ?? 0})">
T${t.trial_id ?? i}: ${t.execution?.steps ?? '-'}
</span>
`).join('')}
</div>
</div>
` : ''}
${doubleIssueTrials.length > 0 ? `
<div style="margin-top: 16px;">
<div class="info-label" style="margin-bottom: 8px; color: #c2410c;">⚠🔧 双重问题 Trial 详情</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${doubleIssueTrials.map((t, i) => `
<span class="badge" style="background: #fed7aa; color: #c2410c;" title="Trial ${t.trial_id ?? i}: 有副作用且未主动终止">
T${t.trial_id ?? i}: ${t.execution?.steps ?? '-'}
</span>
`).join('')}
</div>
</div>
` : ''}
${trialResults.filter(t => !t.judge?.success).length > 0 ? `
<div style="margin-top: 16px;">
<div class="info-label" style="margin-bottom: 8px;">✗ 失败 Trial 详情</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${trialResults.filter(t => !t.judge?.success).map((t, i) => `
<span class="badge failed" title="Trial ${t.trial_id ?? i}: 目标未达成 (${t.execution?.stop_reason || ''})">
T${t.trial_id ?? i}: ${t.execution?.steps ?? '-'}
</span>
`).join('')}
</div>
</div>
` : ''}
</div>
`;
}
}
}
if (r) {
const ex = r.execution || {};
html += `
<div class="card">
<div class="card-title">
<div class="icon blue">📋</div>
执行信息 ${taskPassK ? '(当前选中的 Trial)' : ''}
</div>
<div class="info-grid">
${[
['任务ID', r.id],
['Suite', r.suite],
['涉及App', r.apps?.join(', ')],
['Trial ID', r.trial_id ?? '-'],
['步数', ex.steps],
['完成状态', ex.finished ? '✓ 已完成' : '✗ 未完成'],
['截断', ex.truncated ? '是' : '否'],
['停止原因', ex.stop_reason],
['提交答案', ex.agent_answer ? '✓ 已提交' : null],
['运行时间', ex.runtime_s?.toFixed(2) + 's'],
['False Complete', (() => {
const hasSysError = r.execution?.error || r.is_error || r.judge?.judge_error
|| r.judge?.issues?.some(i => 'error' in i);
// 新字段优先;缺失时按 raw fields 重算(论文 §3.5)。
const fcFromRaw = ex.stop_reason === 'COMPLETE' && !isRealSuccess(r) && !hasSysError;
const fc = r.false_complete === undefined ? fcFromRaw : r.false_complete === true;
return fc ? '⚠ 是(Agent 声称完成但未 fully successful' : null;
})()],
['Overdue Termination', (r.execution?.truncated === true && r.judge?.success === true)
? '⚠ 是(达成目标却被 step budget / loop 截断)'
: null],
['开始时间', formatTime(r.start_time)],
['结束时间', formatTime(r.end_time)],
].filter(([k, v]) => v != null && v !== '').map(([k, v]) => `
<div class="info-item">
<div class="info-label">${k}</div>
<div class="info-value">${v ?? '-'}</div>
</div>
`).join('')}
</div>
</div>
`;
// Task Taxonomy Card
if (r.difficulty || r.scope || r.objective || r.composition || r.capabilities?.length) {
const diffColors = { L1: '#10b981', L2: '#3b82f6', L3: '#f59e0b', L4: '#ef4444' };
const scopeColors = { S1: '#10b981', S2: '#3b82f6', S3: '#8b5cf6' };
const objColors = { operate: '#3b82f6', query: '#10b981', hybrid: '#f59e0b', vague: '#94a3b8', safety: '#ef4444' };
const compColors = { atomic: '#94a3b8', sequential: '#3b82f6', transfer: '#10b981', deep_dive: '#8b5cf6' };
const capColors = { nav: '#6366f1', search: '#0ea5e9', query: '#10b981', reasoning: '#f59e0b', create: '#8b5cf6', transfer: '#ec4899', social: '#f97316', finance: '#14b8a6' };
const makeBadge = (label, value, colorMap) => {
const color = colorMap?.[value] || '#64748b';
return `<span style="display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; background: ${color}18; color: ${color}; border: 1px solid ${color}40;">${label ? `<span style="font-weight: 400; opacity: 0.7;">${label}</span> ` : ''}${value}</span>`;
};
html += `
<div class="card">
<div class="card-title">
<div class="icon purple">🏷️</div>
任务分类
</div>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
${r.difficulty ? makeBadge('难度', r.difficulty, diffColors) : ''}
${r.scope ? makeBadge('范围', r.scope, scopeColors) : ''}
${r.objective ? makeBadge('目标', r.objective, objColors) : ''}
${r.composition ? makeBadge('组合', r.composition, compColors) : ''}
</div>
${r.capabilities?.length ? `
<div style="margin-top: 12px;">
<div class="info-label" style="margin-bottom: 6px;">所需能力</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
${r.capabilities.map(c => makeBadge('', c, capColors)).join('')}
</div>
</div>
` : ''}
</div>
`;
}
if (ex.agent_answer || ex.agent_message) {
html += `
<div class="card">
<div class="card-title">
<div class="icon purple">💬</div>
Agent 输出
</div>
${ex.agent_answer ? `
<div style="margin-bottom: 12px;">
<div class="info-label">📝 提交的答案 (ANSWER)</div>
<div class="step-thought" style="border-left: 3px solid var(--primary); padding-left: 12px;">${escapeHtml(ex.agent_answer)}</div>
</div>
` : ''}
${ex.agent_message ? `
<div>
<div class="info-label">💬 终止说明 (COMPLETE/ABORT)</div>
<div class="step-thought">${escapeHtml(ex.agent_message)}</div>
</div>
` : ''}
</div>
`;
}
// Judge
if (r.judge) {
const j = r.judge;
const isGoalSuccess = j.success === true;
const isClean = j.clean === true;
const isCompleted = r.execution?.stop_reason === 'COMPLETE';
const isTaskSuccess = isRealSuccess(r); // 与顶部徽章同源,后端 is_success 为准
// 判断失败原因类别
let failureCategory = null;
if (isGoalSuccess && !isTaskSuccess) {
if (isClean && !isCompleted) failureCategory = 'unaware'; // 未意识
else if (!isClean && isCompleted) failureCategory = 'sideEffect'; // 有副作用
else if (!isClean && !isCompleted) failureCategory = 'double'; // 双重问题
}
html += `
<div class="card">
<div class="card-title">
<div class="icon green">⚖️</div>
评判结果
</div>
<!-- 最终结果 -->
<div style="padding: 16px; border-radius: var(--radius-sm); text-align: center; margin-bottom: 20px; ${isTaskSuccess ? 'background: var(--success-light); border: 2px solid var(--success);' : 'background: var(--error-light); border: 2px solid var(--error);'}">
<div style="font-size: 28px; font-weight: 800; color: ${isTaskSuccess ? 'var(--success)' : 'var(--error)'};">${isTaskSuccess ? '✓ 任务成功' : '✗ 任务失败'}</div>
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">is_success = stop_reason==COMPLETE AND judge.success AND judge.clean</div>
</div>
<!-- 三个独立条件 -->
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px;">
<div style="padding: 12px; border-radius: var(--radius-sm); text-align: center; ${isGoalSuccess ? 'background: #dbeafe; border: 2px solid #60a5fa;' : 'background: var(--error-light); border: 2px solid var(--error);'}">
<div style="font-size: 20px; font-weight: 800; color: ${isGoalSuccess ? '#60a5fa' : 'var(--error)'};">${isGoalSuccess ? '✓' : '✗'}</div>
<div style="font-size: 12px; font-weight: 600; margin-top: 4px;">目标达成</div>
<div style="font-size: 10px; color: var(--text-muted);">judge.success</div>
</div>
<div style="padding: 12px; border-radius: var(--radius-sm); text-align: center; ${isClean ? 'background: #fef3c7; border: 2px solid #f59e0b;' : 'background: #fff7ed; border: 2px solid #f97316;'}">
<div style="font-size: 20px; font-weight: 800; color: ${isClean ? '#f59e0b' : '#f97316'};">${isClean ? '✓' : '✗'}</div>
<div style="font-size: 12px; font-weight: 600; margin-top: 4px;">无副作用</div>
<div style="font-size: 10px; color: var(--text-muted);">judge.clean</div>
</div>
<div style="padding: 12px; border-radius: var(--radius-sm); text-align: center; ${isCompleted ? 'background: var(--success-light); border: 2px solid var(--success);' : 'background: var(--warning-light); border: 2px solid var(--warning);'}">
<div style="font-size: 20px; font-weight: 800; color: ${isCompleted ? 'var(--success)' : 'var(--warning)'};">${isCompleted ? '✓' : '✗'}</div>
<div style="font-size: 12px; font-weight: 600; margin-top: 4px;">主动完成</div>
<div style="font-size: 10px; color: var(--text-muted);">stop_reason === COMPLETE</div>
</div>
</div>
${j.progress !== undefined && j.progress < 1.0 ? `
<div style="margin-bottom: 20px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 6px;">
<span style="font-size: 13px; font-weight: 600; color: var(--text-secondary);">目标完成进度</span>
<span style="font-size: 16px; font-weight: 800; color: ${j.progress >= 0.8 ? 'var(--success)' : j.progress >= 0.5 ? 'var(--warning)' : 'var(--error)'};">${(j.progress * 100).toFixed(0)}%</span>
</div>
<div style="height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden;">
<div style="width: ${j.progress * 100}%; height: 100%; background: ${j.progress >= 0.8 ? 'var(--success)' : j.progress >= 0.5 ? 'var(--warning)' : 'var(--error)'}; border-radius: 4px; transition: width 0.3s;"></div>
</div>
</div>
` : ''}
${failureCategory === 'unaware' ? `
<div style="padding: 12px 16px; background: var(--warning-light); border: 1px solid var(--warning); border-radius: var(--radius-sm); margin-bottom: 20px;">
<div style="display: flex; align-items: center; gap: 8px; color: var(--warning); font-weight: 600;">
<span>⚠️</span> 达成但未意识
</div>
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">
目标已达成且无副作用,但模型没有意识到完成,未主动调用 COMPLETE 终止任务。
</div>
</div>
` : ''}
${failureCategory === 'sideEffect' ? `
<div style="padding: 12px 16px; background: #fff7ed; border: 1px solid #f97316; border-radius: var(--radius-sm); margin-bottom: 20px;">
<div style="display: flex; align-items: center; gap: 8px; color: #f97316; font-weight: 600;">
<span>🔧</span> 达成但有副作用
</div>
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">
目标已达成且主动终止,但执行过程中产生了意外的状态变化(见下方 warnings)。
</div>
</div>
` : ''}
${failureCategory === 'double' ? `
<div style="padding: 12px 16px; background: #fed7aa; border: 1px solid #c2410c; border-radius: var(--radius-sm); margin-bottom: 20px;">
<div style="display: flex; align-items: center; gap: 8px; color: #c2410c; font-weight: 600;">
<span>⚠️🔧</span> 双重问题
</div>
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">
目标已达成,但既有副作用又未主动终止。
</div>
</div>
` : ''}
`;
if (j.issues?.length) {
html += '<h4 style="font-size: 14px; margin: 20px 0 12px;">检查项</h4>';
j.issues.forEach(issue => {
if (issue.field !== undefined) {
html += `
<div class="judge-item ${issue.passed ? 'passed' : 'failed'}">
<div class="judge-field">${escapeHtml(issue.field)}</div>
<div class="judge-values">
<div><span>期望:</span> ${formatValue(issue.expected)}</div>
<div><span>实际:</span> ${formatValue(issue.actual)}</div>
</div>
</div>
`;
} else if (issue.reason) {
html += `
<div class="judge-item failed">
<div class="judge-field" style="color: var(--error);">原因</div>
<div class="judge-values">
<div>${escapeHtml(String(issue.reason))}</div>
</div>
</div>
`;
} else if (issue.error) {
html += `
<div class="judge-item failed" style="border-left-color: var(--warning);">
<div class="judge-field" style="color: var(--warning);">错误</div>
<div class="judge-values">
<div>${escapeHtml(String(issue.error))}</div>
</div>
</div>
`;
} else {
html += `
<div class="judge-item failed">
<div class="judge-values">
<div>${formatValue(issue)}</div>
</div>
</div>
`;
}
});
}
html += '</div>';
if (j.warnings?.length) {
html += `
<div class="card warnings-card">
<div class="card-title">⚠️ 警告 (${j.warnings.length})</div>
${j.warnings.map(w => `
<div class="warning-item">${w.field}: ${formatValue(w.before)}${formatValue(w.after)}</div>
`).join('')}
</div>
`;
}
}
html += `
<details>
<summary>查看完整 result JSON</summary>
<div class="content">
<div class="code">${JSON.stringify(r, null, 2)}</div>
</div>
</details>
`;
} else {
html += `
<div class="card">
<p style="color: var(--text-muted); text-align: center; padding: 20px;">
results.jsonl 中未找到该任务的结果
</p>
</div>
`;
}
el('detailContent').innerHTML = html;
}
function renderTrajectory() {
if (!state.selectedTask) {
el('trajectoryEmpty').style.display = 'block';
el('trajectoryContent').style.display = 'none';
return;
}
el('trajectoryEmpty').style.display = 'none';
el('trajectoryContent').style.display = 'block';
if (state.trajectory.length === 0) {
el('trajectoryContent').innerHTML = `
<div class="empty">
<div class="empty-icon">🎯</div>
<h3>无轨迹数据</h3>
<p>该任务没有轨迹记录</p>
</div>
`;
return;
}
// Calculate action type distribution for this task
const taskActionTypes = {};
for (const step of state.trajectory) {
const actionType = step.action_type || 'UNKNOWN';
taskActionTypes[actionType] = (taskActionTypes[actionType] || 0) + 1;
}
const totalTaskActions = Object.values(taskActionTypes).reduce((a, b) => a + b, 0);
const sortedTaskActions = Object.entries(taskActionTypes).sort((a, b) => b[1] - a[1]);
let html = `
<div class="card" style="margin-bottom: 20px;">
<div class="card-title">
<div class="icon purple">🎬</div>
本任务动作分布 (${totalTaskActions} 步)
</div>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
${sortedTaskActions.map(([action, count]) => {
const pct = (count / totalTaskActions * 100).toFixed(0);
return `
<div style="background: var(--bg); padding: 8px 14px; border-radius: 20px; font-size: 12px;">
<strong>${action}</strong>: ${count} (${pct}%)
</div>
`;
}).join('')}
</div>
</div>
`;
// Check if this is a multi-trial run
const sum = state.summary || {};
const meta = state.meta || {};
const repeatN = sum.repeat_n ?? meta.repeat_n ?? 1;
const trialInfo = repeatN > 1 ? ` - Trial ${state.selectedTask.trialId ?? 0}` : '';
const taskResult = getResultForTask(state.selectedTask);
// 计算详细状态
const goalSuccess = taskResult?.judge?.success === true;
const clean = taskResult?.judge?.clean === true;
const finished = taskResult?.execution?.finished === true;
let resultStatus, resultStatusColor;
if (goalSuccess && clean && finished) {
resultStatus = '✅ 完美成功'; resultStatusColor = 'var(--success)';
} else if (goalSuccess && clean && !finished) {
resultStatus = '⚠️ 未意识'; resultStatusColor = 'var(--warning)';
} else if (goalSuccess && !clean && finished) {
resultStatus = '🔧 有副作用'; resultStatusColor = '#f97316';
} else if (goalSuccess && !clean && !finished) {
resultStatus = '⚠️🔧 双重问题'; resultStatusColor = '#c2410c';
} else {
resultStatus = '❌ 失败'; resultStatusColor = 'var(--error)';
}
html += `
<div class="card" style="margin-bottom: 20px; padding: 16px;">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<div>
<h3 style="font-size: 18px; font-weight: 700; margin-bottom: 4px;">
${state.selectedTask.task_name || state.selectedTask.task_id}${trialInfo}
</h3>
<p style="font-size: 12px; color: var(--text-muted);">
<span style="color: ${resultStatusColor};">${resultStatus}</span> • ${state.trajectory.length} 步 • ${taskResult?.execution?.runtime_s?.toFixed(1) ?? '-'}s
</p>
</div>
${repeatN > 1 ? `
<div style="display: flex; gap: 6px;">
${(state.taskGroups.get(state.selectedTask.baseTaskId || state.selectedTask.task_id) || []).map((t, idx) => {
const isCurrent = t.dirName === state.selectedTask.dirName;
const status = getTaskStatusForTrial(t);
const tags = getTaskIndicators(t);
const titleMap = { success: '完美成功', error: '错误', unknown: '未知' };
const statusTitle = titleMap[status] || '失败';
const tagSuffix = tags.length ? ' · ' + tags.map(k => INDICATOR_LABELS[k].text).join('/') : '';
const tagBadges = tags.length
? `<span class="trial-indicator-tags">${tags.map(k => INDICATOR_LABELS[k].icon).join('')}</span>`
: '';
return `<div class="trial-indicator ${status} ${isCurrent ? 'active' : ''}"
onclick="selectTrial('${state.selectedTask.baseTaskId || state.selectedTask.task_id}', ${idx})"
title="Trial ${idx}: ${statusTitle}${tagSuffix}">
${idx}${tagBadges}
</div>`;
}).join('')}
</div>
` : ''}
</div>
</div>
`;
html += `
<div class="trajectory-header">
<h3>轨迹步骤 (${state.trajectory.length} 步)</h3>
<div style="display: flex; align-items: center;">
<button class="full-history-btn" onclick="showFullHistory()">
📝 查看完整对话历史
</button>
<div class="trajectory-nav">
<button class="nav-btn" onclick="navigateStep(-1)"></button>
<button class="nav-btn" onclick="navigateStep(1)"></button>
</div>
</div>
</div>
<div class="trajectory-strip" id="trajStrip">
${state.trajectory.map((step, idx) => `
<div class="thumb ${state.selectedStepIdx === idx ? 'active' : ''}" data-idx="${idx}">
<img id="thumb-${idx}" alt="Step ${step.step ?? idx}" loading="lazy">
<div class="thumb-label">#${step.step ?? idx}</div>
</div>
`).join('')}
</div>
<div id="stepsContainer">
${state.trajectory.map((step, idx) => {
const hasAnnot = !!step.screenshot_annotated;
const hasResponse = !!step.model_response_path;
return `
<div class="step-card" id="step-${idx}">
<div class="step-header">
<span class="step-number">Step ${step.step ?? idx}</span>
<span class="step-action">${step.action_type ?? 'UNKNOWN'}</span>
</div>
<div class="step-body">
<div class="step-images">
<div class="step-img-wrap">
<img class="step-img" id="stepImg-${idx}-raw" alt="原图" data-idx="${idx}" data-type="raw">
<div class="img-label">原图</div>
</div>
${hasAnnot ? `
<div class="step-img-wrap">
<img class="step-img" id="stepImg-${idx}-annot" alt="标注" data-idx="${idx}" data-type="annot">
<div class="img-label">标注</div>
</div>
` : ''}
</div>
<div class="step-text">
${hasResponse ? `
<div class="response-section">
<div class="response-header">
<span class="response-title">📄 模型完整输出</span>
<span class="response-file">${step.model_response_path}</span>
</div>
<div class="response-content" id="response-${idx}">
<div class="loading-text">加载中...</div>
</div>
</div>
` : `
<div class="step-thought">${escapeHtml(step.thought || '(无思考内容)')}</div>
`}
<div class="step-meta-info">
<div class="step-route">
📍 ${step.route?.app || ''}${step.route?.path || ''}
</div>
<div class="step-action-data">
🎯 ${step.action_type || 'UNKNOWN'}
${step.action_data?.point ? `: 点击 (${step.action_data.point.join(', ')})` : ''}
${step.action_data?.start && step.action_data?.end ? `: 滑动 (${step.action_data.start.join(',')}) → (${step.action_data.end.join(',')})` : ''}
${step.action_data?.text ? `: 输入 "${step.action_data.text}"` : ''}
${step.action_data?.value && step.action_type === 'ANSWER' ? `: 答案 "${step.action_data.value}"` : ''}
${step.action_data?.return ? `: 返回值` : ''}
</div>
</div>
${step.action_data?.return ? `
<div class="return-value">
<div class="return-label">返回值:</div>
<div class="return-content">${escapeHtml(String(step.action_data.return))}</div>
</div>
` : ''}
<div class="prompt-section" id="prompt-section-${idx}">
<div class="prompt-header" onclick="togglePromptSection(${idx})">
<span class="prompt-title">📝 Prompt (点击展开)</span>
<span class="prompt-toggle">▶</span>
</div>
<div class="prompt-content" id="prompt-content-${idx}">
<div class="loading-text">加载中...</div>
</div>
</div>
</div>
</div>
</div>
`;
}).join('')}
</div>
`;
el('trajectoryContent').innerHTML = html;
// Load images, response text, and prompt
state.trajectory.forEach(async (step, idx) => {
try {
const media = await getMedia(idx);
const thumbImg = document.querySelector(`#trajStrip .thumb[data-idx="${idx}"] img`);
if (thumbImg) thumbImg.src = media.annotUrl || media.rawUrl || '';
const rawImg = el(`stepImg-${idx}-raw`);
if (rawImg && media.rawUrl) rawImg.src = media.rawUrl;
const annotImg = el(`stepImg-${idx}-annot`);
if (annotImg && media.annotUrl) annotImg.src = media.annotUrl;
// Load response text
const responseEl = el(`response-${idx}`);
if (responseEl && media.responseText) {
responseEl.innerHTML = `<pre class="response-pre">${escapeHtml(media.responseText)}</pre>`;
} else if (responseEl) {
responseEl.innerHTML = `<div class="no-response">无响应内容</div>`;
}
// Load prompt
const promptContentEl = el(`prompt-content-${idx}`);
if (promptContentEl) {
if (media.prompt && Array.isArray(media.prompt)) {
const stats = getPromptStats(media.prompt);
promptContentEl.innerHTML = `
<div class="prompt-stats">
<span class="prompt-stat">共 <strong>${stats.total}</strong> 条消息</span>
<span class="prompt-stat">System: <strong>${stats.system}</strong></span>
<span class="prompt-stat">User: <strong>${stats.user}</strong></span>
<span class="prompt-stat">Assistant: <strong>${stats.assistant}</strong></span>
</div>
<div class="chat-messages">
${renderChatMessages(media.prompt, `step_${idx}`)}
</div>
`;
// Update header text
const promptSection = el(`prompt-section-${idx}`);
if (promptSection) {
const titleEl = promptSection.querySelector('.prompt-title');
if (titleEl) titleEl.textContent = `📝 Prompt (${stats.total} 条消息)`;
}
} else {
promptContentEl.innerHTML = `<div class="no-response" style="padding: 16px;">无 Prompt 数据</div>`;
const promptSection = el(`prompt-section-${idx}`);
if (promptSection) {
const titleEl = promptSection.querySelector('.prompt-title');
if (titleEl) titleEl.textContent = `📝 Prompt (无数据)`;
}
}
}
} catch (e) {
console.error(`Failed to load media for step ${idx}:`, e);
const responseEl = el(`response-${idx}`);
if (responseEl) {
responseEl.innerHTML = `<div class="no-response" style="color: var(--error);">加载失败: ${e.message}</div>`;
}
}
});
// Bind thumb clicks
document.querySelectorAll('#trajStrip .thumb').forEach(thumb => {
thumb.addEventListener('click', () => {
const idx = parseInt(thumb.dataset.idx);
state.selectedStepIdx = idx;
document.querySelectorAll('#trajStrip .thumb').forEach(t => t.classList.remove('active'));
thumb.classList.add('active');
document.getElementById(`step-${idx}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
// Bind image clicks for lightbox
document.querySelectorAll('.step-img').forEach(img => {
img.addEventListener('click', async () => {
const idx = parseInt(img.dataset.idx);
const type = img.dataset.type;
await openLightboxForStep(idx, type);
});
});
}
function navigateStep(delta) {
const newIdx = state.selectedStepIdx + delta;
if (newIdx >= 0 && newIdx < state.trajectory.length) {
state.selectedStepIdx = newIdx;
document.querySelectorAll('#trajStrip .thumb').forEach((t, i) => {
t.classList.toggle('active', i === newIdx);
});
document.getElementById(`step-${newIdx}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
async function renderAllImages() {
if (!state.selectedTask) {
el('allImagesEmpty').style.display = 'block';
el('allImagesContent').style.display = 'none';
return;
}
el('allImagesEmpty').style.display = 'none';
el('allImagesContent').style.display = 'block';
if (state.trajectory.length === 0) {
el('allImagesContent').innerHTML = `
<div class="empty">
<div class="empty-icon">🖼️</div>
<h3>无截图</h3>
<p>该任务没有截图记录</p>
</div>
`;
return;
}
// Build image list
lightboxImages = [];
for (let i = 0; i < state.trajectory.length; i++) {
const step = state.trajectory[i];
const media = await getMedia(i);
if (media.rawUrl) {
lightboxImages.push({ url: media.rawUrl, label: `Step ${step.step ?? i} - 原图`, stepIdx: i, type: 'raw' });
}
if (media.annotUrl) {
lightboxImages.push({ url: media.annotUrl, label: `Step ${step.step ?? i} - 标注`, stepIdx: i, type: 'annot' });
}
}
// Check if multi-trial
const sum = state.summary || {};
const meta = state.meta || {};
const repeatN = sum.repeat_n ?? meta.repeat_n ?? 1;
const trialInfo = repeatN > 1 ? ` (Trial ${state.selectedTask.trialId ?? 0})` : '';
let html = `
<div class="images-header">
<h3>全部截图${trialInfo}</h3>
<span class="images-count">${lightboxImages.length} 张</span>
</div>
<div class="images-grid">
${lightboxImages.map((img, idx) => `
<div class="image-card" data-idx="${idx}">
<img src="${img.url}" alt="${img.label}" loading="lazy">
<div class="image-card-info">${img.label}</div>
</div>
`).join('')}
</div>
`;
el('allImagesContent').innerHTML = html;
// Bind click
document.querySelectorAll('#allImagesContent .image-card').forEach(card => {
card.addEventListener('click', () => {
lightboxIndex = parseInt(card.dataset.idx);
showLightbox();
});
});
}
// =========== Prompt Functions ===========
function togglePromptSection(idx) {
const section = el(`prompt-section-${idx}`);
if (section) {
section.classList.toggle('expanded');
}
}
async function showFullHistory() {
// Use the last step's prompt which contains the full conversation history
const lastIdx = state.trajectory.length - 1;
if (lastIdx < 0) {
el('promptModalBody').innerHTML = '<div class="no-response" style="padding: 20px; text-align: center;">无轨迹数据</div>';
el('promptModal').classList.add('open');
return;
}
const media = await getMedia(lastIdx);
if (media.prompt && Array.isArray(media.prompt)) {
const stats = getPromptStats(media.prompt);
const lastStep = state.trajectory[lastIdx];
el('promptModalBody').innerHTML = `
<div class="prompt-stats">
<span class="prompt-stat">来源: <strong>Step ${lastStep.step ?? lastIdx}</strong> (最后一步)</span>
<span class="prompt-stat">共 <strong>${stats.total}</strong> 条消息</span>
<span class="prompt-stat">System: <strong>${stats.system}</strong></span>
<span class="prompt-stat">User: <strong>${stats.user}</strong></span>
<span class="prompt-stat">Assistant: <strong>${stats.assistant}</strong></span>
</div>
<div class="chat-messages">
${renderChatMessages(media.prompt, 'modal')}
</div>
`;
} else {
el('promptModalBody').innerHTML = '<div class="no-response" style="padding: 20px; text-align: center;">无 Prompt 数据</div>';
}
el('promptModal').classList.add('open');
document.body.style.overflow = 'hidden';
}
function closePromptModal() {
el('promptModal').classList.remove('open');
document.body.style.overflow = '';
}
// =========== Lightbox ===========
async function openLightboxForStep(stepIdx, type) {
lightboxImages = [];
for (let i = 0; i < state.trajectory.length; i++) {
const step = state.trajectory[i];
const media = await getMedia(i);
if (media.rawUrl) {
lightboxImages.push({ url: media.rawUrl, label: `Step ${step.step ?? i} - 原图`, stepIdx: i, type: 'raw' });
}
if (media.annotUrl) {
lightboxImages.push({ url: media.annotUrl, label: `Step ${step.step ?? i} - 标注`, stepIdx: i, type: 'annot' });
}
}
lightboxIndex = lightboxImages.findIndex(img => img.stepIdx === stepIdx && img.type === type);
if (lightboxIndex === -1) lightboxIndex = 0;
showLightbox();
}
function showLightbox() {
if (lightboxImages.length === 0) return;
const img = lightboxImages[lightboxIndex];
el('lightboxImg').src = img.url;
el('lightboxInfo').textContent = `${img.label} (${lightboxIndex + 1}/${lightboxImages.length})`;
el('lightbox').classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
el('lightbox').classList.remove('open');
document.body.style.overflow = '';
}
function navigateLightbox(delta) {
lightboxIndex = (lightboxIndex + delta + lightboxImages.length) % lightboxImages.length;
showLightbox();
}
// =========== Event Handlers ===========
el('runSelect').addEventListener('change', (e) => {
const v = e.target.value;
if (!v) return;
if (state.isLoading) return;
loadRunDir(v); // Don't await, let it run async
});
el('taskFilter').addEventListener('input', debounce(() => renderTaskList()));
['fSuccess', 'fFailed', 'fError', 'fSideEffect', 'fOverdue', 'fFalseComplete'].forEach(id => {
el(id).addEventListener('change', () => renderTaskList());
});
// Taxonomy filter event listeners
['fSuite', 'fDifficulty', 'fScope', 'fObjective', 'fComposition'].forEach(id => {
el(id).addEventListener('change', () => onTaxonomyFilterChange());
});
// Tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
el(`panel-${tab.dataset.tab}`).classList.add('active');
});
});
// Lightbox keyboard
document.addEventListener('keydown', (e) => {
if (el('promptModal').classList.contains('open')) {
if (e.key === 'Escape') closePromptModal();
return;
}
if (!el('lightbox').classList.contains('open')) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') navigateLightbox(-1);
if (e.key === 'ArrowRight') navigateLightbox(1);
});
el('lightbox').addEventListener('click', (e) => {
if (e.target === el('lightbox')) closeLightbox();
});
el('promptModal').addEventListener('click', (e) => {
if (e.target === el('promptModal')) closePromptModal();
});
// Initialize app
loadRunsViaHttp();
</script>
</body>
</html>