let sessionId, editor, currentQuestions = [], currentQuestionIndex = 0, isSchemaVisible, isTableVisible, isHintVisible, isSolutionVisible; async function init() { await new Promise((r) => document.readyState === "complete" ? r() : window.addEventListener("load", r) ); const sqlEditor = document.getElementById("sqlEditor"); if (!sqlEditor) throw new Error("SQL Editor element not found"); await new Promise((r) => setTimeout(r, 100)); if (typeof ace === "undefined") throw new Error("Ace Editor library not loaded"); editor = ace.edit("sqlEditor"); editor.setTheme("ace/theme/monokai"); editor.session.setMode("ace/mode/sql"); editor.setOptions({ enableBasicAutocompletion: true, fontSize: "12px" }); editor.setValue("SELECT * FROM customers;"); const response = await fetch("/api/session", { method: "POST" }); if (!response.ok) throw new Error("Failed to create session"); sessionId = (await response.json()).session_id; const domainResponse = await fetch("/api/databases"); if (!domainResponse.ok) throw new Error("Failed to fetch databases"); const domains = (await domainResponse.json()).databases; const domainSelect = document.getElementById("domainSelect"); domainSelect.innerHTML = ''; if (domains.length === 0) { showError("No databases available. Check server configuration."); return; } domains.forEach((domain) => { const option = document.createElement("option"); option.value = domain; option.textContent = domain.charAt(0).toUpperCase() + domain.slice(1); domainSelect.appendChild(option); }); // Enable the load button after populating domains document.getElementById("loadSchemaBtn").disabled = false; document .getElementById("loadSchemaBtn") .addEventListener("click", loadDomain); document .getElementById("showSchemaBtn") .addEventListener("click", showSchema); document.getElementById("showTableBtn").addEventListener("click", showTable); document .getElementById("difficultySelect") .addEventListener("change", loadQuestions); document.getElementById("hintBtn").addEventListener("click", toggleHint); document .getElementById("solutionBtn") .addEventListener("click", toggleSolution); document.getElementById("prevBtn").addEventListener("click", prevQuestion); document.getElementById("nextBtn").addEventListener("click", nextQuestion); document.getElementById("runQueryBtn").addEventListener("click", runQuery); } async function loadDomain() { const domain = document.getElementById("domainSelect").value; if (!domain) { showError("Please select a database."); return; } const loadBtn = document.getElementById("loadSchemaBtn"); loadBtn.disabled = true; loadBtn.textContent = "Loading..."; try { const response = await fetch(`/api/load-schema/${domain}`, { method: "POST", headers: { "session-id": sessionId }, }); if (!response.ok) throw new Error( (await response.json()).detail || "Failed to load database" ); await response.json(); document.getElementById("difficultySelect").disabled = false; document.getElementById("showSchemaBtn").classList.remove("hidden"); document.getElementById("showTableBtn").classList.remove("hidden"); document.getElementById("schemaInfo").classList.add("hidden"); document.getElementById("questionDetails").innerHTML = ""; document.getElementById("hintBtn").style.display = "block"; document.getElementById("solutionBtn").style.display = "block"; currentQuestions = []; currentQuestionIndex = 0; } catch (e) { showError(`Failed to load database: ${e.message}.`); } finally { loadBtn.disabled = false; loadBtn.textContent = "Load Database"; } } async function showSchema() { const domain = document.getElementById("domainSelect").value; if (!domain) return; const schemaInfo = document.getElementById("schemaInfo"); if (isTableVisible) { isTableVisible = false; schemaInfo.classList.add("hidden"); } schemaInfo.classList.toggle("hidden"); isSchemaVisible = !schemaInfo.classList.contains("hidden"); if (isSchemaVisible) { const schemaResponse = await fetch(`/api/schema/${domain}`, { headers: { "session-id": sessionId }, }); if (!schemaResponse.ok) throw new Error( (await schemaResponse.json()).detail || "Failed to load schema" ); const schema = (await schemaResponse.json()).schema; let schemaHtml = '
${col} | `) ); tableHtml += "
---|
${row[col] || "NULL"} | `) ); tableHtml += "
No questions available for this difficulty.
"; document.getElementById("hintBtn").style.display = "none"; document.getElementById("solutionBtn").style.display = "none"; document.getElementById("navButtons").style.display = "none"; currentQuestions = []; currentQuestionIndex = 0; } } function updateQuestionDisplay() { const questionDetails = document.getElementById("questionDetails"); if ( currentQuestions.length && currentQuestionIndex >= 0 && currentQuestionIndex < currentQuestions.length ) { const question = currentQuestions[currentQuestionIndex]; questionDetails.innerHTML = `Practice Question: ${ question.description || "No question available." }
`; } else { questionDetails.innerHTML = 'No questions available.
'; } } function prevQuestion() { if (currentQuestions.length && currentQuestionIndex > 0) { currentQuestionIndex--; updateQuestionDisplay(); updateHintSolutionDisplay(); } } function nextQuestion() { if ( currentQuestions.length && currentQuestionIndex < currentQuestions.length - 1 ) { currentQuestionIndex++; updateQuestionDisplay(); updateHintSolutionDisplay(); } } function updateHintSolutionDisplay() { if (isHintVisible) toggleHint(); if (isSolutionVisible) toggleSolution(); } function toggleHint() { const question = currentQuestions[currentQuestionIndex]; const hintBtn = document.getElementById("hintBtn"); const questionDetails = document.getElementById("questionDetails"); if (isSolutionVisible) toggleSolution(); if (question && question.hint) { if (hintBtn.textContent === "Show Hint") { questionDetails.innerHTML += `Hint: ${question.hint}
`; hintBtn.textContent = "Hide Hint"; isHintVisible = true; } else { questionDetails.innerHTML = questionDetails.innerHTML.replace( `Hint: ${question.hint}
`, "" ); hintBtn.textContent = "Show Hint"; isHintVisible = false; } } else { if (hintBtn.textContent === "Show Hint") { questionDetails.innerHTML += 'No hint available.
'; hintBtn.textContent = "Hide Hint"; isHintVisible = true; } else { questionDetails.innerHTML = questionDetails.innerHTML.replace( 'No hint available.
', "" ); hintBtn.textContent = "Show Hint"; isHintVisible = false; } } } function toggleSolution() { const question = currentQuestions[currentQuestionIndex]; const solutionBtn = document.getElementById("solutionBtn"); const questionDetails = document.getElementById("questionDetails"); if (isHintVisible) toggleHint(); if (question && question.expected_sql) { if (solutionBtn.textContent === "Show Solution") { questionDetails.innerHTML += `Solution: ${question.expected_sql}
Solution: ${question.expected_sql}
No solution available.
'; solutionBtn.textContent = "Hide Solution"; isSolutionVisible = true; } else { questionDetails.innerHTML = questionDetails.innerHTML.replace( 'No solution available.
', "" ); solutionBtn.textContent = "Show Solution"; isSolutionVisible = false; } } } async function runQuery() { const runBtn = document.getElementById("runQueryBtn"); const resultsDiv = document.getElementById("results"); let resultMessage = document.getElementById("resultMessage") || document.createElement("span"); if (!resultMessage.id) { resultMessage.id = "resultMessage"; runBtn.parentNode.appendChild(resultMessage); } runBtn.disabled = true; runBtn.textContent = "Running..."; resultMessage.textContent = ""; try { if (!editor) throw new Error("Editor not initialized. Refresh the page."); let query = editor.getValue().trim().toLowerCase(); if (!query) throw new Error("Please enter a query."); const domain = document.getElementById("domainSelect").value; if (!domain) throw new Error("Please load a database first."); const schemaResponse = await fetch(`/api/schema/${domain}`, { headers: { "session-id": sessionId }, }); if (!schemaResponse.ok) throw new Error("Failed to load schema for table validation"); const schema = (await schemaResponse.json()).schema; const validTables = Object.keys(schema).map((t) => t.toLowerCase()); const tableNames = extractTableNames(query); if (tableNames.some((table) => !validTables.includes(table))) throw new Error( `Invalid table name. Use only: ${validTables.join(", ")}` ); const response = await fetch("/api/run-query", { method: "POST", headers: { "Content-Type": "application/json", "session-id": sessionId }, body: JSON.stringify({ query }), }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || "Server error occurred."); } const result = await response.json(); if (result.columns) { let html = `${col} | `) .join("")}
---|
${row[col] || "NULL"} | `) .join("")}
No results
"; } if ( currentQuestions.length && currentQuestionIndex >= 0 && currentQuestionIndex < currentQuestions.length ) await validateQuery(query, result, resultsDiv); else { resultMessage.textContent = "Select a question first"; resultMessage.className = "text-red-500 ml-4"; } } catch (e) { resultsDiv.innerHTML = ""; resultMessage.textContent = e.message.includes("Internal Server Error") ? "Server error: Please check the query or try again later." : e.message; resultMessage.className = "text-red-500 ml-4"; } finally { runBtn.disabled = false; runBtn.textContent = "Run"; } } function extractTableNames(query) { const tables = new Set(); const tokens = query.replace(/(\s+)/g, " ").split(" "); let inSubquery = false, inOpenQuery = false, inValues = false; for (let i = 0; i < tokens.length; i++) { const token = tokens[i].toLowerCase(); if (token === "(" && !inSubquery && !inValues) { if (i > 0 && tokens[i - 1].toLowerCase() === "values") inValues = true; else inSubquery = true; } if (token === ")" && (inSubquery || inValues)) { if ( inValues && i + 1 < tokens.length && tokens[i + 1].toLowerCase() === "as" ) inValues = false; else if (inSubquery) inSubquery = false; } if (token === "openquery" && i + 1 < tokens.length && tokens[i + 1] === "(") inOpenQuery = true; if (token === ")" && inOpenQuery) inOpenQuery = false; if (inOpenQuery) continue; if ( [ "from", "join", "update", "delete", "insert", "into", "using", "apply", "pivot", "table", ].includes(token) ) { let nextToken = tokens[i + 1] ? tokens[i + 1].replace(/[,;)]/g, "").toLowerCase() : ""; if ( nextToken && ![ "select", "where", "on", "order", "group", "having", "as", "(", ].includes(nextToken) ) { if (i + 2 < tokens.length && tokens[i + 2].toLowerCase() === "as") nextToken = nextToken; else if ( !["left", "right", "inner", "outer", "cross", "full"].includes( nextToken ) ) tables.add(nextToken); } i++; } else if ( token === "merge" && i + 1 < tokens.length && tokens[i + 1].toLowerCase() === "into" ) { let nextToken = tokens[i + 2] ? tokens[i + 2].replace(/[,;)]/g, "").toLowerCase() : ""; if (nextToken && !["using", "select", "where"].includes(nextToken)) tables.add(nextToken); i += 2; while (i + 1 < tokens.length && tokens[i + 1].toLowerCase() !== "using") i++; if (i + 2 < tokens.length) { nextToken = tokens[i + 2].replace(/[,;)]/g, "").toLowerCase(); if (nextToken && !["select", "where"].includes(nextToken)) tables.add(nextToken); } } else if ( token === "select" && i + 1 < tokens.length && tokens[i + 1].toLowerCase() === "into" ) { let nextToken = tokens[i + 2] ? tokens[i + 2].replace(/[,;)]/g, "").toLowerCase() : ""; if (nextToken && !["from", "select"].includes(nextToken)) tables.add(nextToken); i += 2; while (i + 1 < tokens.length && tokens[i + 1].toLowerCase() !== "from") i++; if (i + 2 < tokens.length) { nextToken = tokens[i + 2].replace(/[,;)]/g, "").toLowerCase(); if (nextToken && !["where", "join"].includes(nextToken)) tables.add(nextToken); } } else if (token === "with") { let cteStart = i + 1; while (i + 1 < tokens.length && tokens[i + 1].toLowerCase() !== "as") i++; if (i + 2 < tokens.length && tokens[i + 2] === "(") { let bracketCount = 1, subqueryStart = i + 2; while (i + 1 < tokens.length && bracketCount > 0) { i++; if (tokens[i] === "(") bracketCount++; if (tokens[i] === ")") bracketCount--; } const subquery = tokens.slice(subqueryStart, i).join(" "); tables.add( ...extractTableNames(subquery).filter((t) => !tables.has(t)) ); } } else if ( token === "values" && i + 1 < tokens.length && tokens[i + 1] === "(" ) { let aliasStart = i + 1; while (i + 1 < tokens.length && tokens[i + 1] !== "as") i++; if (i + 2 < tokens.length) { let alias = tokens[i + 2].replace(/[,;)]/g, "").toLowerCase(); if (alias) tables.add(alias); } } else if (["exists", "in"].includes(token)) { let subqueryStart = i + 1; while (i + 1 < tokens.length && tokens[i + 1] !== ")") i++; if (i > subqueryStart) { const subquery = tokens.slice(subqueryStart, i + 1).join(" "); tables.add( ...extractTableNames(subquery).filter((t) => !tables.has(t)) ); } } } return Array.from(tables); } async function validateQuery(query, runResult, resultsDiv) { const question = currentQuestions[currentQuestionIndex]; if (!question || !question.expected_sql) { showError("No question or expected SQL available for validation."); return; } const response = await fetch("/api/validate", { method: "POST", headers: { "Content-Type": "application/json", "session-id": sessionId }, body: JSON.stringify({ user_query: query, expected_query: question.expected_sql, }), }); if (!response.ok) throw new Error((await response.json()).detail || "Failed to validate"); const result = await response.json(); const questionText = document.getElementById("questionText"); const resultMessage = document.getElementById("resultMessage"); if (result.valid) { questionText.classList.remove("text-red-500"); questionText.classList.add("text-green-500"); resultMessage.textContent = "Correct answer!"; resultMessage.className = "text-green-500 ml-4"; if (runResult.columns) { let html = `${col} | `) .join("")}
---|
${row[col] || "NULL"} | `) .join("")}
No results
"; } } else { questionText.classList.remove("text-green-500"); questionText.classList.add("text-red-500"); resultMessage.textContent = "Incorrect answer!"; resultMessage.className = "text-red-500 ml-4"; resultsDiv.innerHTML = ""; } } function showError(message) { let errorMessage = document.getElementById("errorMessage") || document.createElement("span"); if (!errorMessage.id) { errorMessage.id = "errorMessage"; document.getElementById("runQueryBtn").parentNode.appendChild(errorMessage); } errorMessage.textContent = message; errorMessage.className = "text-red-500 mt-2"; } window.onload = init;