Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
feat(ui): pretty-print and syntax-highlight JSON gold answers
Browse files- frontend/leaderboard.html +132 -5
frontend/leaderboard.html
CHANGED
|
@@ -620,6 +620,78 @@
|
|
| 620 |
.modal-content {
|
| 621 |
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
| 622 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
</style>
|
| 624 |
|
| 625 |
<!-- Header & Search -->
|
|
@@ -1018,11 +1090,26 @@
|
|
| 1018 |
const s = String(v).trim();
|
| 1019 |
return s ? s : "Unknown";
|
| 1020 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
const formatDetailValue = (v, emptyAsUnknown = true) => {
|
| 1022 |
if (v === undefined || v === null) return emptyAsUnknown ? "Unknown" : "";
|
| 1023 |
if (typeof v === "string") {
|
| 1024 |
const s = v.trim();
|
| 1025 |
-
|
|
|
|
| 1026 |
}
|
| 1027 |
if (Array.isArray(v)) {
|
| 1028 |
if (!v.length) return emptyAsUnknown ? "Unknown" : "";
|
|
@@ -1057,17 +1144,57 @@
|
|
| 1057 |
.replace(/\"/g, """)
|
| 1058 |
.replace(/'/g, "'");
|
| 1059 |
const DETAIL_CELL_LIMIT = 260;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
const expandableTextHtml = (value, idBase, limit = DETAIL_CELL_LIMIT) => {
|
| 1061 |
const raw = String(value ?? "");
|
| 1062 |
if (!raw) return "";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
if (raw.length <= limit) {
|
| 1064 |
-
return `<div class="
|
| 1065 |
}
|
| 1066 |
const short = `${raw.slice(0, limit).trimEnd()}...`;
|
| 1067 |
return `
|
| 1068 |
<div>
|
| 1069 |
-
<div id="${idBase}-short" class="
|
| 1070 |
-
<div id="${idBase}-full" class="hidden
|
| 1071 |
<button type="button" onclick="window.toggleExpandText('${idBase}', this)" class="mt-2 inline-flex items-center gap-1 px-2 py-1 rounded-md border border-indigo-300 dark:border-indigo-600 bg-indigo-50 dark:bg-indigo-900/30 text-xs font-bold text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors">Expand</button>
|
| 1072 |
</div>
|
| 1073 |
`;
|
|
@@ -2286,4 +2413,4 @@ window.toggleExpandText = function (idBase, btn) {
|
|
| 2286 |
|
| 2287 |
</body>
|
| 2288 |
|
| 2289 |
-
</html>
|
|
|
|
| 620 |
.modal-content {
|
| 621 |
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
| 622 |
}
|
| 623 |
+
|
| 624 |
+
.json-render-block {
|
| 625 |
+
white-space: pre-wrap;
|
| 626 |
+
word-break: break-word;
|
| 627 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 628 |
+
font-size: 11px;
|
| 629 |
+
line-height: 1.55;
|
| 630 |
+
border-radius: 0.75rem;
|
| 631 |
+
border: 1px solid #dbe4f0;
|
| 632 |
+
background:
|
| 633 |
+
linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(241, 245, 249, 0.96)),
|
| 634 |
+
linear-gradient(135deg, rgba(14, 165, 233, 0.05), rgba(16, 185, 129, 0.04));
|
| 635 |
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
| 636 |
+
padding: 0.65rem 0.8rem;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.dark .json-render-block {
|
| 640 |
+
border-color: #243047;
|
| 641 |
+
background:
|
| 642 |
+
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(2, 6, 23, 0.98)),
|
| 643 |
+
linear-gradient(135deg, rgba(56, 189, 248, 0.08), rgba(52, 211, 153, 0.05));
|
| 644 |
+
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.08);
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.json-token-punctuation {
|
| 648 |
+
color: #64748b;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.json-token-key {
|
| 652 |
+
color: #0f766e;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.json-token-string {
|
| 656 |
+
color: #b45309;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.json-token-number {
|
| 660 |
+
color: #1d4ed8;
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
.json-token-boolean {
|
| 664 |
+
color: #7c3aed;
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
.json-token-null {
|
| 668 |
+
color: #be185d;
|
| 669 |
+
font-style: italic;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.dark .json-token-punctuation {
|
| 673 |
+
color: #94a3b8;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.dark .json-token-key {
|
| 677 |
+
color: #5eead4;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.dark .json-token-string {
|
| 681 |
+
color: #fdba74;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.dark .json-token-number {
|
| 685 |
+
color: #93c5fd;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.dark .json-token-boolean {
|
| 689 |
+
color: #c4b5fd;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
.dark .json-token-null {
|
| 693 |
+
color: #f9a8d4;
|
| 694 |
+
}
|
| 695 |
</style>
|
| 696 |
|
| 697 |
<!-- Header & Search -->
|
|
|
|
| 1090 |
const s = String(v).trim();
|
| 1091 |
return s ? s : "Unknown";
|
| 1092 |
};
|
| 1093 |
+
const tryFormatJsonString = (value) => {
|
| 1094 |
+
const s = String(value ?? "").trim();
|
| 1095 |
+
if (!s) return null;
|
| 1096 |
+
const looksLikeJson = (
|
| 1097 |
+
(s.startsWith("{") && s.endsWith("}")) ||
|
| 1098 |
+
(s.startsWith("[") && s.endsWith("]"))
|
| 1099 |
+
);
|
| 1100 |
+
if (!looksLikeJson) return null;
|
| 1101 |
+
try {
|
| 1102 |
+
return JSON.stringify(JSON.parse(s), null, 2);
|
| 1103 |
+
} catch {
|
| 1104 |
+
return null;
|
| 1105 |
+
}
|
| 1106 |
+
};
|
| 1107 |
const formatDetailValue = (v, emptyAsUnknown = true) => {
|
| 1108 |
if (v === undefined || v === null) return emptyAsUnknown ? "Unknown" : "";
|
| 1109 |
if (typeof v === "string") {
|
| 1110 |
const s = v.trim();
|
| 1111 |
+
if (!s) return emptyAsUnknown ? "Unknown" : "";
|
| 1112 |
+
return tryFormatJsonString(s) || s;
|
| 1113 |
}
|
| 1114 |
if (Array.isArray(v)) {
|
| 1115 |
if (!v.length) return emptyAsUnknown ? "Unknown" : "";
|
|
|
|
| 1144 |
.replace(/\"/g, """)
|
| 1145 |
.replace(/'/g, "'");
|
| 1146 |
const DETAIL_CELL_LIMIT = 260;
|
| 1147 |
+
const JSON_TOKEN_RE = /"(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(?=\s*:)|"(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?|[{}\[\],:]/g;
|
| 1148 |
+
const isPrettyJsonText = (value) => {
|
| 1149 |
+
const s = String(value ?? "").trim();
|
| 1150 |
+
if (!s) return false;
|
| 1151 |
+
return (
|
| 1152 |
+
(s.startsWith("{") && s.endsWith("}")) ||
|
| 1153 |
+
(s.startsWith("[") && s.endsWith("]"))
|
| 1154 |
+
);
|
| 1155 |
+
};
|
| 1156 |
+
const syntaxHighlightJson = (text) => {
|
| 1157 |
+
let html = "";
|
| 1158 |
+
let lastIndex = 0;
|
| 1159 |
+
for (const match of text.matchAll(JSON_TOKEN_RE)) {
|
| 1160 |
+
const token = match[0];
|
| 1161 |
+
const idx = match.index ?? 0;
|
| 1162 |
+
html += escapeHtml(text.slice(lastIndex, idx));
|
| 1163 |
+
let cls = "json-token-punctuation";
|
| 1164 |
+
if (token.startsWith('"')) {
|
| 1165 |
+
const nextChar = text.slice(idx + token.length).trimStart().charAt(0);
|
| 1166 |
+
cls = nextChar === ":" ? "json-token-key" : "json-token-string";
|
| 1167 |
+
} else if (token === "true" || token === "false") {
|
| 1168 |
+
cls = "json-token-boolean";
|
| 1169 |
+
} else if (token === "null") {
|
| 1170 |
+
cls = "json-token-null";
|
| 1171 |
+
} else if (/^-?\d/.test(token)) {
|
| 1172 |
+
cls = "json-token-number";
|
| 1173 |
+
}
|
| 1174 |
+
html += `<span class="${cls}">${escapeHtml(token)}</span>`;
|
| 1175 |
+
lastIndex = idx + token.length;
|
| 1176 |
+
}
|
| 1177 |
+
html += escapeHtml(text.slice(lastIndex));
|
| 1178 |
+
return html;
|
| 1179 |
+
};
|
| 1180 |
+
const renderDetailText = (text, isJson) => (
|
| 1181 |
+
isJson ? syntaxHighlightJson(text) : escapeHtml(text)
|
| 1182 |
+
);
|
| 1183 |
const expandableTextHtml = (value, idBase, limit = DETAIL_CELL_LIMIT) => {
|
| 1184 |
const raw = String(value ?? "");
|
| 1185 |
if (!raw) return "";
|
| 1186 |
+
const isJson = isPrettyJsonText(raw);
|
| 1187 |
+
const contentClass = isJson
|
| 1188 |
+
? "json-render-block"
|
| 1189 |
+
: "whitespace-pre-wrap break-words";
|
| 1190 |
if (raw.length <= limit) {
|
| 1191 |
+
return `<div class="${contentClass}">${renderDetailText(raw, isJson)}</div>`;
|
| 1192 |
}
|
| 1193 |
const short = `${raw.slice(0, limit).trimEnd()}...`;
|
| 1194 |
return `
|
| 1195 |
<div>
|
| 1196 |
+
<div id="${idBase}-short" class="${contentClass}">${renderDetailText(short, isJson)}</div>
|
| 1197 |
+
<div id="${idBase}-full" class="hidden ${contentClass}">${renderDetailText(raw, isJson)}</div>
|
| 1198 |
<button type="button" onclick="window.toggleExpandText('${idBase}', this)" class="mt-2 inline-flex items-center gap-1 px-2 py-1 rounded-md border border-indigo-300 dark:border-indigo-600 bg-indigo-50 dark:bg-indigo-900/30 text-xs font-bold text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors">Expand</button>
|
| 1199 |
</div>
|
| 1200 |
`;
|
|
|
|
| 2413 |
|
| 2414 |
</body>
|
| 2415 |
|
| 2416 |
+
</html>
|