| |
| |
| |
| |
|
|
|
|
| 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',
|
| animation: 150,
|
| handle: '.drag-handle',
|
| onAdd: function (evt) {
|
|
|
| const questionType = evt.item.dataset.type;
|
| const newQuestion = createQuestion(questionType);
|
| evt.item.replaceWith(newQuestion);
|
| },
|
| });
|
|
|
|
|
|
|
|
|
| new Sortable(questionTypes, {
|
| group: {
|
| name: 'survey',
|
| pull: 'clone',
|
| put: false,
|
| },
|
| sort: false,
|
| animation: 150,
|
| filter: ".no-drag, button",
|
| onStart: function(evt) {
|
| if (evt.item.matches("button")) {
|
| evt.preventDefault();
|
| }
|
| }
|
| });
|
|
|
|
|
|
|
|
|
| function createQuestion(type) {
|
| savestatus = false;
|
| 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());
|
|
|
| const questionLabel = document.createElement('strong');
|
|
|
| let inputField = null;
|
|
|
| if (type === 'new-page' || type === 'add-video' || type === 'add-image' || type === 'add-text') {
|
|
|
| if (type === 'new-page') {
|
| questionLabel.textContent = 'New Page';
|
| question.appendChild(dragHandle);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| } 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);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| 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);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| 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);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| 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';
|
|
|
|
|
| question.appendChild(dragHandle);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| question.appendChild(questionText);
|
|
|
|
|
| if (type === 'multiple-choice') {
|
| inputField = createMultipleChoiceOptions();
|
| } else if (type === 'multiselect') {
|
| inputField = createMultiselectOptions();
|
| } else if (type === 'slider') {
|
| inputField = createSliderOptions();
|
| } else if (type === 'selectbox') {
|
| inputField = createSelecBoxtOptions();
|
| }
|
| }
|
|
|
|
|
| if (inputField) {
|
| question.appendChild(inputField);
|
| }
|
|
|
| return question;
|
| }
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
| function createSliderOptions() {
|
| const inputField = document.createElement('div');
|
|
|
|
|
| const sliderLabels = document.createElement('div');
|
| sliderLabels.className = 'd-flex justify-content-between mb-2';
|
|
|
| inputField.appendChild(sliderLabels);
|
|
|
|
|
| const minMaxContainer = document.createElement('div');
|
| minMaxContainer.className = 'd-flex justify-content-between mb-2';
|
|
|
|
|
| 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;
|
|
|
| minValueWrapper.appendChild(minValueLabel);
|
| minValueWrapper.appendChild(minValueInput);
|
|
|
|
|
| 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;
|
|
|
| maxValueWrapper.appendChild(maxValueLabel);
|
| maxValueWrapper.appendChild(maxValueInput);
|
|
|
| minMaxContainer.appendChild(minValueWrapper);
|
| minMaxContainer.appendChild(maxValueWrapper);
|
|
|
|
|
| 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);
|
|
|
|
|
| function validateMinMax() {
|
| const min = parseFloat(minValueInput.value);
|
| const max = parseFloat(maxValueInput.value);
|
|
|
| if (max <= min) {
|
| warningMessage.style.display = 'block';
|
| } else {
|
| warningMessage.style.display = 'none';
|
| }
|
| }
|
|
|
|
|
| minValueInput.addEventListener('input', validateMinMax);
|
| maxValueInput.addEventListener('input', validateMinMax);
|
|
|
| return inputField;
|
| }
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| |
| |
| |
| |
|
|
|
|
|
|
|
|
| const exportModalElement = document.getElementById('exportModal');
|
| const githubModalElement = document.getElementById('githubModal');
|
| const hfModalElement = document.getElementById('hfModal');
|
| const exportButton = document.getElementById('save-survey');
|
|
|
|
|
| document.getElementById('save-survey').addEventListener('click', () => {
|
| const exportModal = new bootstrap.Modal(exportModalElement, {
|
| backdrop: 'static',
|
| keyboard: false,
|
| focus: true
|
| });
|
|
|
| exportModalElement.removeAttribute('inert');
|
| exportModal.show();
|
| });
|
|
|
|
|
|
|
| document.getElementById('download-btn').addEventListener('click', () => {
|
| const surveyPythonCode = buildSurveyPythonCode();
|
| downloadSurvey(surveyPythonCode);
|
|
|
| savestatus = true;
|
| const exportModal = bootstrap.Modal.getInstance(exportModalElement);
|
| exportModal.hide();
|
| });
|
|
|
|
|
|
|
| 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');
|
| 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');
|
| hfModal.show();
|
| });
|
|
|
|
|
|
|
| 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');
|
|
|
| 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;
|
| }
|
|
|
|
|
| submitButton.disabled = true;
|
| loadingSpinner.style.display = "inline-block";
|
|
|
| try {
|
|
|
| const surveyPythonCode = buildSurveyPythonCode();
|
| 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);
|
| }
|
|
|
|
|
| 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 {
|
|
|
| loadingSpinner.style.display = "none";
|
| submitButton.disabled = false;
|
| }
|
| });
|
|
|
|
|
|
|
| exportModalElement.addEventListener('hidden.bs.modal', () => {
|
| exportModalElement.setAttribute('inert', 'true');
|
| exportButton.focus();
|
| });
|
|
|
| githubModalElement.addEventListener('hidden.bs.modal', () => {
|
| githubModalElement.setAttribute('inert', 'true');
|
| });
|
|
|
| hfModalElement.addEventListener('hidden.bs.modal', () => {
|
| hfModalElement.setAttribute('inert', 'true');
|
| });
|
|
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
| let sha = null;
|
| const existingFile = await fetch(apiUrl, {
|
| headers: { Authorization: `Bearer ${accessToken}` }
|
| });
|
|
|
| if (existingFile.ok) {
|
| const fileData = await existingFile.json();
|
| sha = fileData.sha;
|
| }
|
|
|
|
|
| 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
|
| })
|
| });
|
|
|
| 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 {
|
|
|
| let sha = null;
|
| const existingFile = await fetch(apiUrl, {
|
| headers: { Authorization: `Bearer ${accessToken}` }
|
| });
|
|
|
| if (existingFile.ok) {
|
| const fileData = await existingFile.json();
|
| sha = fileData.sha;
|
| }
|
|
|
|
|
| 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
|
| })
|
| });
|
|
|
| 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 = '';
|
| const questions = document.querySelectorAll('#survey-area .question');
|
|
|
| let questionIndex = 1;
|
| let pageIndex = 1;
|
| pythonCode += formatPreCode(questions);
|
|
|
| let currentPageQuestions = [];
|
| let currentPagIndicies = [];
|
|
|
| questions.forEach((question) => {
|
| const questionType = question.querySelector('strong').textContent.replace(' Question', '');
|
|
|
| if (questionType.toLowerCase() === 'new page') {
|
| if (pageIndex == 1) {
|
| pythonCode += formatFirstPageCode(currentPagIndicies, currentPageQuestions)
|
| } else {
|
| pythonCode += formatPostCode(currentPagIndicies, currentPageQuestions, pageIndex);
|
| }
|
| pageIndex++;
|
| pythonCode += `elif st.session_state["current_page"] == ${pageIndex}:\n\n`;
|
| currentPageQuestions = [];
|
| 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;
|
|
|
| if (questionType.toLowerCase() === 'multiple-choice') {
|
| pythonCode += formatMultipleChoicePython(questionText, question, questionIndex);
|
| pythonCode += ` st.markdown("""<style> div[class*="stRadio"] > label > div[data-testid="stMarkdownContainer"] > p {font-size: 18px;}</style> <br><br>""", unsafe_allow_html=True)\n\n\n`;
|
| } else if (questionType.toLowerCase() === 'text-input') {
|
| pythonCode += formatTextQuestionPython(questionText, question, questionIndex);
|
| pythonCode += ` st.markdown("""<style> div[class*="stText"] > label > div[data-testid="stMarkdownContainer"] > p {font-size: 18px;}</style> <br><br>""", unsafe_allow_html=True)\n\n\n`;
|
| } else if (questionType.toLowerCase() === 'multiselect') {
|
| pythonCode += formatMultiselectPython(questionText, question, questionIndex);
|
| pythonCode += ` st.markdown("""<style> div[class*="stMulti"] > label > div[data-testid="stMarkdownContainer"] > p {font-size: 18px;}</style> <br><br>""", unsafe_allow_html=True)\n\n\n`;
|
| } else if (questionType.toLowerCase() === 'slider') {
|
| pythonCode += formatSliderQuestionsPython(questionText, question, questionIndex);
|
| pythonCode += ` st.markdown("""<style> div[class*="stSlider"] > label > div[data-testid="stMarkdownContainer"] > p {font-size: 18px;}</style> <br><br>""", unsafe_allow_html=True)\n\n\n`;
|
| } else if (questionType.toLowerCase() === 'select box') {
|
| pythonCode += formatSelectBoxPython(questionText, question, questionIndex);
|
| pythonCode += ` st.markdown("""<style> div[class*="stSelectbox"] > label > div[data-testid="stMarkdownContainer"] > p {font-size: 18px;}</style> <br><br>""", unsafe_allow_html=True)\n\n\n`;
|
| } else if (questionType.toLowerCase() === 'likert') {
|
| pythonCode += formatlikert(questionText, question, questionIndex);
|
| pythonCode += ` st.markdown("""<style> div[class*="stRadio"] > label > div[data-testid="stMarkdownContainer"] > p {font-size: 18px;}</style> <br><br>""", unsafe_allow_html=True)\n\n\n`;
|
| }
|
|
|
|
|
| currentPagIndicies.push(questionIndex);
|
| currentPageQuestions.push(questionType);
|
| questionIndex++;
|
| }
|
| });
|
|
|
|
|
| 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('<p class="big-font">Thank you for participating! <br> Click on the button below to submit your answers. </p>', unsafe_allow_html=True)\n`;
|
| pythonCode += ` st.button('Submit Responses', disabled = st.session_state["disabled"], on_click = button_disable)\n`;
|
| pythonCode += ` if st.session_state["disabled"]:\n`;
|
| pythonCode += ' with st.spinner(r"$\\textsf{\\normalsize Storing data on IPFS and Ethereum. This operation might take a few minutes. Please wait to receive your confirmation code!}$"):\n';
|
| pythonCode += ` try:\n`;
|
| pythonCode += ` response = {\n`;
|
| let questionNumber = 1;
|
| questions.forEach((question) => {
|
| let questionType = question.querySelector('strong').textContent.replace(' Question', '');
|
| if (questionType.toLowerCase() === 'multiple-choice' || questionType.toLowerCase() === 'text-input' || questionType.toLowerCase() === 'multiselect' || questionType.toLowerCase() === 'slider' || questionType.toLowerCase() === 'select box' || questionType.toLowerCase() === 'likert') {
|
| if (questionType.toLowerCase() === 'multiple-choice' || questionType.toLowerCase() === 'likert' || questionType.toLowerCase() === 'select box') {
|
| pythonCode += ` "Q${questionNumber}": Q${questionNumber}_radio_options[st.session_state["Q${questionNumber}"]],\n`
|
| } else {
|
| pythonCode += ` "Q${questionNumber}": st.session_state["Q${questionNumber}"],\n`
|
| }
|
| questionNumber++;
|
| }
|
| });
|
| pythonCode += ` }\n`
|
| pythonCode += ` submission(response)\n`
|
|
|
| pythonCode += ` except Exception as e:\n`;
|
| pythonCode += ` print(e)\n`;
|
| pythonCode += ` st.error(f'An error ocurred. Here is the error message: {e}', icon="🚨")\n\n`;
|
| pythonCode += ` print("Success!")\n\n`;
|
| pythonCode += ` if st.button('Back'):\n`
|
| pythonCode += ` st.session_state["current_page"] -= 1\n`
|
| pythonCode += ` st.rerun()\n`
|
|
|
| pythonCode += ` st.progress(st.session_state["current_page"]/total_number_pages, text="Progress")\n`;
|
| return pythonCode;
|
| }
|
|
|
|
|
| function formatFirstPageCode(questionIndices, questions) {
|
| let pythonCode = ``;
|
|
|
| pythonCode += ` placeholder = st.empty()\n\n`;
|
|
|
| pythonCode += " if st.button('Next', key='next_button_page_1'):\n";
|
|
|
|
|
| pythonCode += ` all_answered = True\n`;
|
| questions.forEach((question, i) => {
|
| const index = questionIndices[i];
|
| pythonCode += ` if st.session_state["Q${index}"] == None or st.session_state["Q${index}"] == []:\n`;
|
| pythonCode += ` all_answered = False\n`;
|
| });
|
|
|
| pythonCode += ` if all_answered:\n`;
|
| pythonCode += ` st.session_state["current_page"] += 1\n`;
|
| pythonCode += ` st.rerun()\n`;
|
| pythonCode += ` else:\n`;
|
| pythonCode += ` with placeholder.container():\n`;
|
| pythonCode += ` st.warning("Please answer all the questions on this page.", icon="⚠️")\n\n`;
|
|
|
| pythonCode += ` st.progress(st.session_state["current_page"]/total_number_pages, text="Progress")\n\n\n`;
|
|
|
| return pythonCode;
|
| }
|
|
|
| function formatPostCode(questionIndices, questions, pageIndex) {
|
|
|
| let pythonCode = '';
|
|
|
| if (pageIndex != 1) {
|
| pythonCode += ' placeholder = st.empty()\n\n';
|
| pythonCode += ' col1, col2 = st.columns(2)\n';
|
|
|
| pythonCode += ' with col1:\n';
|
| pythonCode += " if st.button('Back'):\n";
|
| pythonCode += ' st.session_state["current_page"] -= 1\n';
|
| pythonCode += ' st.rerun()\n';
|
|
|
| pythonCode += ' with col2:\n';
|
| pythonCode += ` if st.button('Next'):\n`;
|
|
|
|
|
| pythonCode += ' all_answered = True\n';
|
| questions.forEach((question, i) => {
|
| const index = questionIndices[i];
|
| pythonCode += ` if st.session_state["Q${index}"] == None or st.session_state["Q${index}"] == []:\n`;
|
| pythonCode += ` all_answered = False\n`;
|
| });
|
|
|
| pythonCode += ' if all_answered:\n';
|
| pythonCode += ' st.session_state["current_page"] += 1\n';
|
| pythonCode += ' st.rerun()\n';
|
| pythonCode += ' else:\n';
|
| pythonCode += ' with placeholder.container():\n';
|
| pythonCode += ' st.warning("Please answer all the questions on this page.", icon="⚠️")\n\n';
|
|
|
| pythonCode += ' st.progress(st.session_state["current_page"]/total_number_pages, text="Progress")\n\n\n';
|
| } else {
|
| pythonCode += ' placeholder = st.empty()\n\n';
|
| pythonCode += ` if st.button('Next'):\n`;
|
| pythonCode += ' all_answered = True\n';
|
|
|
| questions.forEach((question, i) => {
|
| const index = questionIndices[i];
|
| pythonCode += ` if st.session_state["Q${index}"] == None or st.session_state["Q${index}"] == []:\n`;
|
| pythonCode += ` all_answered = False\n`;
|
| });
|
|
|
| pythonCode += ' if all_answered:\n';
|
| pythonCode += ' st.session_state["current_page"] += 1\n';
|
| pythonCode += ' st.rerun()\n';
|
| pythonCode += ' else:\n';
|
| pythonCode += ' with placeholder.container():\n';
|
| pythonCode += ' st.warning("Please answer all the questions on this page.", icon="⚠️")\n\n';
|
|
|
| pythonCode += ' st.progress(st.session_state["current_page"]/total_number_pages, text="Progress")\n\n\n';
|
| }
|
|
|
| return pythonCode;
|
| }
|
|
|
|
|
|
|
| function formatPreCode(questions) {
|
| pythonCode = '';
|
| const surveyTitle = document.getElementById('survey-title').value.trim();
|
| const surveyDescription = document.getElementById('survey-description').value.trim();
|
| totalPages = 1;
|
| totalQuestions = 0;
|
| questions.forEach(question => {
|
| const questionType = question.querySelector('strong').textContent.replace(' Question', '');
|
| if (questionType.toLowerCase() === 'new page') {
|
| totalPages++;
|
| } else if (questionType.toLowerCase() === 'add video' || questionType.toLowerCase() === 'add image' || questionType.toLowerCase() === 'add text') {
|
| } else {
|
| totalQuestions++;
|
| }
|
|
|
| });
|
|
|
| pythonCode += `import streamlit as st\nimport requests\nimport json\nimport web3\n\n`;
|
|
|
| pythonCode += 'abi = [\n'
|
| pythonCode += '\t{\n'
|
| pythonCode += '\t\t"anonymous": False,\n'
|
| pythonCode += '\t\t"inputs": [\n'
|
| pythonCode += '\t\t\t{\n'
|
| pythonCode += '\t\t\t\t"indexed": False,\n'
|
| pythonCode += '\t\t\t\t"internalType": "string",\n'
|
| pythonCode += '\t\t\t\t"name": "ipfsHash",\n'
|
| pythonCode += '\t\t\t\t"type": "string"\n'
|
| pythonCode += '\t\t\t}\n'
|
| pythonCode += '\t\t],\n'
|
| pythonCode += '\t\t"name": "Store",\n'
|
| pythonCode += '\t\t"type": "event"\n'
|
| pythonCode += '\t},\n'
|
| pythonCode += '\t{\n'
|
| pythonCode += '\t\t"inputs": [],\n'
|
| pythonCode += '\t\t"name": "getHashes",\n'
|
| pythonCode += '\t\t"outputs": [\n'
|
| pythonCode += '\t\t\t{\n'
|
| pythonCode += '\t\t\t\t"internalType": "string[]",\n'
|
| pythonCode += '\t\t\t\t"name": "",\n'
|
| pythonCode += '\t\t\t\t"type": "string[]"\n'
|
| pythonCode += '\t\t\t}\n'
|
| pythonCode += '\t\t],\n'
|
| pythonCode += '\t\t"stateMutability": "view",\n'
|
| pythonCode += '\t\t"type": "function"\n'
|
| pythonCode += '\t},\n'
|
| pythonCode += '\t{\n'
|
| pythonCode += '\t\t"inputs": [\n'
|
| pythonCode += '\t\t\t{\n'
|
| pythonCode += '\t\t\t\t"internalType": "string",\n'
|
| pythonCode += '\t\t\t\t"name": "ipfsHash",\n'
|
| pythonCode += '\t\t\t\t"type": "string"\n'
|
| pythonCode += '\t\t\t}\n'
|
| pythonCode += '\t\t],\n'
|
| pythonCode += '\t\t"name": "storeHash",\n'
|
| pythonCode += '\t\t"outputs": [],\n'
|
| pythonCode += '\t\t"stateMutability": "nonpayable",\n'
|
| pythonCode += '\t\t"type": "function"\n'
|
| pythonCode += '\t}\n'
|
| pythonCode += ']\n'
|
|
|
| pythonCode += `\n# function that uploads the results to ipfs
|
| def upload_json_to_ipfs(data):
|
| try:
|
| url = "https://api.pinata.cloud/pinning/pinJSONToIPFS"
|
|
|
| print("starting upload to ipfs")
|
|
|
| # check for JWT secret
|
| if "PinataJWT" not in st.secrets:
|
| st.write("No JWT secret found, please add your JWT in a secret titled \\"PinataJWT\\"")
|
| return
|
|
|
| jwt_token = st.secrets["PinataJWT"].strip()
|
| headers = {
|
| "Authorization": f"Bearer {jwt_token}",
|
| "Content-Type": "application/json"
|
| }
|
|
|
| # Convert Python dictionary to JSON string
|
| response = requests.post(url, headers=headers, json=data)
|
|
|
| if response.status_code == 200:
|
| # Print the IPFS hash from the successful response
|
| ipfs_hash = response.json().get("IpfsHash")
|
| return ipfs_hash
|
| else:
|
| st.write(f"Failed to upload JSON. Status code: {response.status_code}")
|
| st.write(response.text)
|
| return None
|
| except Exception as e:
|
| st.write(f"Error uploading to Pinata: {e}")
|
|
|
| # function that uploads to blockchain
|
| def upload_to_blockchain(ipfs_hash):
|
| print("starting blockchain upload")
|
|
|
| w3 = web3.Web3(web3.HTTPProvider(st.secrets["infura"]))
|
| if not w3.is_connected():
|
| raise RuntimeError("Failed to connect to Ethereum network")
|
|
|
| try:
|
| contract_address = st.secrets["ContractAddress"]
|
| checksum_address = w3.to_checksum_address(contract_address)
|
| except Exception as e:
|
| raise ValueError(f"Invalid contract address: {contract_address} Error: {e}")
|
|
|
| try:
|
| assert isinstance(abi, list), "ABI must be a list"
|
| except Exception as e:
|
| raise ValueError(f"ABI format error: {e}")
|
|
|
| contract = w3.eth.contract(address=checksum_address, abi=abi)
|
|
|
| # Build transaction
|
| call_function = contract.functions.storeHash(ipfs_hash).build_transaction({
|
| "chainId": 11155111,
|
| "from": st.secrets["EthWallet"],
|
| "nonce": w3.eth.get_transaction_count(st.secrets["EthWallet"]),
|
| "gas": 300000,
|
| "gasPrice": w3.to_wei("10", "gwei")
|
| })
|
|
|
| # Sign transaction
|
| signed_tx = w3.eth.account.sign_transaction(call_function, private_key=st.secrets["pk"])
|
|
|
| # Send transaction
|
| tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
|
| # Wait for transaction receipt
|
| tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
|
|
|
| print("Transaction successful!")
|
| print("ETH Tx Hash:", tx_receipt.transactionHash.hex())
|
| st.success('Data successfully stored. Thank you for taking the survey!', icon="✅")
|
| st.info(f'The Ethereum hash is: {tx_receipt.logs[0].transactionHash.hex()}', icon="ℹ️")
|
|
|
| return tx_receipt.transactionHash.hex()
|
|
|
| def get_ipfs_hashes():
|
| w3 = web3.Web3(web3.HTTPProvider(st.secrets["infura"]))
|
|
|
| # Create an instance of the contract
|
| contract = w3.eth.contract(address=st.secrets["ContractAddress"], abi=abi)
|
|
|
| # Call the getHashes function
|
| try:
|
| ipfs_hashes = contract.functions.getHashes().call()
|
| return ipfs_hashes
|
| except Exception as e:
|
| st.write(f"Error retrieving hashes: {e}")
|
| return []
|
|
|
| def retreive_ipfs_hash_data(hashes):
|
| results = []
|
| for ipfs_hash in hashes:
|
| url = f"https://gateway.pinata.cloud/ipfs/{ipfs_hash}"
|
| try:
|
| response = requests.get(url)
|
| if response.status_code == 200:
|
| data = response.json()
|
| results.append({"hash": ipfs_hash, "data": data})
|
| else:
|
| results.append({"hash": ipfs_hash, "error": f"Failed to retrieve data (status {response.status_code})"})
|
| except Exception as e:
|
| results.append({"hash": ipfs_hash, "error": str(e)})
|
| return results
|
|
|
|
|
| # function that handles survey submission
|
| # sets up ipfs and blockchain
|
| def submission(survey_data):
|
| ipfs_hash = upload_json_to_ipfs(survey_data)
|
| if ipfs_hash:
|
| upload_to_blockchain(ipfs_hash)
|
| st.info(f'The IPFS hash is: {ipfs_hash}', icon="ℹ️")\n\n`
|
|
|
|
|
| pythonCode += `total_number_pages = ${totalPages+1}\n`;
|
| pythonCode += 'placeholder_buttons = None\n\n';
|
| pythonCode += formatRadioOptions(questions);
|
|
|
| pythonCode += "\n\n# Function that records radio element changes\n"+
|
| "def radio_change(element, state, key):\n"+
|
| " st.session_state[state] = element.index(st.session_state[key]) # Setting previously selected option\n\n"+
|
| "def multi_change(element, state, key):\n"+
|
| " st.session_state[state] = []\n" +
|
| " for selected_option in st.session_state[key]:\n"+
|
| " st.session_state[state].append(selected_option)\n\n"+
|
| "# Function that disables the last button while data is uploaded to IPFS\n"+
|
| "def button_disable():\n"+
|
| " st.session_state['disabled'] = True\n\n"+
|
| "def answer_change(state, key):\n"+
|
| " st.session_state[state] = st.session_state[key]\n\n";
|
|
|
| pythonCode += "st.set_page_config(page_title='IPFS-Based Survey',)\n";
|
| pythonCode += `st.title('${surveyTitle}')\n\n`;
|
|
|
| pythonCode += 'st.markdown("<style>.row-widget.stButton {text-align: center;}</style>", unsafe_allow_html=True)\n'+
|
| 'st.markdown("<style>.big-font {font-size:24px;}</style>", unsafe_allow_html=True)\n\n\n';
|
|
|
|
|
| pythonCode += 'if "current_page" not in st.session_state:\n';
|
| pythonCode += ' st.session_state["current_page"] = 1\n';
|
|
|
| for (let i = 1; i <= totalQuestions; i++) {
|
| pythonCode += ` st.session_state["Q${i}"] = None\n`;
|
| }
|
|
|
|
|
| pythonCode += ' st.session_state["disabled"] = False\n\n';
|
|
|
| pythonCode += `# Page 1; Video\nif st.session_state["current_page"] == 1:
|
|
|
| st.markdown("""<p style='font-size:18px ;'>${surveyDescription}</p>""", unsafe_allow_html=True)\n\n`
|
|
|
| return pythonCode;
|
| }
|
|
|
|
|
| function formatRadioOptions(questions) {
|
| let pythonCode = '';
|
| 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);
|
| }
|
| });
|
|
|
| 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;
|
| }
|
|
|
| function formatTextQuestionPython(questionText, question, index) {
|
| let pythonCode = '';
|
|
|
| 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;
|
| }
|
|
|
| function formatMultiselectPython(questionText, question, index) {
|
| let pythonCode = '';
|
|
|
|
|
| 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;
|
| }
|
|
|
| function formatSliderQuestionsPython(questions, question, index) {
|
| let pythonCode = '';
|
|
|
| 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 = '';
|
|
|
|
|
| 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;
|
| }
|
|
|
| function formatVideoPython(questionText) {
|
| let pythonCode = '';
|
| pythonCode += ` st.video("${questionText}") \n`;
|
| return pythonCode;
|
| }
|
|
|
| function formatImagePython(questionText) {
|
| let pythonCode = '';
|
| pythonCode += ` st.image("${questionText}") \n`;
|
| return pythonCode;
|
| }
|
|
|
| function formatTextPython(questionText) {
|
| let pythonCode = '';
|
| pythonCode += ` st.markdown("""<p style='font-size:18px;'>${questionText}</p>""", unsafe_allow_html=True)\n`;
|
| return pythonCode;
|
| }
|
|
|
|
|
|
|
| function formatMultipleChoicePython(questionText, question, index) {
|
| let pythonCode = '';
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
| function formatlikert(questionText, question, index) {
|
| let pythonCode = '';
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| |
| |
| |
| |
|
|
|
|
|
|
|
|
| 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;
|
|
|
|
|
| const parsedData = parsePythonSurveyFile(fileContent);
|
|
|
|
|
| populateSurveyForm(parsedData);
|
|
|
|
|
| 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: []
|
| };
|
|
|
|
|
| const titleMatch = fileContent.match(/st\.title\('(.+?)'\)/);
|
| const descriptionMatch = fileContent.match(/st\.markdown\("""<p style='font-size:18px ;'>(.*?)<\/p>""",\s*unsafe_allow_html=True\)/);
|
| surveyData.title = titleMatch ? titleMatch[1] : '';
|
| surveyData.description = descriptionMatch ? descriptionMatch[1] : '';
|
|
|
|
|
| const lines = fileContent.split('\n');
|
| let currentRadio = null;
|
|
|
|
|
| for (let i = 0; i < lines.length; i++) {
|
| let line = lines[i].trim();
|
|
|
| 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' });
|
| }
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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;
|
| }
|
|
|
| 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\s+style=['"]font-size:\s*18px;['"]>(.+?)<\/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 (currentRadio) {
|
| const optionsMatch = line.match(/options\s*=\s*([\w_]+)_radio_options/);
|
| if (optionsMatch) {
|
|
|
| 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];
|
|
|
| surveyData.components.push(currentRadio);
|
| currentRadio = null;
|
| }
|
| }
|
| }
|
|
|
| return surveyData;
|
| }
|
|
|
|
|
| function populateSurveyForm(data) {
|
|
|
| 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 = '';
|
|
|
| data.components.forEach((component) => {
|
| const componentElement = createQuestionFromPreSavedData(component.type, component);
|
|
|
|
|
| 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());
|
|
|
|
|
| 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') {
|
|
|
| if (type === 'new-page') {
|
| questionLabel.textContent = 'New Page';
|
| question.appendChild(dragHandle);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| } 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 || '';
|
| question.appendChild(dragHandle);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| 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 || '';
|
| question.appendChild(dragHandle);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| 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 || '';
|
| question.appendChild(dragHandle);
|
| question.appendChild(questionLabel);
|
| question.appendChild(deleteButton);
|
| 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 || '';
|
|
|
| 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');
|
|
|
|
|
| const sliderLabels = document.createElement('div');
|
| sliderLabels.className = 'd-flex justify-content-between mb-2';
|
| inputField.appendChild(sliderLabels);
|
|
|
|
|
| const minMaxContainer = document.createElement('div');
|
| minMaxContainer.className = 'd-flex justify-content-between mb-2';
|
|
|
|
|
| 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;
|
|
|
| minValueWrapper.appendChild(minValueLabel);
|
| minValueWrapper.appendChild(minValueInput);
|
|
|
|
|
| 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;
|
|
|
| maxValueWrapper.appendChild(maxValueLabel);
|
| maxValueWrapper.appendChild(maxValueInput);
|
|
|
| minMaxContainer.appendChild(minValueWrapper);
|
| minMaxContainer.appendChild(maxValueWrapper);
|
|
|
|
|
| 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);
|
|
|
|
|
| function validateMinMax() {
|
| const min = parseFloat(minValueInput.value);
|
| const max = parseFloat(maxValueInput.value);
|
|
|
| if (max <= min) {
|
| warningMessage.style.display = 'block';
|
| } else {
|
| warningMessage.style.display = 'none';
|
| }
|
| }
|
|
|
|
|
| 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;
|
| } |