/*
SURVEY CODE (CODE FOR BUILDING SURVEY)
*/
// Select DOM elements for question types and survey area
const questionTypes = document.getElementById('question-types');
const surveyArea = document.getElementById('survey-area');
let savestatus = false;
window.addEventListener("beforeunload", function (event) {
const ques = document.querySelectorAll('#survey-area .question');
if (ques.length > 0 && savestatus == false) {
event.preventDefault();
}
});
const sortableSurvey = new Sortable(surveyArea, {
group: 'survey', // Enable drag and drop within this group
animation: 150, // Animation speed when moving elements
handle: '.drag-handle', // Set the handle for dragging
onAdd: function (evt) {
// This event is triggered when a new question is added from the list
const questionType = evt.item.dataset.type; // Get the type of question
const newQuestion = createQuestion(questionType); // Create a new question element
evt.item.replaceWith(newQuestion); // Replace placeholder with actual question
},
});
// Initialize Sortable.js for the question types container to enable dragging
new Sortable(questionTypes, {
group: {
name: 'survey',
pull: 'clone',
put: false,
},
sort: false,
animation: 150,
filter: ".no-drag, button", // Exclude buttons from dragging
onStart: function(evt) {
if (evt.item.matches("button")) {
evt.preventDefault(); // Prevent dragging
}
}
});
function createQuestion(type) {
savestatus = false;
const question = document.createElement('div'); // Create a div for the question
question.className = 'question'; // Set class for styling
question.dataset.type = type; // Keep the data-type
// Create the custom handle
const dragHandle = document.createElement('span');
dragHandle.className = 'drag-handle';
dragHandle.innerHTML = '⋮'; // A simple icon for the drag handle
dragHandle.style.cursor = 'move'; // Show the grab cursor for the handle
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-danger btn-sm';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => question.remove());
const questionLabel = document.createElement('strong');
let inputField = null;
if (type === 'new-page' || type === 'add-video' || type === 'add-image' || type === 'add-text') {
// For the 'new-page' component, only display "New Page" and no input fields
if (type === 'new-page') {
questionLabel.textContent = 'New Page';
question.appendChild(dragHandle); // Add the custom handle
question.appendChild(questionLabel);
question.appendChild(deleteButton); // Append the delete button
} else if (type === 'add-video') {
questionLabel.textContent = 'Add Video';
const questionText = document.createElement('input');
questionText.type = 'text';
questionText.placeholder = 'Enter your video link here';
questionText.className = 'form-control mb-2';
question.appendChild(dragHandle); // Add the custom handle
question.appendChild(questionLabel);
question.appendChild(deleteButton); // Append the delete button
question.appendChild(questionText);
} else if (type === 'add-image') {
questionLabel.textContent = 'Add Image';
const questionText = document.createElement('input');
questionText.type = 'text';
questionText.placeholder = 'Enter your image link here';
questionText.className = 'form-control mb-2';
question.appendChild(dragHandle); // Add the custom handle
question.appendChild(questionLabel);
question.appendChild(deleteButton); // Append the delete button
question.appendChild(questionText);
} else if (type === 'add-text') {
questionLabel.textContent = 'Add Text';
const questionText = document.createElement('input');
questionText.type = 'text';
questionText.placeholder = 'Enter the text you want to appear here';
questionText.className = 'form-control mb-2';
question.appendChild(dragHandle); // Add the custom handle
question.appendChild(questionLabel);
question.appendChild(deleteButton); // Append the delete button
question.appendChild(questionText);
}
} else {
if (type === 'multiple-choice') {
questionLabel.textContent = 'Multiple-choice Question';
} else if (type === 'multiselect') {
questionLabel.textContent = 'Multiselect Question';
} else if (type === 'slider') {
questionLabel.textContent = 'Slider Question';
} else if (type === 'text-input') {
questionLabel.textContent = 'Text-Input Question';
} else if (type === 'selectbox') {
questionLabel.textContent = 'Select Box Question';
} else if (type === 'likert') {
questionLabel.textContent = 'Likert Question'
}
const questionText = document.createElement('input');
questionText.type = 'text';
questionText.placeholder = 'Enter your question here';
questionText.className = 'form-control mb-2';
// Append the question label before the text input
question.appendChild(dragHandle); // Add the custom handle
question.appendChild(questionLabel);
question.appendChild(deleteButton); // Append the delete button
question.appendChild(questionText); // Append the text input to the question div
// Handle specific question types
if (type === 'multiple-choice') {
inputField = createMultipleChoiceOptions();
} else if (type === 'multiselect') {
inputField = createMultiselectOptions();
} else if (type === 'slider') {
inputField = createSliderOptions();
} else if (type === 'selectbox') {
inputField = createSelecBoxtOptions();
}
}
// Append the drag handle and delete button
if (inputField) {
question.appendChild(inputField); // Append the input field if applicable
}
return question;
}
// Helper function for multiple-choice options (unchanged)
function createMultipleChoiceOptions() {
const inputField = document.createElement('div');
function createOptionInput(placeholder) {
const optionContainer = document.createElement('div');
optionContainer.className = 'input-group mb-1';
const optionInput = document.createElement('input');
optionInput.type = 'text';
optionInput.placeholder = placeholder;
optionInput.className = 'form-control';
const deleteOptionButton = document.createElement('button');
deleteOptionButton.className = 'btn btn-danger btn-sm';
deleteOptionButton.textContent = 'X';
deleteOptionButton.addEventListener('click', () => optionContainer.remove());
const inputGroupAppend = document.createElement('div');
inputGroupAppend.className = 'input-group-append';
inputGroupAppend.appendChild(deleteOptionButton);
optionContainer.appendChild(optionInput);
optionContainer.appendChild(inputGroupAppend);
return optionContainer;
}
inputField.appendChild(createOptionInput('Option 1'));
inputField.appendChild(createOptionInput('Option 2'));
const addOptionButton = document.createElement('button');
addOptionButton.className = 'btn btn-secondary btn-sm mb-2';
addOptionButton.textContent = 'Add Option';
addOptionButton.addEventListener('click', () => {
const optionCount = inputField.querySelectorAll('.input-group').length + 1;
inputField.insertBefore(createOptionInput(`Option ${optionCount}`), addOptionButton);
});
inputField.appendChild(addOptionButton);
return inputField;
}
// Helper function for multiselect options
function createMultiselectOptions() {
const inputField = document.createElement('div');
function createMultiselectOption(placeholder) {
const multiselectContainer = document.createElement('div');
multiselectContainer.className = 'input-group2 mb-1';
const labelInput = document.createElement('input');
labelInput.type = 'text';
labelInput.className = 'form-control-sm';
labelInput.placeholder = placeholder;
const deleteOptionButton = document.createElement('button');
deleteOptionButton.className = 'btn btn-danger btn-sm';
deleteOptionButton.textContent = 'X';
deleteOptionButton.addEventListener('click', () => multiselectContainer.remove());
multiselectContainer.appendChild(labelInput);
multiselectContainer.appendChild(deleteOptionButton);
return multiselectContainer;
}
inputField.appendChild(createMultiselectOption('Option 1'));
inputField.appendChild(createMultiselectOption('Option 2'));
const addmultiselectButton = document.createElement('button');
addmultiselectButton.className = 'btn btn-secondary btn-sm mb-2';
addmultiselectButton.textContent = 'Add Option';
addmultiselectButton.addEventListener('click', () => {
const optionCount = inputField.querySelectorAll('.input-group2').length + 1;
inputField.insertBefore(createMultiselectOption(`Option ${optionCount}`), addmultiselectButton);
});
inputField.appendChild(addmultiselectButton);
return inputField;
}
// Helper function for slider options
// Helper function to create slider options
function createSliderOptions() {
const inputField = document.createElement('div');
// Create a container for the slider labels
const sliderLabels = document.createElement('div');
sliderLabels.className = 'd-flex justify-content-between mb-2';
inputField.appendChild(sliderLabels);
// Create input fields for the min and max values of the slider with labels
const minMaxContainer = document.createElement('div');
minMaxContainer.className = 'd-flex justify-content-between mb-2';
// Min Value Label and Input
const minValueWrapper = document.createElement('div');
const minValueLabel = document.createElement('label');
minValueLabel.innerText = 'Min Value';
minValueLabel.className = 'form-label';
const minValueInput = document.createElement('input');
minValueInput.type = 'number';
minValueInput.className = 'form-control';
minValueInput.placeholder = 'Min Value';
minValueInput.value = 0; // Default min value
minValueWrapper.appendChild(minValueLabel);
minValueWrapper.appendChild(minValueInput);
// Max Value Label and Input
const maxValueWrapper = document.createElement('div');
const maxValueLabel = document.createElement('label');
maxValueLabel.innerText = 'Max Value';
maxValueLabel.className = 'form-label';
const maxValueInput = document.createElement('input');
maxValueInput.type = 'number';
maxValueInput.className = 'form-control';
maxValueInput.placeholder = 'Max Value';
maxValueInput.value = 10; // Default max value
maxValueWrapper.appendChild(maxValueLabel);
maxValueWrapper.appendChild(maxValueInput);
minMaxContainer.appendChild(minValueWrapper);
minMaxContainer.appendChild(maxValueWrapper);
// Create a warning message for invalid input
const warningMessage = document.createElement('small');
warningMessage.style.color = 'red';
warningMessage.style.display = 'none';
warningMessage.innerText = 'Max value must be greater than Min value';
inputField.appendChild(minMaxContainer);
inputField.appendChild(warningMessage);
// Add event listeners to check max > min validation
function validateMinMax() {
const min = parseFloat(minValueInput.value);
const max = parseFloat(maxValueInput.value);
if (max <= min) {
warningMessage.style.display = 'block'; // Show the warning message
} else {
warningMessage.style.display = 'none'; // Hide the warning message
}
}
// Listen for changes in the min and max input fields
minValueInput.addEventListener('input', validateMinMax);
maxValueInput.addEventListener('input', validateMinMax);
return inputField;
}
// Helper function for multiselect options
function createSelecBoxtOptions() {
const inputField = document.createElement('div');
function createSelectBoxtOption(placeholder) {
const multiselectContainer = document.createElement('div');
multiselectContainer.className = 'input-group2 mb-1';
const labelInput = document.createElement('input');
labelInput.type = 'text';
labelInput.className = 'form-control-sm';
labelInput.placeholder = placeholder;
const deleteOptionButton = document.createElement('button');
deleteOptionButton.className = 'btn btn-danger btn-sm';
deleteOptionButton.textContent = 'X';
deleteOptionButton.addEventListener('click', () => multiselectContainer.remove());
multiselectContainer.appendChild(labelInput);
multiselectContainer.appendChild(deleteOptionButton);
return multiselectContainer;
}
inputField.appendChild(createSelectBoxtOption('Option 1'));
inputField.appendChild(createSelectBoxtOption('Option 2'));
const addmultiselectButton = document.createElement('button');
addmultiselectButton.className = 'btn btn-secondary btn-sm mb-2';
addmultiselectButton.textContent = 'Add Option';
addmultiselectButton.addEventListener('click', () => {
const optionCount = inputField.querySelectorAll('.input-group2').length + 1;
inputField.insertBefore(createSelectBoxtOption(`Option ${optionCount}`), addmultiselectButton);
});
inputField.appendChild(addmultiselectButton);
return inputField;
}
/*
SAVING CODE (CODE FOR SAVING SURVEY TO PYTHON FILE)
*/
// Function to save the survey as a text file
// Function to save the survey as a Python file
const exportModalElement = document.getElementById('exportModal');
const githubModalElement = document.getElementById('githubModal');
const hfModalElement = document.getElementById('hfModal');
const exportButton = document.getElementById('save-survey');
// Event listener for opening the export modal
document.getElementById('save-survey').addEventListener('click', () => {
const exportModal = new bootstrap.Modal(exportModalElement, {
backdrop: 'static',
keyboard: false,
focus: true
});
exportModalElement.removeAttribute('inert'); // Allow interaction
exportModal.show();
});
// Event listener for local download
// Prompts the user to enter their sepolia URL and private key
document.getElementById('download-btn').addEventListener('click', () => {
const surveyPythonCode = buildSurveyPythonCode(); // Get Python survey code
downloadSurvey(surveyPythonCode); // Trigger local download
savestatus = true;
const exportModal = bootstrap.Modal.getInstance(exportModalElement);
exportModal.hide(); // Close the modal
});
// Event listener for opening the GitHub modal
document.getElementById('upload-btn-git').addEventListener('click', () => {
const exportModal = bootstrap.Modal.getInstance(exportModalElement);
exportModal.hide();
const githubModal = new bootstrap.Modal(githubModalElement, {
backdrop: 'static',
keyboard: false,
focus: true
});
githubModalElement.removeAttribute('inert'); // Allow interaction
githubModal.show();
});
document.getElementById('upload-btn-hf').addEventListener('click', () => {
const exportModal = bootstrap.Modal.getInstance(exportModalElement);
exportModal.hide();
const hfModal = new bootstrap.Modal(hfModalElement, {
backdrop: 'static',
keyboard: false,
focus: true
});
hfModalElement.removeAttribute('inert'); // Allow interaction
hfModal.show();
});
// Event listener for uploading to GitHub
document.getElementById('submit-upload-git').addEventListener('click', async () => {
const repoId = document.getElementById('repo-id-git').value.trim();
const accessToken = document.getElementById('access-token-git').value.trim();
const submitButton = document.getElementById('submit-upload-git');
const loadingSpinner = document.getElementById('loading-spinner-git'); // Reference to loading spinner
if (!repoId || !accessToken) {
alert("Please enter both a repository name and an access token.");
return;
}
if ((document.querySelectorAll('#survey-area .question')).length == 0) {
alert("No Content added to Canvas. Please add Components before uploading to Github");
return;
}
// Show loading spinner and disable the submit button
submitButton.disabled = true;
loadingSpinner.style.display = "inline-block";
try {
const surveyPythonCode = buildSurveyPythonCode(); // Get survey Python code
const requirements = `web3==7.8.0\nrequests==2.31.0\nstreamlit==1.28.2`;
const [username, repoName] = repoId.split("/");
if (!username || !repoName) {
alert("Invalid repository format. Use 'username/repository'.");
} else {
await uploadReqToGitHub(repoId, accessToken, requirements);
await uploadToGitHub(repoId, accessToken, surveyPythonCode);
}
// Close the GitHub modal after successful upload
const githubModal = bootstrap.Modal.getInstance(document.getElementById('githubModal'));
githubModal.hide();
} catch (error) {
console.error("Upload failed:", error);
alert("Failed to upload. Make sure your repository exists and your access token is correct");
} finally {
// Hide loading spinner and re-enable the button after upload completes
loadingSpinner.style.display = "none";
submitButton.disabled = false;
}
});
// Ensure modals use inert when hidden
exportModalElement.addEventListener('hidden.bs.modal', () => {
exportModalElement.setAttribute('inert', 'true');
exportButton.focus();
});
githubModalElement.addEventListener('hidden.bs.modal', () => {
githubModalElement.setAttribute('inert', 'true'); // Disable interaction
});
hfModalElement.addEventListener('hidden.bs.modal', () => {
hfModalElement.setAttribute('inert', 'true'); // Disable interaction
});
// Function to trigger local download
function downloadSurvey(content) {
const blob = new Blob([content], { type: 'text/plain' });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "app.py";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Function to upload survey to GitHub
async function uploadToGitHub(repoId, accessToken, content) {
const [username, repoName] = repoId.split("/");
const encodedContent = btoa(unescape(encodeURIComponent(content)));
const fileName = "survey.py";
const apiUrl = `https://api.github.com/repos/${username}/${repoName}/contents/${fileName}`;
try {
// Check if file already exists
let sha = null;
const existingFile = await fetch(apiUrl, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (existingFile.ok) {
const fileData = await existingFile.json();
sha = fileData.sha;
}
// Upload or update file
const response = await fetch(apiUrl, {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
message: "Upload survey script",
content: encodedContent,
branch: "main",
sha: sha // Include sha if updating an existing file
})
});
if (!response.ok) throw new Error(`GitHub API error: ${response.statusText}`);
savestatus = true;
const result = await response.json();
alert(`Survey uploaded successfully: ${result.content.html_url}`);
} catch (error) {
console.error("Upload failed:", error);
alert("Failed to upload. Make sure your repository exists and your access token is correct");
}
}
async function uploadReqToGitHub(repoId, accessToken, content) {
const [username, repoName] = repoId.split("/");
const encodedContent = btoa(unescape(encodeURIComponent(content)));
const fileName = "requirements.txt";
const apiUrl = `https://api.github.com/repos/${username}/${repoName}/contents/${fileName}`;
try {
// Check if file already exists
let sha = null;
const existingFile = await fetch(apiUrl, {
headers: { Authorization: `Bearer ${accessToken}` }
});
if (existingFile.ok) {
const fileData = await existingFile.json();
sha = fileData.sha;
}
// Upload or update file
const response = await fetch(apiUrl, {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
message: "Upload requirements file",
content: encodedContent,
branch: "main",
sha: sha // Include sha if updating an existing file
})
});
if (!response.ok) throw new Error(`GitHub API error: ${response.statusText}`);
savestatus = true;
const result = await response.json();
} catch (error) {
console.error("Error:", error);
}
}
function buildSurveyPythonCode() {
let pythonCode = ''; // Initialize the Python code content
const questions = document.querySelectorAll('#survey-area .question');
let questionIndex = 1; // Track question index
let pageIndex = 1; // Track page index
pythonCode += formatPreCode(questions);
let currentPageQuestions = []; // List to hold the questions for each page
let currentPagIndicies = []; // List to hold the question indices for each page
questions.forEach((question) => {
const questionType = question.querySelector('strong').textContent.replace(' Question', '');
// Detect if it's a New Page component
if (questionType.toLowerCase() === 'new page') {
if (pageIndex == 1) {
pythonCode += formatFirstPageCode(currentPagIndicies, currentPageQuestions)
} else {
pythonCode += formatPostCode(currentPagIndicies, currentPageQuestions, pageIndex); // Pass the current page's questions for validation
}
pageIndex++; // Increment the page index
pythonCode += `elif st.session_state["current_page"] == ${pageIndex}:\n\n`; // Set up the next page logic
currentPageQuestions = []; // Reset for the new page
currentPagIndicies = [];
} else if (questionType.toLowerCase() === 'add video' || questionType.toLowerCase() === 'add image' || questionType.toLowerCase() === 'add text') {
const questionText = question.querySelector('input[type="text"]').value;
if (questionType.toLowerCase() === 'add video') {
pythonCode += formatVideoPython(questionText);
} else if (questionType.toLowerCase() === 'add image') {
pythonCode += formatImagePython(questionText);
} else if (questionType.toLowerCase() === 'add text') {
pythonCode += formatTextPython(questionText);
}
} else {
const questionText = question.querySelector('input[type="text"]').value;
// Handle multiple-choice questions for now
if (questionType.toLowerCase() === 'multiple-choice') {
pythonCode += formatMultipleChoicePython(questionText, question, questionIndex);
pythonCode += ` st.markdown("""
""", unsafe_allow_html=True)\n\n\n`;
} else if (questionType.toLowerCase() === 'text-input') {
pythonCode += formatTextQuestionPython(questionText, question, questionIndex);
pythonCode += ` st.markdown("""
""", unsafe_allow_html=True)\n\n\n`;
} else if (questionType.toLowerCase() === 'multiselect') {
pythonCode += formatMultiselectPython(questionText, question, questionIndex);
pythonCode += ` st.markdown("""
""", unsafe_allow_html=True)\n\n\n`;
} else if (questionType.toLowerCase() === 'slider') {
pythonCode += formatSliderQuestionsPython(questionText, question, questionIndex);
pythonCode += ` st.markdown("""
""", unsafe_allow_html=True)\n\n\n`;
} else if (questionType.toLowerCase() === 'select box') {
pythonCode += formatSelectBoxPython(questionText, question, questionIndex);
pythonCode += ` st.markdown("""
""", unsafe_allow_html=True)\n\n\n`;
} else if (questionType.toLowerCase() === 'likert') {
pythonCode += formatlikert(questionText, question, questionIndex);
pythonCode += ` st.markdown("""
""", unsafe_allow_html=True)\n\n\n`;
}
// Add the question index to the current page's questions list
currentPagIndicies.push(questionIndex);
currentPageQuestions.push(questionType);
questionIndex++; // Increment the question index
}
});
// Finalize the last page's code
pythonCode += formatPostCode(currentPagIndicies, currentPageQuestions, pageIndex);
pythonCode += formatFinalPageCode(questions, pageIndex);
return pythonCode;
}
function formatFinalPageCode(questions, pageCount) {
pythonCode = ``;
pythonCode += `elif st.session_state["current_page"] == ${pageCount+1}: # Last Page\n`
pythonCode += ` st.markdown('
Thank you for participating!
Click on the button below to submit your answers.
${surveyDescription}
""", unsafe_allow_html=True)\n\n` return pythonCode; } function formatRadioOptions(questions) { let pythonCode = ''; // Initialize Python code for options let questionIndex = 1; questions.forEach(question => { const questionType = question.querySelector('strong').textContent.replace(' Question', ''); if (questionType.toLowerCase() === 'multiple-choice' || questionType.toLowerCase() === 'multiselect' || questionType.toLowerCase() === 'select box') { const options = []; let optionInputs; if (questionType.toLowerCase() === 'multiple-choice') { optionInputs = question.querySelectorAll('.input-group .form-control'); } else if (questionType.toLowerCase() === 'multiselect' || questionType.toLowerCase() === 'select box') { optionInputs = question.querySelectorAll('.input-group2 .form-control-sm'); } optionInputs.forEach(optionInput => { const optionText = optionInput.value.trim(); if (optionText) { options.push(optionText); // Add the non-empty options } }); const optionsVariable = `Q${questionIndex}_radio_options`; if (options.length === 0) { options.push(""); } pythonCode += `${optionsVariable} = ${JSON.stringify(options)}\n`; questionIndex++; } else if (questionType.toLowerCase() === 'slider' || questionType.toLowerCase() === 'text-input') { questionIndex++; } else if (questionType.toLowerCase() === 'likert') { pythonCode += `Q${questionIndex}_radio_options = ["N/A", "Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"]\n` questionIndex++; } }); return pythonCode; // Return the formatted Python code for radio options } function formatTextQuestionPython(questionText, question, index) { let pythonCode = ''; // Initialize the Python code for this question pythonCode += ` st.text_area(label = "${questionText}", \n`; pythonCode += ` value= "" if st.session_state["Q${index}"] == None else st.session_state["Q${index}"],\n` pythonCode += ` key = 'Q${index}_text', \n` pythonCode += ` on_change = answer_change,\n` pythonCode += ` args = ( "Q${index}", "Q${index}_text",))\n\n`; return pythonCode; // Return the formatted Python code for this question } function formatMultiselectPython(questionText, question, index) { let pythonCode = ''; // Initialize the Python code for this question // Generate the st.radio function pythonCode += ` st.multiselect(label = "${questionText}", \n`; pythonCode += ` default = None if st.session_state["Q${index}"] == None else st.session_state["Q${index}"], \n`; pythonCode += ` options = Q${index}_radio_options, \n`; pythonCode += ` key = 'Q${index}_multi', \n`; pythonCode += ` on_change = multi_change, \n`; pythonCode += ` args = (Q${index}_radio_options, "Q${index}", "Q${index}_multi",))\n\n`; return pythonCode; // Return the formatted Python code for this question } function formatSliderQuestionsPython(questions, question, index) { let pythonCode = ''; // Initialize Python code const questionType = question.querySelector('strong').textContent.replace(' Question', ''); const questionText = question.querySelector('input[type="text"]').value; const minValue = question.querySelector('input[placeholder="Min Value"]').value || 0; const maxValue = question.querySelector('input[placeholder="Max Value"]').value || 10; const defaultValue = Math.ceil((parseInt(minValue) + parseInt(maxValue)) / 2); pythonCode += ` if st.session_state["Q${index}"] == None:\n` pythonCode += ` st.session_state["Q${index}"] = ${defaultValue}\n` pythonCode += ` st.slider(label="${questionText}",min_value=${minValue},max_value=${maxValue}, value= st.session_state["Q${index}"], key = "Q${index}_slider", on_change = answer_change, args = ("Q${index}", "Q${index}_slider",))\n`; return pythonCode; } function formatSelectBoxPython(questionText, question, index) { let pythonCode = ''; // Initialize the Python code for this question // Generate the st.radio function pythonCode += ` st.selectbox(label = "${questionText}", \n`; pythonCode += ` options = Q${index}_radio_options, \n`; pythonCode += ` index = None if st.session_state["Q${index}"] == None else st.session_state["Q${index}"], \n`; pythonCode += ` key = 'Q${index}_radio', \n`; pythonCode += ` on_change = radio_change, \n`; pythonCode += ` args = (Q${index}_radio_options, "Q${index}", "Q${index}_radio",))\n\n`; return pythonCode; // Return the formatted Python code for this question } function formatVideoPython(questionText) { let pythonCode = ''; // Initialize the Python code for this question pythonCode += ` st.video("${questionText}") \n`; return pythonCode; // Return the formatted Python code for this video } function formatImagePython(questionText) { let pythonCode = ''; // Initialize the Python code for this question pythonCode += ` st.image("${questionText}") \n`; return pythonCode; // Return the formatted Python code for this video } function formatTextPython(questionText) { let pythonCode = ''; // Initialize the Python code for this question pythonCode += ` st.markdown("""${questionText}
""", unsafe_allow_html=True)\n`; return pythonCode; // Return the formatted Python code for this video } // Function to format a multiple-choice question as Streamlit Python code function formatMultipleChoicePython(questionText, question, index) { let pythonCode = ''; // Initialize the Python code for this question // Generate the st.radio function pythonCode += ` st.radio(label = "${questionText}", \n`; pythonCode += ` options = Q${index}_radio_options, \n`; pythonCode += ` index = None if st.session_state["Q${index}"] == None else st.session_state["Q${index}"], \n`; pythonCode += ` key = 'Q${index}_radio', \n`; pythonCode += ` on_change = radio_change, \n`; pythonCode += ` args = (Q${index}_radio_options, "Q${index}", "Q${index}_radio",))\n\n`; return pythonCode; // Return the formatted Python code for this question } function formatlikert(questionText, question, index) { let pythonCode = ''; // Initialize the Python code for this question // Generate the st.radio function pythonCode += ` st.radio(label = "${questionText}", # Likert\n`; pythonCode += ` options = Q${index}_radio_options, \n`; pythonCode += ` index = None if st.session_state["Q${index}"] == None else st.session_state["Q${index}"], \n`; pythonCode += ` key = 'Q${index}_radio', \n`; pythonCode += ` on_change = radio_change, \n`; pythonCode += ` args = (Q${index}_radio_options, "Q${index}", "Q${index}_radio",))\n\n`; return pythonCode; // Return the formatted Python code for this question } /* IMPORTING CODE (CODE FOR IMPORTING SAVED SURVEY) */ // Function to handle file upload and populate form with pre-made data document.getElementById('import-confirm').addEventListener('click', function () { const fileInput = document.getElementById('import-survey'); const file = fileInput.files[0]; if (file) { const reader = new FileReader(); reader.onload = function(e) { const fileContent = e.target.result; // Parse the Python file content const parsedData = parsePythonSurveyFile(fileContent); // Populate survey form with parsed data populateSurveyForm(parsedData); // Close the modal programmatically after importing const importSurveyModal = bootstrap.Modal.getInstance(document.getElementById('importSurveyModal')); importSurveyModal.hide(); }; reader.readAsText(file); } else { alert("Please select a file to import."); } }); function parsePythonSurveyFile(fileContent) { const surveyData = { title: '', description: '', components: [] }; // Extract title and description const titleMatch = fileContent.match(/st\.title\('(.+?)'\)/); const descriptionMatch = fileContent.match(/st\.markdown\("""(.*?)<\/p>""",\s*unsafe_allow_html=True\)/); surveyData.title = titleMatch ? titleMatch[1] : ''; surveyData.description = descriptionMatch ? descriptionMatch[1] : ''; // Split content by lines const lines = fileContent.split('\n'); let currentRadio = null; for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Detect new-page components if (line.match(/elif\s+st\.session_state\[\s*["']current_page["']\s*\]\s*==/)) { if (line.match(/elif\s+st\.session_state\[\s*["']current_page["']\s*\]\s*==/)["input"].substring(line.length - 9) !== "Last Page") { surveyData.components.push({ type: 'new-page' }); } } // Detect the start of a radio question (captures label) const radioLabelMatch = line.match(/st\.radio\s*\(\s*label\s*=\s*["](.+?)["](?!\s*,?\s*#)/); if (radioLabelMatch) { currentRadio = { type: 'multiple-choice', label: radioLabelMatch[1], options: [], key: null }; continue; // Move to the next line to find options and key } const likertLabelMatch = line.match(/st\.radio\(.*?label\s*=\s*["](.+?)["]/); if (likertLabelMatch) { const selectSliderQuestion = { type: 'likert', label: likertLabelMatch[1], key: likertLabelMatch[2], }; surveyData.components.push(selectSliderQuestion); continue; } const selectboxLabelMatch = line.match(/st\.selectbox\(.*?label\s*=\s*["](.+?)["]/); if (selectboxLabelMatch) { currentRadio = { type: 'selectbox', label: selectboxLabelMatch[1], options: [], key: null }; continue; // Move to the next line to find options and key } const textAreaMatch = line.match(/st\.text_area\s*\(\s*label\s*=\s*["](.+?)["]/i); if (textAreaMatch) { const textQuestion = { type: 'text-area', label: textAreaMatch[1], key: textAreaMatch[2], }; surveyData.components.push(textQuestion); continue; } const multiSelectLabelMatch = line.match(/st\.multiselect\(.*?label\s*=\s*["](.+?)["]/); if (multiSelectLabelMatch) { currentRadio = { type: 'multiselect', label: multiSelectLabelMatch[1], options: [], key: null }; continue; // Move to the next line to find options and key } const sliderLabelMatch = line.match(/st\.slider\s*\(\s*label\s*=\s*["](.+?)["]\s*,\s*min_value\s*=\s*([0-9]+)\s*,\s*max_value\s*=\s*([0-9]+)/); if (sliderLabelMatch) { const sliderQuestion = { type: 'slider', label: sliderLabelMatch[1], minValue: parseInt(sliderLabelMatch[2], 10), maxValue: parseInt(sliderLabelMatch[3], 10), }; surveyData.components.push(sliderQuestion); continue; } const videoLabelMatch = line.match(/^\s*st\.video\s*\(\s*["](.+?)["]\s*\)/); if (videoLabelMatch) { const videoInput = { type: 'add-video', label: videoLabelMatch[1], }; surveyData.components.push(videoInput); continue; } const imageLabelMatch = line.match(/^\s*st\.image\s*\(\s*["](.+?)["]\s*\)/); if (imageLabelMatch) { const imageInput = { type: 'add-image', label: imageLabelMatch[1], }; surveyData.components.push(imageInput); continue; } const textLabelMatch = line.match(/^\s*st\.markdown\s*\(\s*["']{3}
(.+?)<\/p>["']{3}\s*,\s*unsafe_allow_html\s*=\s*True\s*\)/i); if (textLabelMatch) { const textInput = { type: 'add-text', label: textLabelMatch[1], }; surveyData.components.push(textInput); continue; } // If we're inside a radio question, find options and key if (currentRadio) { const optionsMatch = line.match(/options\s*=\s*([\w_]+)_radio_options/); if (optionsMatch) { // Search for the options array in fileContent const optionsVar = optionsMatch[1] + "_radio_options"; const optionsArrayMatch = fileContent.match(new RegExp(`${optionsVar}\\s*=\\s*(\\[.+?\\])`)); currentRadio.options = optionsArrayMatch ? JSON.parse(optionsArrayMatch[1]) : []; } const keyMatch = line.match(/key\s*=\s*["'](.+?)["']/); if (keyMatch) { currentRadio.key = keyMatch[1]; // When we have both options and key, add the question to components surveyData.components.push(currentRadio); currentRadio = null; // Reset for the next radio question } } } return surveyData; } function populateSurveyForm(data) { // Populate title and description if (data.title) { document.getElementById('survey-title').value = data.title; } if (data.description) { document.getElementById('survey-description').value = data.description; } const surveyArea = document.getElementById('survey-area'); surveyArea.innerHTML = ''; // Clear existing form elements // Populate components data.components.forEach((component) => { const componentElement = createQuestionFromPreSavedData(component.type, component); // Append each created component element to the survey area if (componentElement) { surveyArea.appendChild(componentElement); } }); } function createQuestionFromPreSavedData(type, questionData) { const question = document.createElement('div'); question.className = 'question'; question.dataset.type = type; const dragHandle = document.createElement('span'); dragHandle.className = 'drag-handle'; dragHandle.innerHTML = '⋮'; dragHandle.style.cursor = 'move'; const deleteButton = document.createElement('button'); deleteButton.className = 'btn btn-danger btn-sm'; deleteButton.textContent = 'Delete'; deleteButton.addEventListener('click', () => question.remove()); // Create the label element and use the questionData label if available const questionLabel = document.createElement('strong'); questionLabel.textContent = questionData.label || `${type.charAt(0).toUpperCase() + type.slice(1)} Question`; let inputField = null; if (type === 'new-page' || type === 'add-video' || type === 'add-image' || type === 'add-text') { // For the 'new-page' component, only display "New Page" and no input fields if (type === 'new-page') { questionLabel.textContent = 'New Page'; question.appendChild(dragHandle); // Add the custom handle question.appendChild(questionLabel); question.appendChild(deleteButton); // Append the delete button } else if (type === 'add-video') { questionLabel.textContent = 'Add Video'; const questionText = document.createElement('input'); questionText.type = 'text'; questionText.placeholder = 'Enter your video link here'; questionText.className = 'form-control mb-2'; questionText.value = questionData.label || ''; // Set the label as the input value question.appendChild(dragHandle); // Add the custom handle question.appendChild(questionLabel); question.appendChild(deleteButton); // Append the delete button question.appendChild(questionText); } else if (type === 'add-image') { questionLabel.textContent = 'Add Image'; const questionText = document.createElement('input'); questionText.type = 'text'; questionText.placeholder = 'Enter your image link here'; questionText.className = 'form-control mb-2'; questionText.value = questionData.label || ''; // Set the label as the input value question.appendChild(dragHandle); // Add the custom handle question.appendChild(questionLabel); question.appendChild(deleteButton); // Append the delete button question.appendChild(questionText); } else if (type === 'add-text') { questionLabel.textContent = 'Add Text'; const questionText = document.createElement('input'); questionText.type = 'text'; questionText.placeholder = 'Enter the text you want to appear here'; questionText.className = 'form-control mb-2'; questionText.value = questionData.label || ''; // Set the label as the input value question.appendChild(dragHandle); // Add the custom handle question.appendChild(questionLabel); question.appendChild(deleteButton); // Append the delete button question.appendChild(questionText); } } else { const questionText = document.createElement('input'); questionText.type = 'text'; questionText.placeholder = 'Enter your question here'; questionText.className = 'form-control mb-2'; questionText.value = questionData.label || ''; // Set the label as the input value if (type === 'multiple-choice') { questionLabel.textContent = 'Multiple-choice Question'; } else if (type === 'multiselect') { questionLabel.textContent = 'Multiselect Question'; } else if (type === 'slider') { questionLabel.textContent = 'Slider Question'; } else if (type === 'text-area') { questionLabel.textContent = 'Text-Input Question'; } else if (type === 'selectbox') { questionLabel.textContent = 'Select Box Question'; } else if (type === 'likert') { questionLabel.textContent = 'Likert Question'; } question.appendChild(dragHandle); question.appendChild(questionLabel); question.appendChild(deleteButton); question.appendChild(questionText); if (type === 'multiple-choice') { inputField = createMultipleChoiceOptionsFromData(questionData.options); } else if (type === 'multiselect') { inputField = createMultiselectOptionsFromData(questionData.options); } else if (type === 'slider') { inputField = createSliderOptionsFromData(questionData); } else if (type === 'selectbox') { inputField = createSelectBoxOptionsFromData(questionData.options); } } if (inputField) { question.appendChild(inputField); } return question; } function createMultipleChoiceOptionsFromData(options) { const inputField = document.createElement('div'); options.forEach((optionText, index) => { const optionContainer = document.createElement('div'); optionContainer.className = 'input-group mb-1'; const optionInput = document.createElement('input'); optionInput.type = 'text'; optionInput.placeholder = `Option ${index + 1}`; optionInput.className = 'form-control'; optionInput.value = optionText; const deleteOptionButton = document.createElement('button'); deleteOptionButton.className = 'btn btn-danger btn-sm'; deleteOptionButton.textContent = 'X'; deleteOptionButton.addEventListener('click', () => optionContainer.remove()); const inputGroupAppend = document.createElement('div'); inputGroupAppend.className = 'input-group-append'; inputGroupAppend.appendChild(deleteOptionButton); optionContainer.appendChild(optionInput); optionContainer.appendChild(inputGroupAppend); inputField.appendChild(optionContainer); }); const addOptionButton = document.createElement('button'); addOptionButton.className = 'btn btn-secondary btn-sm mb-2'; addOptionButton.textContent = 'Add Option'; addOptionButton.addEventListener('click', () => { const optionCount = inputField.querySelectorAll('.input-group').length + 1; inputField.insertBefore(createOptionInput(`Option ${optionCount}`), addOptionButton); }); inputField.appendChild(addOptionButton); return inputField; } function createOptionInput(placeholder) { const optionContainer = document.createElement('div'); optionContainer.className = 'input-group mb-1'; const optionInput = document.createElement('input'); optionInput.type = 'text'; optionInput.placeholder = placeholder; optionInput.className = 'form-control'; const deleteOptionButton = document.createElement('button'); deleteOptionButton.className = 'btn btn-danger btn-sm'; deleteOptionButton.textContent = 'X'; deleteOptionButton.addEventListener('click', () => optionContainer.remove()); const inputGroupAppend = document.createElement('div'); inputGroupAppend.className = 'input-group-append'; inputGroupAppend.appendChild(deleteOptionButton); optionContainer.appendChild(optionInput); optionContainer.appendChild(inputGroupAppend); return optionContainer; } function createMultiselectOptionsFromData(options) { const inputField = document.createElement('div'); options.forEach((optionText, index) => { const optionContainer = document.createElement('div'); optionContainer.className = 'input-group2 mb-1'; const optionInput = document.createElement('input'); optionInput.type = 'text'; optionInput.placeholder = `Option ${index + 1}`; optionInput.className = 'form-control-sm'; optionInput.value = optionText; const deleteOptionButton = document.createElement('button'); deleteOptionButton.className = 'btn btn-danger btn-sm'; deleteOptionButton.textContent = 'X'; deleteOptionButton.addEventListener('click', () => optionContainer.remove()); const inputGroupAppend = document.createElement('div'); inputGroupAppend.className = 'input-group-append'; inputGroupAppend.appendChild(deleteOptionButton); optionContainer.appendChild(optionInput); optionContainer.appendChild(inputGroupAppend); inputField.appendChild(optionContainer); }); function createMultiselectOption(placeholder) { const multiselectContainer = document.createElement('div'); multiselectContainer.className = 'input-group2 mb-1'; const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.className = 'form-control-sm'; labelInput.placeholder = placeholder; const deleteOptionButton = document.createElement('button'); deleteOptionButton.className = 'btn btn-danger btn-sm'; deleteOptionButton.textContent = 'X'; deleteOptionButton.addEventListener('click', () => multiselectContainer.remove()); multiselectContainer.appendChild(labelInput); multiselectContainer.appendChild(deleteOptionButton); return multiselectContainer; } const addmultiselectButton = document.createElement('button'); addmultiselectButton.className = 'btn btn-secondary btn-sm mb-2'; addmultiselectButton.textContent = 'Add Option'; addmultiselectButton.addEventListener('click', () => { const optionCount = inputField.querySelectorAll('.input-group2').length + 1; inputField.insertBefore(createMultiselectOption(`Option ${optionCount}`), addmultiselectButton); }); inputField.appendChild(addmultiselectButton); return inputField; } function createSliderOptionsFromData(questionData) { const inputField = document.createElement('div'); // Create a container for the slider labels const sliderLabels = document.createElement('div'); sliderLabels.className = 'd-flex justify-content-between mb-2'; inputField.appendChild(sliderLabels); // Create input fields for the min and max values of the slider with labels const minMaxContainer = document.createElement('div'); minMaxContainer.className = 'd-flex justify-content-between mb-2'; // Min Value Label and Input const minValueWrapper = document.createElement('div'); const minValueLabel = document.createElement('label'); minValueLabel.innerText = 'Min Value'; minValueLabel.className = 'form-label'; const minValueInput = document.createElement('input'); minValueInput.type = 'number'; minValueInput.className = 'form-control'; minValueInput.placeholder = 'Min Value'; minValueInput.value = questionData.minValue || 0; // Use min value from questionData or default to 0 minValueWrapper.appendChild(minValueLabel); minValueWrapper.appendChild(minValueInput); // Max Value Label and Input const maxValueWrapper = document.createElement('div'); const maxValueLabel = document.createElement('label'); maxValueLabel.innerText = 'Max Value'; maxValueLabel.className = 'form-label'; const maxValueInput = document.createElement('input'); maxValueInput.type = 'number'; maxValueInput.className = 'form-control'; maxValueInput.placeholder = 'Max Value'; maxValueInput.value = questionData.maxValue || 10; // Use max value from questionData or default to 10 maxValueWrapper.appendChild(maxValueLabel); maxValueWrapper.appendChild(maxValueInput); minMaxContainer.appendChild(minValueWrapper); minMaxContainer.appendChild(maxValueWrapper); // Create a warning message for invalid input const warningMessage = document.createElement('small'); warningMessage.style.color = 'red'; warningMessage.style.display = 'none'; warningMessage.innerText = 'Max value must be greater than Min value'; inputField.appendChild(minMaxContainer); inputField.appendChild(warningMessage); // Add event listeners to check max > min validation function validateMinMax() { const min = parseFloat(minValueInput.value); const max = parseFloat(maxValueInput.value); if (max <= min) { warningMessage.style.display = 'block'; // Show the warning message } else { warningMessage.style.display = 'none'; // Hide the warning message } } // Listen for changes in the min and max input fields minValueInput.addEventListener('input', validateMinMax); maxValueInput.addEventListener('input', validateMinMax); return inputField; } function createSelectBoxOptionsFromData(options) { const inputField = document.createElement('div'); options.forEach((optionText, index) => { const optionContainer = document.createElement('div'); optionContainer.className = 'input-group2 mb-1'; const optionInput = document.createElement('input'); optionInput.type = 'text'; optionInput.placeholder = `Option ${index + 1}`; optionInput.className = 'form-control-sm'; optionInput.value = optionText; const deleteOptionButton = document.createElement('button'); deleteOptionButton.className = 'btn btn-danger btn-sm'; deleteOptionButton.textContent = 'X'; deleteOptionButton.addEventListener('click', () => optionContainer.remove()); const inputGroupAppend = document.createElement('div'); inputGroupAppend.className = 'input-group-append'; inputGroupAppend.appendChild(deleteOptionButton); optionContainer.appendChild(optionInput); optionContainer.appendChild(inputGroupAppend); inputField.appendChild(optionContainer); }); function createSelectBoxOption(placeholder) { const multiselectContainer = document.createElement('div'); multiselectContainer.className = 'input-group2 mb-1'; const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.className = 'form-control-sm'; labelInput.placeholder = placeholder; const deleteOptionButton = document.createElement('button'); deleteOptionButton.className = 'btn btn-danger btn-sm'; deleteOptionButton.textContent = 'X'; deleteOptionButton.addEventListener('click', () => multiselectContainer.remove()); multiselectContainer.appendChild(labelInput); multiselectContainer.appendChild(deleteOptionButton); return multiselectContainer; } const addmultiselectButton = document.createElement('button'); addmultiselectButton.className = 'btn btn-secondary btn-sm mb-2'; addmultiselectButton.textContent = 'Add Option'; addmultiselectButton.addEventListener('click', () => { const optionCount = inputField.querySelectorAll('.input-group2').length + 1; inputField.insertBefore(createSelectBoxOption(`Option ${optionCount}`), addmultiselectButton); }); inputField.appendChild(addmultiselectButton); return inputField; }