Spaces:
Sleeping
Sleeping
Download static/index.html from Deadman922/sql-quality-env: direct link, hf CLI and curl.
- Browser
- Download file 57.7 kB
-
https://huggingface.co/spaces/Deadman922/sql-quality-env/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/Deadman922/sql-quality-env/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Deadman922/sql-quality-env/resolve/main/static/index.html
57.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL Quality Environment</title>
<meta name="description" content="OpenEnv-compatible SQL data quality audit environment for AI agent training and evaluation">
<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=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'DM Sans', sans-serif;
background: #F8F9FA;
color: #18181B;
min-height: 100vh;
}
.header {
height: 56px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.logo {
font-family: 'DM Mono', monospace;
font-weight: 500;
font-size: 16px;
color: #E0E7FF;
letter-spacing: -0.5px;
}
.header-center {
display: flex;
align-items: center;
gap: 12px;
}
.task-select {
font-family: 'DM Sans', sans-serif;
padding: 6px 14px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
font-size: 14px;
background: rgba(255,255,255,0.1);
color: #E0E7FF;
cursor: pointer;
transition: all 0.2s;
}
.task-select:hover { background: rgba(255,255,255,0.15); }
.task-select option { background: #1a1a2e; color: #E0E7FF; }
.btn {
font-family: 'DM Sans', sans-serif;
padding: 7px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.25s ease;
position: relative;
overflow: hidden;
}
.btn-primary {
background: linear-gradient(135deg, #2D6A4F 0%, #40916C 100%);
color: #FFFFFF;
box-shadow: 0 2px 6px rgba(45,106,79,0.3);
}
.btn-primary:hover {
background: linear-gradient(135deg, #40916C 0%, #52B788 100%);
box-shadow: 0 4px 12px rgba(45,106,79,0.4);
transform: translateY(-1px);
}
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled {
background: #D1D5DB;
color: #9CA3AF;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
.btn-danger {
background: linear-gradient(135deg, #DC2626 0%, #EF4444 100%);
color: #FFFFFF;
}
.btn-danger:hover { background: linear-gradient(135deg, #EF4444 0%, #F87171 100%); }
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #E0E7FF;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-ready .status-dot { background: #22C55E; }
.status-active .status-dot { background: #F59E0B; animation: pulse 1s infinite; }
.status-error .status-dot { background: #EF4444; }
.main-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 76px 24px 80px;
max-width: 1600px;
margin: 0 auto;
}
.panel {
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
transition: box-shadow 0.3s;
}
.panel:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.panel-title {
font-size: 15px;
font-weight: 700;
color: #111827;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid #E5E7EB;
}
.zone-label {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: #6B7280;
margin-bottom: 6px;
}
.upload-section { margin-bottom: 20px; }
.file-input-wrapper {
display: flex;
gap: 12px;
align-items: center;
}
.file-input { display: none; }
.file-input-label {
font-family: 'DM Sans', sans-serif;
padding: 8px 16px;
border: 1.5px dashed #D1D5DB;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
background: #FAFAFA;
display: inline-block;
transition: all 0.2s;
}
.file-input-label:hover { border-color: #2D6A4F; background: #F0FDF4; color: #2D6A4F; }
.file-name { font-size: 13px; color: #6B7280; }
.schema-panel {
min-height: 100px;
max-height: 300px;
overflow-y: auto;
}
.schema-empty { font-size: 13px; color: #9CA3AF; font-style: italic; }
.schema-table-block {
margin-bottom: 14px;
padding: 12px;
background: #F9FAFB;
border-radius: 8px;
border: 1px solid #F3F4F6;
}
.schema-table-name {
font-family: 'DM Mono', monospace;
font-weight: 500;
font-size: 13px;
color: #111827;
margin-bottom: 4px;
}
.schema-table-meta { font-size: 12px; color: #6B7280; margin-bottom: 8px; }
.schema-columns { display: flex; flex-wrap: wrap; gap: 6px; }
.schema-column {
font-family: 'DM Mono', monospace;
font-size: 11px;
padding: 4px 10px;
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
-webkit-user-select: none;
user-select: none;
}
.schema-column:hover {
background: linear-gradient(135deg, #2D6A4F, #40916C);
color: #FFFFFF;
border-color: #2D6A4F;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(45,106,79,0.3);
}
.query-section { margin-top: 20px; }
.section-label {
font-size: 13px;
font-weight: 600;
color: #111827;
margin-bottom: 8px;
}
.sql-textarea {
font-family: 'DM Mono', monospace;
width: 100%;
min-height: 80px;
padding: 12px;
border: 1.5px solid #E5E7EB;
border-radius: 8px;
font-size: 13px;
resize: vertical;
transition: border-color 0.2s, box-shadow 0.2s;
line-height: 1.5;
}
.sql-textarea:focus {
outline: none;
border-color: #2D6A4F;
box-shadow: 0 0 0 3px rgba(45,106,79,0.1);
}
.query-controls {
display: flex;
gap: 10px;
margin-top: 12px;
}
.result-panel {
margin-top: 16px;
min-height: 80px;
max-height: 400px;
overflow: auto;
border-radius: 8px;
}
.result-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.result-table th,
.result-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid #F3F4F6;
font-family: 'DM Mono', monospace;
}
.result-table th {
background: #F9FAFB;
font-weight: 600;
color: #374151;
position: sticky;
top: 0;
}
.result-table tr:nth-child(even) { background: #FAFAFA; }
.result-table tr:hover { background: #F0FDF4; }
.result-empty { font-size: 13px; color: #9CA3AF; font-style: italic; padding: 20px; }
.error-box {
padding: 12px 16px;
background: #FEF2F2;
border: 1px solid #FECACA;
border-radius: 8px;
color: #DC2626;
font-size: 13px;
margin-top: 12px;
}
.success-box {
padding: 12px 16px;
background: #F0FDF4;
border: 1px solid #BBF7D0;
border-radius: 8px;
color: #166534;
font-size: 13px;
margin-top: 12px;
animation: fadeIn 0.3s ease;
}
.warning-box {
padding: 12px 16px;
background: #FFFBEB;
border: 1px solid #FDE68A;
border-radius: 8px;
color: #92400E;
font-size: 13px;
margin-top: 12px;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.task-description {
font-size: 13px;
color: #6B7280;
margin: 12px 0;
line-height: 1.6;
font-weight: 500;
padding: 10px 14px;
background: #F9FAFB;
border-radius: 8px;
border-left: 3px solid #D1D5DB;
}
.task-description.active {
color: #F97316;
background: #FFF7ED;
border-left-color: #F97316;
}
.fix-context-label {
font-size: 12px;
color: #6B7280;
font-style: italic;
margin-bottom: 8px;
}
.insights-panel {
margin-top: 16px;
padding: 14px;
background: linear-gradient(135deg, #F8FAFC, #F1F5F9);
border-radius: 8px;
min-height: 80px;
border: 1px solid #E2E8F0;
}
.insights-empty { font-size: 13px; color: #94A3B8; font-style: italic; }
.insight-item {
font-size: 12px;
color: #1E293B;
margin-bottom: 6px;
padding: 4px 0;
line-height: 1.5;
}
.insight-item::before { content: 'β '; color: #2D6A4F; font-weight: bold; }
.ai-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 700;
color: #7C3AED;
background: #EDE9FE;
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
letter-spacing: 0.5px;
}
.suggestions-panel { margin-top: 16px; }
.suggestion-item {
padding: 10px 14px;
background: #F0FDF4;
border: 1px solid #BBF7D0;
border-radius: 8px;
margin-bottom: 8px;
font-size: 12px;
color: #166534;
cursor: pointer;
transition: all 0.2s;
}
.suggestion-item:hover { background: #DCFCE7; transform: translateX(2px); }
.results-panel {
margin-top: 16px;
padding: 20px;
background: linear-gradient(135deg, #F8FAFC, #F0FDF4);
border-radius: 10px;
border: 1px solid #D1FAE5;
}
.score-display {
font-size: 42px;
font-weight: 700;
background: linear-gradient(135deg, #2D6A4F, #16A34A);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 16px;
}
.dimension-bar { margin-bottom: 10px; }
.dimension-label {
display: flex;
justify-content: space-between;
font-size: 12px;
margin-bottom: 4px;
font-weight: 500;
}
.dimension-track {
height: 8px;
background: #E5E7EB;
border-radius: 4px;
overflow: hidden;
}
.dimension-fill {
height: 100%;
background: linear-gradient(90deg, #2D6A4F, #40916C);
border-radius: 4px;
transition: width 0.6s ease;
}
.feedback-text {
font-size: 13px;
color: #374151;
margin-top: 14px;
line-height: 1.6;
padding: 10px;
background: #FFFFFF;
border-radius: 6px;
}
.extra-fields { margin-top: 12px; display: flex; gap: 12px; }
.extra-field { flex: 1; }
.extra-field label { display: block; font-size: 12px; color: #6B7280; margin-bottom: 4px; font-weight: 500; }
.extra-field input {
font-family: 'DM Mono', monospace;
width: 100%;
padding: 6px 10px;
border: 1.5px solid #E5E7EB;
border-radius: 8px;
font-size: 13px;
transition: border-color 0.2s;
}
.extra-field input:focus { outline: none; border-color: #2D6A4F; }
/* Toast notification */
.toast-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: linear-gradient(135deg, #1a1a2e, #16213e);
color: #E0E7FF;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 500;
z-index: 200;
transform: translateY(100%);
transition: transform 0.3s ease;
gap: 8px;
}
.toast-bar.visible { transform: translateY(0); }
.toast-bar.success { background: linear-gradient(135deg, #166534, #15803D); }
.toast-bar.error { background: linear-gradient(135deg, #991B1B, #DC2626); }
.toast-bar.info { background: linear-gradient(135deg, #1E40AF, #3B82F6); }
/* Loading spinner */
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.btn-loading { pointer-events: none; opacity: 0.8; }
.btn-loading .spinner { margin-right: 6px; }
/* Chart container */
.chart-container {
margin-top: 16px;
padding: 16px;
background: #FFFFFF;
border: 1px solid #E5E7EB;
border-radius: 8px;
min-height: 120px;
}
.chart-container canvas { width: 100% !important; }
/* Utility classes to replace inline styles */
.hidden { display: none !important; }
.mt-20 { margin-top: 20px; }
.mb-16 { margin-bottom: 16px; }
@media (max-width: 1024px) {
.main-container { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="header">
<div class="logo">SQL QUALITY ENV</div>
<div class="header-center hidden" id="header-task-section">
<select class="task-select" id="task-select">
<option value="">Select a task...</option>
<option value="null_audit">null_audit</option>
<option value="type_repair">type_repair</option>
<option value="dedup_bug">dedup_bug</option>
<option value="fk_integrity">fk_integrity</option>
<option value="schema_drift">schema_drift</option>
</select>
<button class="btn btn-primary" id="init-btn" onclick="initializeEpisode()" disabled>Initialize</button>
</div>
<div class="status-indicator status-ready" id="status-indicator">
<span class="status-dot"></span>
<span id="status-text">Ready</span>
</div>
</header>
<div class="main-container">
<div class="panel">
<div class="zone-label">Zone 1 β Data Zone</div>
<div class="panel-title">Upload & Query Your Data</div>
<div class="upload-section">
<div class="file-input-wrapper">
<input type="file" class="file-input" id="file-input" accept=".csv,.sqlite,.db">
<label class="file-input-label" for="file-input">Choose File</label>
<span class="file-name" id="file-name">No file selected</span>
<button class="btn btn-primary" id="upload-btn" onclick="uploadFile()">Upload</button>
</div>
</div>
<div class="schema-panel" id="schema-panel">
<div class="schema-empty">Upload a file to see its schema</div>
</div>
<div class="query-section">
<div class="section-label">Run a SELECT query on your uploaded data</div>
<textarea class="sql-textarea" id="query-sql" placeholder="SELECT * FROM your_table LIMIT 10"></textarea>
<div class="query-controls">
<button class="btn btn-primary" id="run-query-btn" onclick="runQuery()">Run Query</button>
</div>
<div id="query-error"></div>
<div class="result-panel" id="query-result">
<div class="result-empty">Query results will appear here</div>
</div>
</div>
<!-- Data Quality Chart -->
<div class="chart-container hidden" id="chart-container">
<div class="zone-label">Query Result Completeness</div>
<canvas id="quality-chart" height="140"></canvas>
</div>
<div class="insights-panel" id="insights-panel">
<div class="zone-label">AI Insights <span class="ai-badge">β¦ AI</span></div>
<div class="insights-empty">Run a query to see insights</div>
</div>
<div class="suggestions-panel" id="suggestions-panel">
<div class="zone-label">Smart Task Suggestions</div>
<div class="insights-empty">Upload data and run a query to see suggestions</div>
</div>
</div>
<div class="panel">
<div class="zone-label">Zone 2 β Evaluation Zone</div>
<div class="panel-title">RL Task Evaluation</div>
<div class="task-description" id="task-description">
Select a task from the dropdown above and click Initialize to start an evaluation episode.
</div>
<div id="rl-schema-panel" class="mb-16">
<div class="schema-empty">Task schema will appear after initialization</div>
</div>
<div class="mt-20">
<div class="section-label" id="task-input-label">Submit Your Answer</div>
<div class="fix-context-label" id="fix-context">Select a task to see context</div>
<div class="extra-fields hidden" id="dedup-fields">
<div class="extra-field">
<label>Root Cause Table</label>
<input type="text" id="root-cause-table" placeholder="table_name">
</div>
<div class="extra-field">
<label>Offending Key Column</label>
<input type="text" id="offending-key" placeholder="column_name">
</div>
</div>
<textarea class="sql-textarea" id="fix-sql" placeholder="Enter your fix query or answer here..."></textarea>
<div class="query-controls">
<button class="btn btn-danger" id="apply-fix-btn" onclick="applyFix()">Preview Fix</button>
<button class="btn btn-primary" id="submit-final-btn" onclick="submitFinal()">Submit Final</button>
</div>
<div id="fix-error"></div>
</div>
<div class="results-panel hidden" id="results-panel">
<div class="score-display" id="final-score">0%</div>
<div id="dimension-bars"></div>
<div class="feedback-text" id="feedback-text"></div>
</div>
</div>
</div>
<!-- Toast bar for status messages -->
<div class="toast-bar" id="toast-bar">
<span id="toast-text"></span>
</div>
<script>
const BASE = window.location.origin;
let uploadedFile = null;
let uploadedSchema = [];
let uploadedTableName = null;
let episodeId = null;
let currentTask = null;
let currentTaskContext = null;
let lastQueryResult = null;
let lastQuerySQL = null;
let uploadedTaskContexts = {};
let mode = 'upload';
const taskDescriptions = {
null_audit: 'Find the column with the highest null rate in the dataset.',
type_repair: 'Fix numeric type mismatches where numbers are stored as TEXT.',
dedup_bug: 'Remove duplicate rows that are causing JOIN inflation.',
fk_integrity: 'Remove orphan records that violate foreign key constraints.',
schema_drift: 'Normalize drifted schemas by canonicalizing column names and types.'
};
const fixContexts = {
null_audit: 'Enter the column name with the highest null rate',
type_repair: 'Enter a SQL query that fixes the type mismatch',
dedup_bug: 'Enter a SQL query that removes duplicate rows + table name + key column',
fk_integrity: 'Enter a SQL query that removes orphan records',
schema_drift: 'Enter a SQL query that normalizes the schema'
};
function isAnswerOnlyTask(taskId) {
return taskId === 'null_audit';
}
function showWarning(elementId, messages) {
const items = Array.isArray(messages) ? messages.filter(Boolean) : [messages];
if (!items.length) {
document.getElementById(elementId).innerHTML = '';
return;
}
document.getElementById(elementId).innerHTML =
'<div class="warning-box">' + items.map(escapeHtml).join('<br>') + '</div>';
}
function resetTaskUIState() {
episodeId = null;
currentTaskContext = null;
document.getElementById('fix-sql').value = '';
document.getElementById('fix-sql').placeholder = 'Enter your fix query or answer here...';
document.getElementById('root-cause-table').value = '';
document.getElementById('offending-key').value = '';
document.getElementById('fix-error').innerHTML = '';
document.getElementById('results-panel').classList.add('hidden');
document.getElementById('rl-schema-panel').innerHTML = '<div class="schema-empty">Task schema will appear after initialization</div>';
document.getElementById('task-description').textContent = 'Select a task from the dropdown above and click Initialize to start an evaluation episode.';
document.getElementById('task-description').classList.remove('active');
document.getElementById('fix-context').textContent = 'Select a task to see context';
document.getElementById('task-input-label').textContent = 'Submit Your Answer';
document.getElementById('apply-fix-btn').textContent = 'Preview Fix';
document.getElementById('submit-final-btn').textContent = 'Submit Final';
}
function updateTaskMode(taskId, taskContext) {
const inputLabel = document.getElementById('task-input-label');
const applyBtn = document.getElementById('apply-fix-btn');
const submitBtn = document.getElementById('submit-final-btn');
const textarea = document.getElementById('fix-sql');
const answerMode = isAnswerOnlyTask(taskId) || (taskContext && taskContext.input_mode === 'answer');
inputLabel.textContent = answerMode ? 'Submit Column Answer' : 'Submit Fix SQL';
applyBtn.textContent = answerMode ? 'Check Answer' : 'Preview Fix';
submitBtn.textContent = answerMode ? 'Submit Answer' : 'Submit Final';
if (taskContext && taskContext.input_placeholder) {
textarea.placeholder = taskContext.input_placeholder;
} else {
textarea.placeholder = answerMode
? 'Enter the exact column name'
: 'Enter the SQL fix you want graded on the cloned database';
}
}
function renderBackendSuggestions(suggestions) {
if (!suggestions || suggestions.length === 0) return false;
renderSuggestions(suggestions);
return true;
}
function isAggregateLikeColumn(col) {
const tokens = String(col || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
const aggregateTokens = new Set(['avg', 'sum', 'count', 'total', 'min', 'max', 'rank', 'highest', 'lowest']);
return tokens.some(token => aggregateTokens.has(token));
}
function isKeyLikeColumn(col) {
const tokens = String(col || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
const keyTokens = new Set(['id', 'email', 'phone', 'ssn', 'uuid', 'employee', 'patient']);
return tokens.some(token => keyTokens.has(token));
}
// ββ Toast notification system ββ
let toastTimer = null;
function showToast(message, type = 'info', duration = 4000) {
const bar = document.getElementById('toast-bar');
const text = document.getElementById('toast-text');
bar.className = 'toast-bar visible ' + type;
text.textContent = message;
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => { bar.classList.remove('visible'); }, duration);
}
// ββ Loading state helpers ββ
function setLoading(btnId, loading) {
const btn = document.getElementById(btnId);
if (!btn) return;
if (loading) {
btn._origText = btn.textContent;
btn.innerHTML = '<span class="spinner"></span>' + btn._origText + '...';
btn.classList.add('btn-loading');
} else {
// Explicitly check for undefined to handle intentionally empty labels
if (btn._origText !== undefined) {
btn.textContent = btn._origText;
} else {
// Fallback: strip spinner HTML and ellipsis
btn.textContent = btn.textContent.replace(/<span class="spinner"><\/span>/g, '').replace(/\.\.\.$/, '');
}
btn.classList.remove('btn-loading');
}
}
// ββ Health check ββ
async function checkHealth() {
try {
const r = await fetch(BASE + '/health');
const indicator = document.getElementById('status-indicator');
const text = document.getElementById('status-text');
if (r.ok) {
if (uploadedFile || episodeId) {
indicator.className = 'status-indicator status-active';
text.textContent = 'Active';
} else {
indicator.className = 'status-indicator status-ready';
text.textContent = 'Ready';
}
} else {
indicator.className = 'status-indicator status-error';
text.textContent = 'Error';
}
} catch (e) {
document.getElementById('status-indicator').className = 'status-indicator status-error';
document.getElementById('status-text').textContent = 'Offline';
}
}
// ββ File input handler ββ
document.getElementById('file-input').addEventListener('change', function() {
const file = this.files[0];
document.getElementById('file-name').textContent = file ? file.name : 'No file selected';
});
// ββ Task select handler ββ
function updateTaskAvailability(taskContexts, compatibleTasks) {
uploadedTaskContexts = taskContexts || {};
const supported = new Set(compatibleTasks || []);
const select = document.getElementById('task-select');
for (const option of Array.from(select.options)) {
if (!option.value) continue;
const ctx = uploadedTaskContexts[option.value];
const enabled = supported.has(option.value);
option.disabled = !enabled;
option.textContent = enabled ? option.value : option.value + ' (not supported)';
option.title = (ctx && (ctx.context_hint || ctx.reason)) || '';
}
}
document.getElementById('task-select').addEventListener('change', function() {
currentTask = this.value;
currentTaskContext = uploadedTaskContexts[currentTask] || null;
document.getElementById('fix-sql').value = '';
document.getElementById('fix-error').innerHTML = '';
document.getElementById('init-btn').disabled = !currentTask;
const descEl = document.getElementById('task-description');
const taskContext = currentTaskContext;
if (currentTask) {
if (taskContext && taskContext.reason && taskContext.supported === false) {
descEl.textContent = taskContext.reason;
} else if (taskContext && taskContext.context_hint) {
descEl.textContent = taskContext.context_hint;
} else {
descEl.textContent = taskDescriptions[currentTask];
}
descEl.classList.add('active');
} else {
descEl.textContent = 'Select a task from the dropdown above and click Initialize to start an evaluation episode.';
descEl.classList.remove('active');
}
document.getElementById('fix-context').textContent = currentTask
? ((taskContext && (taskContext.action_guidance || taskContext.context_hint)) || fixContexts[currentTask])
: 'Select a task to see context';
updateTaskMode(currentTask, taskContext);
const dedupFields = document.getElementById('dedup-fields');
if (currentTask === 'dedup_bug') {
dedupFields.classList.remove('hidden');
if (taskContext && taskContext.table) {
document.getElementById('root-cause-table').value = taskContext.table;
}
if (taskContext && taskContext.key_columns && taskContext.key_columns.length > 0) {
document.getElementById('offending-key').value = taskContext.key_columns.join(', ');
} else if (taskContext && taskContext.column) {
document.getElementById('offending-key').value = taskContext.column;
}
} else {
dedupFields.classList.add('hidden');
}
if (taskContext && taskContext.supported === false) {
document.getElementById('init-btn').disabled = true;
}
});
// ββ File upload ββ
async function uploadFile() {
const fileInput = document.getElementById('file-input');
const file = fileInput.files[0];
if (!file) {
showError('query-error', 'Please select a file first.');
return;
}
setLoading('upload-btn', true);
const formData = new FormData();
formData.append('file', file);
try {
const r = await fetch(BASE + '/upload-db', { method: 'POST', body: formData });
const data = await r.json();
if (!r.ok) {
showError('query-error', data.detail || 'Upload failed');
return;
}
uploadedFile = file.name;
uploadedSchema = data.tables || [];
uploadedTableName = uploadedSchema.length > 0 ? uploadedSchema[0].table_name : null;
uploadedTaskContexts = data.task_contexts || {};
mode = 'upload';
resetTaskUIState();
currentTask = null;
document.getElementById('task-select').value = '';
renderSchema(data.tables, 'schema-panel', 'query-sql');
updateTaskAvailability(data.task_contexts || {}, data.compatible_tasks || []);
document.getElementById('header-task-section').classList.remove('hidden');
document.getElementById('query-error').innerHTML = '';
document.getElementById('query-result').innerHTML = '<div class="result-empty">Run a query to see results</div>';
document.getElementById('insights-panel').innerHTML = '<div class="zone-label">AI Insights <span class="ai-badge">β¦ AI</span></div><div class="insights-empty">Run a query to see source-aware insights</div>';
document.getElementById('suggestions-panel').innerHTML = '<div class="zone-label">Smart Task Suggestions</div><div class="insights-empty">Run a query to see backend-ranked task suggestions</div>';
// Auto-populate query with table name
if (uploadedTableName) {
document.getElementById('query-sql').value = 'SELECT * FROM ' + uploadedTableName + ' LIMIT 10';
}
showToast('β File uploaded: ' + file.name + ' (' + uploadedSchema.length + ' table(s))', 'success');
checkHealth();
} catch (e) {
showError('query-error', 'Upload error: ' + e.message);
} finally {
setLoading('upload-btn', false);
}
}
// ββ Schema renderer (reusable for Zone 1 and Zone 2) ββ
function renderSchema(tables, panelId, targetTextareaId) {
const panel = document.getElementById(panelId);
if (!tables || tables.length === 0) {
panel.innerHTML = '<div class="schema-empty">No schema available</div>';
return;
}
let html = '';
for (const tbl of tables) {
const tableName = tbl.table_name || tbl.name || 'Unknown';
const rowCount = tbl.row_count ?? '?';
const columns = tbl.columns || [];
html += '<div class="schema-table-block">';
html += '<div class="schema-table-name">' + escapeHtml(tableName) + '</div>';
html += '<div class="schema-table-meta">' + rowCount + ' rows</div>';
html += '<div class="schema-columns">';
for (const col of columns) {
const colName = typeof col === 'string' ? col.split(' (')[0] : (col.name || col);
const colType = typeof col === 'string' ? (col.match(/\(([^)]+)\)/)?.[1] || 'TEXT') : (col.type || 'TEXT');
html += '<span class="schema-column" data-col="' + escapeHtml(colName) + '" data-target="' + targetTextareaId + '">' +
escapeHtml(colName) + ' <small>(' + escapeHtml(colType) + ')</small></span>';
}
html += '</div></div>';
}
panel.innerHTML = html;
// Add delegated click listener for schema columns (only once)
if (!panel._hasSchemaListener) {
panel.addEventListener('click', function(e) {
if (e.target.classList.contains('schema-column')) {
const col = e.target.getAttribute('data-col');
const target = e.target.getAttribute('data-target');
insertColumnTo(col, target);
}
});
panel._hasSchemaListener = true;
}
}
// ββ Insert column name into any textarea ββ
function insertColumnTo(columnName, textareaId) {
const textarea = document.getElementById(textareaId);
if (!textarea) return;
const cursorPos = textarea.selectionStart;
const textBefore = textarea.value.substring(0, cursorPos);
const textAfter = textarea.value.substring(cursorPos);
const escapedName = columnName.replace(/"/g, '""');
textarea.value = textBefore + '"' + escapedName + '"' + textAfter;
textarea.focus();
showToast('Inserted column: ' + columnName, 'info', 1500);
}
// ββ BUG FIX #1: Allow WITH/CTE queries ββ
function isReadOnlySQL(sql) {
const trimmed = sql.trim().toUpperCase();
// Remove leading comments
const noComments = trimmed.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '').trim();
const firstWord = noComments.split(/\s+/)[0] || '';
return ['SELECT', 'WITH', 'EXPLAIN', 'PRAGMA'].includes(firstWord);
}
// ββ Run Query ββ
async function runQuery() {
const sql = document.getElementById('query-sql').value.trim();
if (!uploadedFile) { showError('query-error', 'Upload a file first.'); return; }
if (!sql) { showError('query-error', 'Enter a SQL query.'); return; }
// BUG FIX #1: Use the proper check that allows WITH/CTE queries
if (!isReadOnlySQL(sql)) {
showError('query-error', 'Only SELECT queries allowed here. Supported prefixes: SELECT, WITH, EXPLAIN, PRAGMA.');
return;
}
setLoading('run-query-btn', true);
try {
const r = await fetch(BASE + '/query-custom', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sql })
});
const data = await r.json();
if (data.error) {
showError('query-error', data.error + (data.message ? ' β ' + data.message : ''));
if (data.warnings && data.warnings.length > 0) {
document.getElementById('query-error').innerHTML +=
'<div class="warning-box">' + data.warnings.map(escapeHtml).join('<br>') + '</div>';
}
if (!renderBackendSuggestions(data.task_suggestions || [])) {
if (data.suggestions) {
renderSuggestions(data.suggestions);
}
}
return;
}
if (!r.ok) {
showError('query-error', data.detail || 'Query failed');
return;
}
document.getElementById('query-error').innerHTML = '';
lastQueryResult = data.rows || [];
lastQuerySQL = sql;
renderQueryResults(lastQueryResult);
showWarning('query-error', data.warnings || []);
// Draw chart
drawQualityChart(lastQueryResult);
// Get AI insights (async - don't block)
fetchAIInsights(sql, lastQueryResult);
// Prefer backend task suggestions over result-grid heuristics.
if (!renderBackendSuggestions(data.task_suggestions || [])) {
generateSuggestions(lastQueryResult);
}
showToast('Query returned ' + lastQueryResult.length + ' row(s)', 'success', 2000);
} catch (e) {
showError('query-error', 'Query error: ' + e.message);
} finally {
setLoading('run-query-btn', false);
}
}
// ββ Render query results table ββ
function renderQueryResults(rows) {
const panel = document.getElementById('query-result');
if (!rows || rows.length === 0) {
panel.innerHTML = '<div class="result-empty">No results returned</div>';
return;
}
const columns = Object.keys(rows[0]);
let html = '<table class="result-table"><thead><tr>';
for (const col of columns) { html += '<th>' + escapeHtml(col) + '</th>'; }
html += '</tr></thead><tbody>';
for (const row of rows.slice(0, 100)) {
html += '<tr>';
for (const col of columns) {
const val = row[col];
const display = val === null ? '<em style="color:#9CA3AF">NULL</em>' : escapeHtml(String(val));
html += '<td>' + display + '</td>';
}
html += '</tr>';
}
html += '</tbody></table>';
if (rows.length > 100) html += '<div style="padding:8px;font-size:12px;color:#6B7280;">Showing 100 of ' + rows.length + ' rows</div>';
panel.innerHTML = html;
}
// ββ BUG FIX #2: AI Insights via Groq API endpoint ββ
async function fetchAIInsights(sql, rows) {
const panel = document.getElementById('insights-panel');
panel.innerHTML = '<div class="zone-label">AI Insights <span class="ai-badge">β¦ AI</span></div><div class="insights-empty"><span class="spinner" style="border-color:rgba(0,0,0,0.1);border-top-color:#2D6A4F;width:12px;height:12px;"></span> Analyzing data...</div>';
try {
const r = await fetch(BASE + '/ai-insights', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sql: sql,
rows: rows.slice(0, 20),
row_count: rows.length,
columns: rows.length > 0 ? Object.keys(rows[0]) : []
})
});
if (r.ok) {
const data = await r.json();
const insights = data.insights || [];
if (insights.length > 0) {
let html = '<div class="zone-label">AI Insights <span class="ai-badge">β¦ AI</span></div>';
for (const insight of insights) {
html += '<div class="insight-item">' + escapeHtml(insight) + '</div>';
}
panel.innerHTML = html;
return;
}
}
} catch (e) {
console.log('AI insights endpoint not available, using local analysis');
}
// Fallback: enhanced local insights
computeLocalInsights(rows);
}
// ββ Enhanced local insights (fallback) ββ
function computeLocalInsights(rows) {
const panel = document.getElementById('insights-panel');
if (!rows || rows.length === 0) {
panel.innerHTML = '<div class="zone-label">AI Insights <span class="ai-badge">β¦ AI</span></div><div class="insights-empty">Query returned no data. Try a broader query or check table name.</div>';
return;
}
const columns = Object.keys(rows[0]);
const insights = [];
insights.push('Query returned ' + rows.length + ' rows across ' + columns.length + ' columns');
const rankedContexts = Object.entries(uploadedTaskContexts)
.filter(([, ctx]) => ctx && ctx.supported)
.sort((a, b) => (b[1].confidence || 0) - (a[1].confidence || 0));
if (rankedContexts.length > 0) {
insights.push('Source signal: ' + (rankedContexts[0][1].context_hint || rankedContexts[0][1].description || rankedContexts[0][0]));
}
// Null analysis
const nullCols = [];
for (const col of columns) {
const values = rows.map(r => r[col]);
const nullCount = values.filter(v => v === null || v === undefined || v === '').length;
if (nullCount > 0) {
const pct = Math.round(nullCount / rows.length * 100);
nullCols.push({ col, pct, count: nullCount });
}
}
if (nullCols.length > 0) {
nullCols.sort((a, b) => b.pct - a.pct);
const worst = nullCols[0];
insights.push('β οΈ Highest null rate: "' + worst.col + '" at ' + worst.pct + '% (' + worst.count + ' nulls)');
if (nullCols.length > 1) {
insights.push('π ' + nullCols.length + ' columns have null values total');
}
} else {
insights.push('No NULL values detected in returned data, but source-table quality issues may still exist outside this result set');
}
// Numeric analysis
for (const col of columns) {
const numeric = rows.map(r => r[col]).filter(v => v !== null && !isNaN(parseFloat(v)) && isFinite(v));
if (numeric.length > rows.length * 0.5) {
const nums = numeric.map(v => parseFloat(v));
const min = Math.min(...nums);
const max = Math.max(...nums);
const avg = (nums.reduce((a, b) => a + b, 0) / nums.length).toFixed(2);
insights.push('"' + col + '": range [' + min + ' β ' + max + '], avg=' + avg);
}
}
// Duplicate detection
for (const col of columns) {
if (isAggregateLikeColumn(col) || !isKeyLikeColumn(col)) continue;
const values = rows.map(r => r[col]).filter(v => v !== null);
const unique = new Set(values.map(String));
if (unique.size < values.length * 0.8 && values.length > 5) {
const dupCount = values.length - unique.size;
insights.push('"' + col + '": ' + dupCount + ' duplicate values detected (' + unique.size + ' unique / ' + values.length + ' total)');
}
}
let html = '<div class="zone-label">AI Insights <span class="ai-badge">β¦ LOCAL</span></div>';
for (const insight of insights.slice(0, 8)) {
html += '<div class="insight-item">' + escapeHtml(insight) + '</div>';
}
panel.innerHTML = html;
}
// ββ Smart suggestions ββ
function generateSuggestions(rows) {
const panel = document.getElementById('suggestions-panel');
const rankedContexts = Object.entries(uploadedTaskContexts)
.filter(([, ctx]) => ctx && ctx.supported)
.sort((a, b) => (b[1].confidence || 0) - (a[1].confidence || 0));
if (rankedContexts.length > 0) {
renderSuggestions(rankedContexts.slice(0, 4).map(([taskId, ctx]) => taskId + ': ' + ((ctx && (ctx.context_hint || ctx.description)) || taskId)));
return;
}
if (!rows || rows.length === 0) {
panel.innerHTML = '<div class="zone-label">Smart Task Suggestions</div><div class="insights-empty">Query returned 0 rows β try broadening your query</div>';
return;
}
const suggestions = [];
const columns = Object.keys(rows[0]);
for (const col of columns) {
const values = rows.map(r => r[col]);
const nullCount = values.filter(v => v === null || v === undefined).length;
if (nullCount > 0) {
const pct = Math.round(nullCount / rows.length * 100);
if (pct > 10) {
suggestions.push({
text: 'π Consider running null_audit task β column "' + col + '" has ' + pct + '% nulls',
task: 'null_audit'
});
}
}
}
// Check for duplicates
const suggestionCols = columns.filter(col => !isAggregateLikeColumn(col) && isKeyLikeColumn(col));
const allValues = rows.flatMap(r => suggestionCols.map(col => r[col])).filter(v => v !== null);
const duplicates = allValues.filter((v, i, a) => a.indexOf(v) !== i);
if (suggestionCols.length > 0 && duplicates.length > rows.length * 0.1) {
suggestions.push({
text: 'π Consider running dedup_bug task β significant duplicate values detected',
task: 'dedup_bug'
});
}
// Check for type mismatches
for (const col of columns) {
const nonNull = rows.map(r => r[col]).filter(v => v !== null && v !== undefined);
const allStrings = nonNull.every(v => typeof v === 'string');
const allNumericLooking = nonNull.length > 0 && nonNull.every(v => /^-?\$?\d+\.?\d*%?$/.test(String(v).trim().replace(/[$,%]/g, '')));
if (allStrings && allNumericLooking && nonNull.length > 3) {
suggestions.push({
text: 'π§ Consider running type_repair task β column "' + col + '" has numeric values stored as text',
task: 'type_repair'
});
}
}
if (suggestions.length === 0) {
suggestions.push({ text: 'β
Data looks healthy. Try running an RL task to verify quality.', task: null });
}
let html = '<div class="zone-label">Smart Task Suggestions</div>';
for (const suggestion of suggestions.slice(0, 4)) {
const onclick = suggestion.task
? ' onclick="document.getElementById(\'task-select\').value=\'' + suggestion.task + '\';document.getElementById(\'task-select\').dispatchEvent(new Event(\'change\'));"'
: '';
html += '<div class="suggestion-item"' + onclick + '>' + escapeHtml(suggestion.text) + '</div>';
}
panel.innerHTML = html;
}
function renderSuggestions(suggestionTexts) {
const panel = document.getElementById('suggestions-panel');
let html = '<div class="zone-label">Smart Task Suggestions</div>';
for (const text of suggestionTexts) {
const taskMatch = String(text).match(/^(null_audit|type_repair|dedup_bug|fk_integrity|schema_drift):/);
const onclick = taskMatch
? ' onclick="document.getElementById(\'task-select\').value=\'' + taskMatch[1] + '\';document.getElementById(\'task-select\').dispatchEvent(new Event(\'change\'));"'
: '';
html += '<div class="suggestion-item"' + onclick + '>' + escapeHtml(text) + '</div>';
}
panel.innerHTML = html;
}
// ββ BUG FIX #7: Initialize Episode β use /initialize endpoint with correct response format ββ
async function initializeEpisode() {
if (!currentTask) { alert('Select a task first'); return; }
setLoading('init-btn', true);
showToast('β³ Initializing ' + currentTask + ' episode...', 'info', 10000);
try {
// Use /initialize endpoint which returns { schema: [...] } format
const r = await fetch(BASE + '/initialize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: currentTask })
});
const data = await r.json();
if (!r.ok) {
showToast('β Initialization failed: ' + (data.detail || 'Unknown error'), 'error');
return;
}
episodeId = data.episode_id;
mode = 'task';
currentTaskContext = data.task_context || {};
// BUG FIX #7: The /initialize endpoint returns { schema: [...] } not { schema_info: [...] }
const schemaData = data.schema || [];
if (schemaData.length > 0) {
// Render schema with clickable columns pointing to fix-sql textarea (BUG FIX #5)
renderSchema(schemaData, 'rl-schema-panel', 'fix-sql');
} else {
document.getElementById('rl-schema-panel').innerHTML = '<div class="schema-empty">No schema data available for this task</div>';
}
// Update task description with actual description from server
if (data.task_description) {
document.getElementById('task-description').textContent = data.task_description;
}
const taskContext = currentTaskContext;
if (taskContext.context_hint) {
document.getElementById('fix-context').textContent = taskContext.action_guidance || taskContext.context_hint;
}
updateTaskMode(currentTask, taskContext);
if (taskContext.target_table && currentTask === 'dedup_bug') {
document.getElementById('root-cause-table').value = taskContext.target_table;
}
if (taskContext.key_columns && taskContext.key_columns.length > 0 && currentTask === 'dedup_bug') {
document.getElementById('offending-key').value = taskContext.key_columns.join(', ');
} else if (taskContext.target_column && currentTask === 'dedup_bug') {
document.getElementById('offending-key').value = taskContext.target_column;
}
if (taskContext.input_placeholder) {
document.getElementById('fix-sql').placeholder = taskContext.input_placeholder;
} else if (taskContext.verification_sql && !document.getElementById('fix-sql').value.trim()) {
document.getElementById('fix-sql').placeholder = 'Suggested verification: ' + taskContext.verification_sql;
}
// Show investigation hint
if (data.investigation_hint) {
showToast('π‘ Hint: ' + data.investigation_hint, 'info', 5000);
}
const safeId = episodeId ? episodeId.slice(0, 8) : 'unknown';
showToast('\u2713 Episode initialized: ' + currentTask + ' (ID: ' + safeId + '...)', 'success');
checkHealth();
} catch (e) {
showToast('β Initialization error: ' + e.message, 'error');
} finally {
setLoading('init-btn', false);
}
}
// ββ BUG FIX #3: Apply Fix β send fix_query field + show feedback ββ
async function applyFix() {
if (!episodeId) { showError('fix-error', 'Initialize an episode first.'); return; }
const sql = document.getElementById('fix-sql').value.trim();
if (!sql) { showError('fix-error', 'Enter a fix query or answer.'); return; }
setLoading('apply-fix-btn', true);
// BUG FIX #3: Use fix_query field (not sql) for fix actions
const body = {
task_id: currentTask,
action_type: 'fix',
episode_id: episodeId,
fix_query: sql // FIXED: was 'sql', should be 'fix_query'
};
if (currentTask === 'dedup_bug') {
body.root_cause_table = document.getElementById('root-cause-table').value;
body.offending_key = document.getElementById('offending-key').value;
}
try {
const r = await fetch(BASE + '/step', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await r.json();
if (!r.ok) {
showError('fix-error', data.detail || 'Fix failed');
return;
}
// BUG FIX #3: Show success feedback
const feedback = data.reward?.feedback || 'Fix applied successfully';
const score = data.reward?.step_reward ?? 0;
document.getElementById('fix-error').innerHTML =
'<div class="success-box">β ' + escapeHtml(feedback) +
' (step reward: ' + (score * 100).toFixed(1) + '%)</div>';
showToast('β ' + feedback, 'success');
} catch (e) {
showError('fix-error', 'Fix error: ' + e.message);
} finally {
setLoading('apply-fix-btn', false);
}
}
// ββ BUG FIX #4: Submit Final β send answer for null_audit, fix_query for others ββ
async function submitFinal() {
if (!episodeId) { showError('fix-error', 'Initialize an episode first.'); return; }
const sql = document.getElementById('fix-sql').value.trim();
// Validation: non-null_audit tasks require non-empty sql
if (currentTask !== 'null_audit' && !sql) {
showError('fix-error', 'Please enter a FIX SQL query before submitting.');
return;
}
setLoading('submit-final-btn', true);
const body = {
task_id: currentTask,
action_type: 'submit',
episode_id: episodeId
};
// BUG FIX #4: Route answer vs fix_query based on task type
if (currentTask === 'null_audit') {
// null_audit expects 'answer' field with the column name
body.answer = sql;
} else if (sql) {
body.fix_query = sql;
}
if (currentTask === 'dedup_bug') {
body.root_cause_table = document.getElementById('root-cause-table').value;
body.offending_key = document.getElementById('offending-key').value;
}
try {
const r = await fetch(BASE + '/step', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await r.json();
if (!r.ok) {
showError('fix-error', data.detail || 'Submit failed');
return;
}
document.getElementById('fix-error').innerHTML = '';
renderResults(data.reward);
showToast('β Final score: ' + Math.round((data.reward?.score ?? 0) * 100) + '%', 'success', 6000);
} catch (e) {
showError('fix-error', 'Submit error: ' + e.message);
} finally {
setLoading('submit-final-btn', false);
}
}
// ββ Render scoring results ββ
function renderResults(reward) {
const panel = document.getElementById('results-panel');
panel.classList.remove('hidden');
const score = reward?.score ?? 0;
document.getElementById('final-score').textContent = Math.round(score * 100) + '%';
const dims = reward?.dimensions || {};
let barsHtml = '';
const dimNames = {
d1_diagnostic_efficiency: 'D1 β Diagnostic Efficiency',
d2_hypothesis_quality: 'D2 β Hypothesis Quality',
d3_fix_correctness: 'D3 β Fix Correctness',
d4_self_verification: 'D4 β Self Verification',
d5_generalization: 'D5 β Generalization'
};
for (const [key, label] of Object.entries(dimNames)) {
const val = dims[key] ?? 0;
const pct = Math.round(val * 100);
const color = pct >= 70 ? '#16A34A' : pct >= 40 ? '#F59E0B' : '#DC2626';
barsHtml += '<div class="dimension-bar">';
barsHtml += '<div class="dimension-label"><span>' + label + '</span><span style="color:' + color + '">' + pct + '%</span></div>';
barsHtml += '<div class="dimension-track"><div class="dimension-fill" style="width: ' + pct + '%; background: ' + color + '"></div></div>';
barsHtml += '</div>';
}
document.getElementById('dimension-bars').innerHTML = barsHtml;
const feedback = reward?.feedback || '';
const severity = dims?.d3_fix_severity || '';
let feedbackHtml = escapeHtml(feedback);
if (severity) {
const severityColor = {PERFECT:'#16A34A',EXCELLENT:'#22C55E',ACCEPTABLE:'#84CC16',GOOD:'#65A30D',FAIR:'#F59E0B',PARTIAL:'#F97316',DEGRADED:'#FB923C',DANGEROUS:'#EF4444',CRITICAL:'#DC2626',CATASTROPHIC:'#991B1B'}[severity] || '#6B7280';
feedbackHtml += ' <span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;color:white;background:' + severityColor + '">' + severity + '</span>';
}
document.getElementById('feedback-text').innerHTML = feedbackHtml;
}
// ββ Data Quality Chart (simple canvas bar chart) ββ
function drawQualityChart(rows) {
const container = document.getElementById('chart-container');
if (!rows || rows.length === 0) {
container.classList.add('hidden');
return;
}
container.classList.remove('hidden');
const canvas = document.getElementById('quality-chart');
const ctx = canvas.getContext('2d');
// Calculate dimensions
canvas.width = canvas.parentElement.clientWidth - 32;
canvas.height = 140;
const columns = Object.keys(rows[0]);
const barWidth = Math.min(60, (canvas.width - 40) / columns.length - 8);
const maxHeight = 100;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw bars showing non-null percentage per column
columns.forEach((col, i) => {
const values = rows.map(r => r[col]);
const nonNull = values.filter(v => v !== null && v !== undefined && v !== '').length;
const pct = nonNull / values.length;
const barHeight = pct * maxHeight;
const x = 20 + i * (barWidth + 8);
const y = maxHeight - barHeight + 10;
// Bar gradient
const grad = ctx.createLinearGradient(x, y, x, y + barHeight);
if (pct >= 0.9) { grad.addColorStop(0, '#22C55E'); grad.addColorStop(1, '#16A34A'); }
else if (pct >= 0.7) { grad.addColorStop(0, '#F59E0B'); grad.addColorStop(1, '#D97706'); }
else { grad.addColorStop(0, '#EF4444'); grad.addColorStop(1, '#DC2626'); }
ctx.fillStyle = grad;
ctx.beginPath();
if (typeof ctx.roundRect === 'function') {
ctx.roundRect(x, y, barWidth, barHeight, 3);
} else {
// Fallback for older browsers
ctx.rect(x, y, barWidth, barHeight);
}
ctx.fill();
// Label
ctx.fillStyle = '#6B7280';
ctx.font = '9px DM Mono';
ctx.textAlign = 'center';
const label = col.length > 8 ? col.slice(0, 7) + 'β¦' : col;
ctx.fillText(label, x + barWidth / 2, maxHeight + 24);
// Percentage
ctx.fillStyle = '#374151';
ctx.font = 'bold 10px DM Sans';
ctx.fillText(Math.round(pct * 100) + '%', x + barWidth / 2, y - 4);
});
}
// ββ Helpers ββ
function showError(elementId, message) {
document.getElementById(elementId).innerHTML = '<div class="error-box">' + escapeHtml(message) + '</div>';
}
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// ββ Init ββ
checkHealth();
setInterval(checkHealth, 30000);
</script>
</body>
</html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL Quality Environment</title>
<meta name="description" content="OpenEnv-compatible SQL data quality audit environment for AI agent training and evaluation">
<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=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'DM Sans', sans-serif;
background: #F8F9FA;
color: #18181B;
min-height: 100vh;
}
.header {
height: 56px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.logo {
font-family: 'DM Mono', monospace;
font-weight: 500;
font-size: 16px;
color: #E0E7FF;
letter-spacing: -0.5px;
}
.header-center {
display: flex;
align-items: center;
gap: 12px;
}
.task-select {
font-family: 'DM Sans', sans-serif;
padding: 6px 14px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
font-size: 14px;
background: rgba(255,255,255,0.1);
color: #E0E7FF;
cursor: pointer;
transition: all 0.2s;
}
.task-select:hover { background: rgba(255,255,255,0.15); }
.task-select option { background: #1a1a2e; color: #E0E7FF; }
.btn {
font-family: 'DM Sans', sans-serif;
padding: 7px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.25s ease;
position: relative;
overflow: hidden;
}
.btn-primary {
background: linear-gradient(135deg, #2D6A4F 0%, #40916C 100%);
color: #FFFFFF;
box-shadow: 0 2px 6px rgba(45,106,79,0.3);
}
.btn-primary:hover {
background: linear-gradient(135deg, #40916C 0%, #52B788 100%);
box-shadow: 0 4px 12px rgba(45,106,79,0.4);
transform: translateY(-1px);
}
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled {
background: #D1D5DB;
color: #9CA3AF;
cursor: not-allowed;
box-shadow: none;
transform: none;
}
.btn-danger {
background: linear-gradient(135deg, #DC2626 0%, #EF4444 100%);
color: #FFFFFF;
}
.btn-danger:hover { background: linear-gradient(135deg, #EF4444 0%, #F87171 100%); }
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #E0E7FF;
}