Spaces:
Sleeping
Sleeping
Upload index.html
Browse files- static/index.html +326 -3
static/index.html
CHANGED
|
@@ -527,6 +527,123 @@
|
|
| 527 |
::-webkit-scrollbar-track { background: transparent; }
|
| 528 |
::-webkit-scrollbar-thumb { background: var(--card-border); border-radius: 3px; }
|
| 529 |
::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
</style>
|
| 531 |
</head>
|
| 532 |
<body>
|
|
@@ -569,6 +686,44 @@
|
|
| 569 |
<!-- MAIN CONTENT -->
|
| 570 |
<main class="main">
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
<!-- ROW 1: Bento Grid (3 columns) -->
|
| 573 |
<div class="bento-row bento-3">
|
| 574 |
|
|
@@ -1003,7 +1158,7 @@ async function runQuery() {
|
|
| 1003 |
output.innerHTML = '<div class="output-error">' + escapeHtml(feedback) +
|
| 1004 |
'<div class="error-tip">Tip: Use SELECT statements only in this panel. For data modification, use the FIX tab or Submit.</div></div>';
|
| 1005 |
} else {
|
| 1006 |
-
output.
|
| 1007 |
}
|
| 1008 |
} else {
|
| 1009 |
output.innerHTML = '<div class="output-error">Error: ' + escapeHtml(r.data.detail || 'Unknown error') + '</div>';
|
|
@@ -1114,7 +1269,7 @@ async function submitEpisode() {
|
|
| 1114 |
state.done = r.data.done || false;
|
| 1115 |
state.score = r.data.reward?.score ?? state.score;
|
| 1116 |
updateResults(r.data);
|
| 1117 |
-
output.
|
| 1118 |
if (state.done) {
|
| 1119 |
updateStatusPill('done');
|
| 1120 |
toast('Episode complete! Score: ' + (state.score * 100).toFixed(1) + '%', 'success');
|
|
@@ -1214,7 +1369,175 @@ function updateResults(stepData) {
|
|
| 1214 |
|
| 1215 |
// βββ UTILITIES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1216 |
function formatResult(data) {
|
| 1217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1218 |
}
|
| 1219 |
|
| 1220 |
function escapeHtml(str) {
|
|
|
|
| 527 |
::-webkit-scrollbar-track { background: transparent; }
|
| 528 |
::-webkit-scrollbar-thumb { background: var(--card-border); border-radius: 3px; }
|
| 529 |
::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
|
| 530 |
+
|
| 531 |
+
/* βββ HELP / HOW TO USE βββββββββββββββββββββββββββββββββββββββββββββ */
|
| 532 |
+
.help-section {
|
| 533 |
+
background: linear-gradient(135deg, #E8F5E9 0%, #F1F8E9 100%);
|
| 534 |
+
border: 1px solid #A5D6A7;
|
| 535 |
+
border-radius: var(--radius);
|
| 536 |
+
padding: 16px;
|
| 537 |
+
margin-bottom: 16px;
|
| 538 |
+
}
|
| 539 |
+
.help-title {
|
| 540 |
+
font-size: 14px;
|
| 541 |
+
font-weight: 500;
|
| 542 |
+
color: var(--primary);
|
| 543 |
+
margin-bottom: 12px;
|
| 544 |
+
display: flex;
|
| 545 |
+
align-items: center;
|
| 546 |
+
gap: 8px;
|
| 547 |
+
}
|
| 548 |
+
.help-steps {
|
| 549 |
+
display: grid;
|
| 550 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 551 |
+
gap: 12px;
|
| 552 |
+
}
|
| 553 |
+
.help-step {
|
| 554 |
+
background: var(--bg);
|
| 555 |
+
border: 1px solid var(--card-border);
|
| 556 |
+
border-radius: var(--radius);
|
| 557 |
+
padding: 12px;
|
| 558 |
+
}
|
| 559 |
+
.help-step-num {
|
| 560 |
+
display: inline-flex;
|
| 561 |
+
align-items: center;
|
| 562 |
+
justify-content: center;
|
| 563 |
+
width: 20px;
|
| 564 |
+
height: 20px;
|
| 565 |
+
background: var(--primary);
|
| 566 |
+
color: white;
|
| 567 |
+
font-size: 11px;
|
| 568 |
+
font-weight: 500;
|
| 569 |
+
border-radius: 50%;
|
| 570 |
+
margin-bottom: 6px;
|
| 571 |
+
}
|
| 572 |
+
.help-step-title {
|
| 573 |
+
font-size: 12px;
|
| 574 |
+
font-weight: 500;
|
| 575 |
+
color: var(--text-primary);
|
| 576 |
+
margin-bottom: 4px;
|
| 577 |
+
}
|
| 578 |
+
.help-step-desc {
|
| 579 |
+
font-size: 11px;
|
| 580 |
+
color: var(--text-secondary);
|
| 581 |
+
line-height: 1.4;
|
| 582 |
+
}
|
| 583 |
+
.help-actions {
|
| 584 |
+
display: flex;
|
| 585 |
+
gap: 8px;
|
| 586 |
+
margin-top: 12px;
|
| 587 |
+
padding-top: 12px;
|
| 588 |
+
border-top: 1px solid #A5D6A7;
|
| 589 |
+
}
|
| 590 |
+
.help-badge {
|
| 591 |
+
display: inline-flex;
|
| 592 |
+
align-items: center;
|
| 593 |
+
padding: 2px 6px;
|
| 594 |
+
font-size: 10px;
|
| 595 |
+
background: var(--bg);
|
| 596 |
+
border: 1px solid var(--card-border);
|
| 597 |
+
border-radius: 4px;
|
| 598 |
+
color: var(--text-secondary);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
/* βββ FORMATTED RESULT TABLE ββββββββββββββββββββββββββββββββββββββββββ */
|
| 602 |
+
.result-table {
|
| 603 |
+
width: 100%;
|
| 604 |
+
border-collapse: collapse;
|
| 605 |
+
font-size: 12px;
|
| 606 |
+
margin-top: 8px;
|
| 607 |
+
}
|
| 608 |
+
.result-table th {
|
| 609 |
+
text-align: left;
|
| 610 |
+
padding: 8px 10px;
|
| 611 |
+
background: var(--surface);
|
| 612 |
+
color: var(--text-secondary);
|
| 613 |
+
font-weight: 500;
|
| 614 |
+
border-bottom: 2px solid var(--card-border);
|
| 615 |
+
white-space: nowrap;
|
| 616 |
+
}
|
| 617 |
+
.result-table td {
|
| 618 |
+
padding: 6px 10px;
|
| 619 |
+
border-bottom: 1px solid var(--card-border);
|
| 620 |
+
color: var(--text-primary);
|
| 621 |
+
max-width: 200px;
|
| 622 |
+
overflow: hidden;
|
| 623 |
+
text-overflow: ellipsis;
|
| 624 |
+
}
|
| 625 |
+
.result-table tr:hover td {
|
| 626 |
+
background: var(--surface);
|
| 627 |
+
}
|
| 628 |
+
.result-summary {
|
| 629 |
+
padding: 10px 12px;
|
| 630 |
+
background: var(--surface);
|
| 631 |
+
border-radius: var(--radius);
|
| 632 |
+
margin-bottom: 12px;
|
| 633 |
+
font-size: 12px;
|
| 634 |
+
color: var(--text-secondary);
|
| 635 |
+
}
|
| 636 |
+
.result-summary strong {
|
| 637 |
+
color: var(--text-primary);
|
| 638 |
+
}
|
| 639 |
+
.result-null {
|
| 640 |
+
color: var(--text-tertiary);
|
| 641 |
+
font-style: italic;
|
| 642 |
+
}
|
| 643 |
+
.result-number {
|
| 644 |
+
color: var(--success);
|
| 645 |
+
font-weight: 500;
|
| 646 |
+
}
|
| 647 |
</style>
|
| 648 |
</head>
|
| 649 |
<body>
|
|
|
|
| 686 |
<!-- MAIN CONTENT -->
|
| 687 |
<main class="main">
|
| 688 |
|
| 689 |
+
<!-- HOW TO USE SECTION -->
|
| 690 |
+
<div class="help-section" id="help-section">
|
| 691 |
+
<div class="help-title">
|
| 692 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 693 |
+
<circle cx="12" cy="12" r="10"/>
|
| 694 |
+
<path d="M12 16v-4M12 8h.01"/>
|
| 695 |
+
</svg>
|
| 696 |
+
How to Use This Application
|
| 697 |
+
</div>
|
| 698 |
+
<div class="help-steps">
|
| 699 |
+
<div class="help-step">
|
| 700 |
+
<span class="help-step-num">1</span>
|
| 701 |
+
<div class="help-step-title">Select Task</div>
|
| 702 |
+
<div class="help-step-desc">Choose a data quality task from the dropdown. Each has a specific data issue to investigate.</div>
|
| 703 |
+
</div>
|
| 704 |
+
<div class="help-step">
|
| 705 |
+
<span class="help-step-num">2</span>
|
| 706 |
+
<div class="help-step-title">Investigate</div>
|
| 707 |
+
<div class="help-step-desc">Use <strong>Query</strong> (SELECT only) to explore data. Use <strong>Profile</strong> to check column stats. Review the Schema panel.</div>
|
| 708 |
+
</div>
|
| 709 |
+
<div class="help-step">
|
| 710 |
+
<span class="help-step-num">3</span>
|
| 711 |
+
<div class="help-step-title">Identify Issue</div>
|
| 712 |
+
<div class="help-step-desc">Find the data quality problem: null values, type mismatches, duplicates, or orphans.</div>
|
| 713 |
+
</div>
|
| 714 |
+
<div class="help-step">
|
| 715 |
+
<span class="help-step-num">4</span>
|
| 716 |
+
<div class="help-step-title">Submit Fix</div>
|
| 717 |
+
<div class="help-step-desc">Use the appropriate tab: <strong>Text Answer</strong> for null_audit, <strong>SQL Fix</strong> for repairs, <strong>Dedup</strong> for duplicates.</div>
|
| 718 |
+
</div>
|
| 719 |
+
</div>
|
| 720 |
+
<div class="help-actions">
|
| 721 |
+
<span class="help-badge">Query = SELECT only</span>
|
| 722 |
+
<span class="help-badge">Fix = UPDATE/INSERT/DELETE allowed</span>
|
| 723 |
+
<span class="help-badge">Submit = Final answer</span>
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
|
| 727 |
<!-- ROW 1: Bento Grid (3 columns) -->
|
| 728 |
<div class="bento-row bento-3">
|
| 729 |
|
|
|
|
| 1158 |
output.innerHTML = '<div class="output-error">' + escapeHtml(feedback) +
|
| 1159 |
'<div class="error-tip">Tip: Use SELECT statements only in this panel. For data modification, use the FIX tab or Submit.</div></div>';
|
| 1160 |
} else {
|
| 1161 |
+
output.innerHTML = formatResult(r.data);
|
| 1162 |
}
|
| 1163 |
} else {
|
| 1164 |
output.innerHTML = '<div class="output-error">Error: ' + escapeHtml(r.data.detail || 'Unknown error') + '</div>';
|
|
|
|
| 1269 |
state.done = r.data.done || false;
|
| 1270 |
state.score = r.data.reward?.score ?? state.score;
|
| 1271 |
updateResults(r.data);
|
| 1272 |
+
output.innerHTML = formatResult(r.data);
|
| 1273 |
if (state.done) {
|
| 1274 |
updateStatusPill('done');
|
| 1275 |
toast('Episode complete! Score: ' + (state.score * 100).toFixed(1) + '%', 'success');
|
|
|
|
| 1369 |
|
| 1370 |
// βββ UTILITIES βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1371 |
function formatResult(data) {
|
| 1372 |
+
// Format as readable table instead of raw JSON
|
| 1373 |
+
if (!data) return 'No data';
|
| 1374 |
+
|
| 1375 |
+
// Handle array of objects (query results)
|
| 1376 |
+
if (Array.isArray(data)) {
|
| 1377 |
+
if (data.length === 0) return '<div class="result-summary">0 rows returned</div>';
|
| 1378 |
+
return formatArrayAsTable(data);
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
// Handle nested observation structure
|
| 1382 |
+
if (data.observation && data.observation.investigation_hint) {
|
| 1383 |
+
return formatInvestigationHint(data.observation.investigation_hint);
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
+
// Handle reward feedback (text feedback)
|
| 1387 |
+
if (data.reward && data.reward.feedback) {
|
| 1388 |
+
return formatFeedback(data.reward.feedback);
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
// Default: format as readable object
|
| 1392 |
+
return formatObjectAsReadable(data);
|
| 1393 |
+
}
|
| 1394 |
+
|
| 1395 |
+
function formatArrayAsTable(arr) {
|
| 1396 |
+
if (!arr || arr.length === 0) return '<div class="result-summary">0 rows returned</div>';
|
| 1397 |
+
|
| 1398 |
+
const columns = Object.keys(arr[0]);
|
| 1399 |
+
let html = '<div class="result-summary"><strong>' + arr.length + '</strong> row' + (arr.length > 1 ? 's' : '') + ' returned</div>';
|
| 1400 |
+
|
| 1401 |
+
html += '<table class="result-table"><thead><tr>';
|
| 1402 |
+
columns.forEach(col => {
|
| 1403 |
+
html += '<th>' + escapeHtml(col) + '</th>';
|
| 1404 |
+
});
|
| 1405 |
+
html += '</tr></thead><tbody>';
|
| 1406 |
+
|
| 1407 |
+
// Show up to 20 rows
|
| 1408 |
+
const displayRows = arr.slice(0, 20);
|
| 1409 |
+
displayRows.forEach(row => {
|
| 1410 |
+
html += '<tr>';
|
| 1411 |
+
columns.forEach(col => {
|
| 1412 |
+
const val = row[col];
|
| 1413 |
+
let formatted = formatCellValue(val);
|
| 1414 |
+
html += '<td>' + formatted + '</td>';
|
| 1415 |
+
});
|
| 1416 |
+
html += '</tr>';
|
| 1417 |
+
});
|
| 1418 |
+
|
| 1419 |
+
html += '</tbody></table>';
|
| 1420 |
+
|
| 1421 |
+
if (arr.length > 20) {
|
| 1422 |
+
html += '<div class="result-summary" style="margin-top:8px;">... and ' + (arr.length - 20) + ' more rows</div>';
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
return html;
|
| 1426 |
+
}
|
| 1427 |
+
|
| 1428 |
+
function formatCellValue(val) {
|
| 1429 |
+
if (val === null || val === undefined) {
|
| 1430 |
+
return '<span class="result-null">NULL</span>';
|
| 1431 |
+
}
|
| 1432 |
+
if (typeof val === 'number') {
|
| 1433 |
+
return '<span class="result-number">' + escapeHtml(String(val)) + '</span>';
|
| 1434 |
+
}
|
| 1435 |
+
if (typeof val === 'boolean') {
|
| 1436 |
+
return '<span class="result-number">' + (val ? 'true' : 'false') + '</span>';
|
| 1437 |
+
}
|
| 1438 |
+
const str = String(val);
|
| 1439 |
+
if (str.length > 50) {
|
| 1440 |
+
return escapeHtml(str.substring(0, 50)) + '...';
|
| 1441 |
+
}
|
| 1442 |
+
return escapeHtml(str);
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
function formatInvestigationHint(hint) {
|
| 1446 |
+
// Try to parse as JSON array
|
| 1447 |
+
try {
|
| 1448 |
+
const parsed = JSON.parse(hint.replace(/'/g, '"'));
|
| 1449 |
+
if (Array.isArray(parsed)) {
|
| 1450 |
+
return formatArrayAsTable(parsed);
|
| 1451 |
+
}
|
| 1452 |
+
} catch (e) {
|
| 1453 |
+
// Not valid JSON, show as text
|
| 1454 |
+
}
|
| 1455 |
+
|
| 1456 |
+
// If it looks like a list of records, try to format
|
| 1457 |
+
if (hint.includes('column_name') && hint.includes('null_count')) {
|
| 1458 |
+
// Parse null audit result format
|
| 1459 |
+
const rows = parseNullAuditResult(hint);
|
| 1460 |
+
if (rows.length > 0) {
|
| 1461 |
+
return formatArrayAsTable(rows);
|
| 1462 |
+
}
|
| 1463 |
+
}
|
| 1464 |
+
|
| 1465 |
+
return '<div class="result-summary">Query Result</div><div style="padding:12px;">' + escapeHtml(hint) + '</div>';
|
| 1466 |
+
}
|
| 1467 |
+
|
| 1468 |
+
function parseNullAuditResult(hint) {
|
| 1469 |
+
const rows = [];
|
| 1470 |
+
// Match patterns like {'column_name': 'city', 'null_count': 520, ...}
|
| 1471 |
+
const regex = /\{[^}]+\}/g;
|
| 1472 |
+
const matches = hint.match(regex);
|
| 1473 |
+
if (matches) {
|
| 1474 |
+
matches.forEach(match => {
|
| 1475 |
+
try {
|
| 1476 |
+
// Convert Python-style dict to JSON
|
| 1477 |
+
const jsonStr = match
|
| 1478 |
+
.replace(/'/g, '"')
|
| 1479 |
+
.replace(/(\w+):/g, '"$1":');
|
| 1480 |
+
const obj = JSON.parse(jsonStr);
|
| 1481 |
+
rows.push(obj);
|
| 1482 |
+
} catch (e) {}
|
| 1483 |
+
});
|
| 1484 |
+
}
|
| 1485 |
+
return rows;
|
| 1486 |
+
}
|
| 1487 |
+
|
| 1488 |
+
function formatFeedback(feedback) {
|
| 1489 |
+
// Format feedback text with better styling
|
| 1490 |
+
if (feedback.includes('|')) {
|
| 1491 |
+
// Multi-part feedback with dimensions
|
| 1492 |
+
const parts = feedback.split('|').map(p => p.trim()).filter(p => p);
|
| 1493 |
+
let html = '<div class="result-summary"><strong>Score Breakdown</strong></div>';
|
| 1494 |
+
html += '<div style="padding:12px;">';
|
| 1495 |
+
parts.forEach(part => {
|
| 1496 |
+
const [label, value] = part.split(':').map(s => s.trim());
|
| 1497 |
+
if (label && value) {
|
| 1498 |
+
html += '<div style="margin-bottom:6px;"><strong>' + escapeHtml(label) + ':</strong> ' + escapeHtml(value) + '</div>';
|
| 1499 |
+
} else {
|
| 1500 |
+
html += '<div style="margin-bottom:6px;">' + escapeHtml(part) + '</div>';
|
| 1501 |
+
}
|
| 1502 |
+
});
|
| 1503 |
+
html += '</div>';
|
| 1504 |
+
return html;
|
| 1505 |
+
}
|
| 1506 |
+
|
| 1507 |
+
// Simple feedback text
|
| 1508 |
+
return '<div style="padding:12px;">' + escapeHtml(feedback) + '</div>';
|
| 1509 |
+
}
|
| 1510 |
+
|
| 1511 |
+
function formatObjectAsReadable(obj) {
|
| 1512 |
+
let html = '<div style="padding:12px;">';
|
| 1513 |
+
|
| 1514 |
+
function renderValue(val, indent) {
|
| 1515 |
+
if (val === null) return '<span class="result-null">null</span>';
|
| 1516 |
+
if (typeof val === 'object') {
|
| 1517 |
+
if (Array.isArray(val)) {
|
| 1518 |
+
if (val.length === 0) return '[]';
|
| 1519 |
+
if (typeof val[0] === 'object') {
|
| 1520 |
+
return formatArrayAsTable(val);
|
| 1521 |
+
}
|
| 1522 |
+
return '[' + val.map(v => renderValue(v, indent)).join(', ') + ']';
|
| 1523 |
+
}
|
| 1524 |
+
// Object
|
| 1525 |
+
let objHtml = '<div style="margin-left:' + (indent * 16) + 'px;">';
|
| 1526 |
+
for (const [k, v] of Object.entries(val)) {
|
| 1527 |
+
objHtml += '<div><strong>' + escapeHtml(k) + ':</strong> ' + renderValue(v, indent + 1) + '</div>';
|
| 1528 |
+
}
|
| 1529 |
+
objHtml += '</div>';
|
| 1530 |
+
return objHtml;
|
| 1531 |
+
}
|
| 1532 |
+
return escapeHtml(String(val));
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
for (const [key, value] of Object.entries(obj)) {
|
| 1536 |
+
html += '<div style="margin-bottom:8px;"><strong>' + escapeHtml(key) + ':</strong> ' + renderValue(value, 1) + '</div>';
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
html += '</div>';
|
| 1540 |
+
return html;
|
| 1541 |
}
|
| 1542 |
|
| 1543 |
function escapeHtml(str) {
|