import datetime import json from nicegui import ui import constants as const # --- Survey Options --- FREQUENCY_OPTIONS = ["Daily", "Weekly", "Monthly", "Rarely"] FEATURE_OPTIONS = ["News Feed", "Marketplace", "Groups", "Messenger", "Stories"] OTHER_APP_OPTIONS = ["Instagram", "TikTok", "X/Twitter", "LinkedIn", "Snapchat", "Reddit"] GENDER_OPTIONS = ["Male", "Female", "Non-binary", "Prefer not to say"] class SurveyState: def __init__(self): self.frequency = None self.no_use_reason = "" self.selected_features = [] self.other_tasks = "" self.feature_feedback = {} self.suggestions = "" self.priority_tasks = [] def convert_to_json(self): return { "usage": { "frequency": self.frequency, "Reason for not using": self.no_use_reason, "features_used": self.selected_features, "other_tasks": self.other_tasks, }, "feature_feedback": self.feature_feedback, "suggestions": self.suggestions, "priority_tasks": self.priority_tasks, } def save_to_json(self): data = self.convert_to_json() with open(f"results/response_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json", "w") as f: json.dump(data, f, indent=4) state = SurveyState() def create_survey(): container = ui.column().classes("w-full items-center q-pa-md") # --- PAGE 1: USAGE --- def show_page_1(): container.clear() with container: ui.label("ARQ Usage").classes("text-h4 mb-4") with ui.card().classes("w-full max-w-lg p-6"): ui.label("How frequently do you use ARQ?").classes("text-bold") p1_radio = ui.radio(options=const.USAGE_FREQUENCY_OPTIONS).bind_value(state, "frequency") with ui.column().classes("w-full").bind_visibility_from( p1_radio, "value", backward=lambda v: v == "Never used it" ): ui.label("What are the reasons for not using ARQ?") no_use_reason = ( ui.input(validation={"Minimum 20 characters": lambda value: len(value) >= 20}) .classes("w-full") .bind_value(state, "no_use_reason") ) ui.label("What tasks have you performed using this app?").classes("text-bold mt-4") p1_select = ( ui.select(options=const.TASK_OPTIONS, multiple=True) .props("use-chips") .classes("w-full") .bind_value(state, "selected_features") ) with ui.column().classes("w-full").bind_visibility_from( p1_select, "value", backward=lambda v: "Others" in v ): ui.label("What other tasks have you performed using ARQ?") ui.input().classes("w-full").bind_value(state, "other_tasks") with ui.row().classes("w-full"): next_btn = ui.button("Next", on_click=show_page_2).classes("mt-6 w-full") ui.tooltip( "Please answer all questions (min 20 chars for 'Reasons for failure')" ).bind_visibility_from(next_btn, "enabled", backward=lambda v: not v) # Validation def enable_next(): if p1_radio.value == "Never used it": if len(no_use_reason.value) < 20: return False return state.frequency is not None and len(state.selected_features) > 0 ui.timer(0.1, lambda: next_btn.set_enabled(enable_next())) # --- PAGE 2: STAR RATINGS & CONDITIONAL FEEDBACK --- def show_page_2(): container.clear() # Initialize dictionary for selected features for f in state.selected_features: if f == "Others": continue if f not in state.feature_feedback: state.feature_feedback[f] = { "completed_on_arq": None, "rating": 0, "low_rating_suggestions": "", "completed_how": None, "arq_helped": [], "excel_features": [], "other_apps": "", "abandoned_why": [], "abandoned_why_others": "", } with container: ui.label("Feature Feedback").classes("text-h4 mb-4") # We define the button and validation logic first to ensure they are in scope def is_page_2_valid(): valid = True for f, ob in state.feature_feedback.items(): if ob["completed_on_arq"] is None: valid = False if ob["completed_on_arq"] == "Yes" and ob["rating"] == 0: valid = False if ob["completed_on_arq"] == "No" and ob["completed_how"] is None: valid = False if ob["completed_how"] == "both" and not ob["arq_helped"]: valid = False if ob["completed_how"] in ["both", "excel"] and not ob["excel_features"]: valid = False if ob["completed_how"] == "abandoned" and not ob["abandoned_why"]: valid = False if ob["completed_how"] == "other" and not ob["other_apps"]: valid = False return valid for feature in state.selected_features: if feature == "Others": continue with ui.card().classes("w-full max-w-lg p-6 mb-4"): ui.label(f"{const.TASK_OPTIONS[feature]}").classes("text-h6 text-primary") ui.label("Were you able to complete this task on ARQ").classes("text-bold mt-4") p2q1_select = ( ui.radio(["Yes", "No"]) .props("inline") .bind_value(state.feature_feedback[feature], "completed_on_arq") ) # Star Rating (Quasar based) with ui.column().classes("w-full").bind_visibility_from( p2q1_select, "value", backward=lambda v: v == "Yes" ): ui.label("How would you rate the features available to complete this task").classes( "text-bold mt-4" ) rating = ( ui.rating(value=5, icon="star") .classes("text-3xl") .bind_value(state.feature_feedback[feature], "rating") ) with ui.column().classes("w-full").bind_visibility_from( rating, "value", backward=lambda v: 0 < v < 4 ): ui.input(label="What could have been improved for a better experience?").bind_value( state.feature_feedback[feature], "low_rating_suggestions" ).classes("w-full") with ui.column().classes("w-full").bind_visibility_from( p2q1_select, "value", backward=lambda v: v == "No" ): ui.label("How did you complete this task?") p2q2_select = ( ui.radio( options={ "both": "Using both ARQ and Excel", "excel": "Entirely on Excel", "other": "Using other applications", "abandoned": "Task abandoned", } ) .props("inline") .bind_value(state.feature_feedback[feature], "completed_how") ) with ui.column().classes("w-full").bind_visibility_from( p2q2_select, "value", backward=lambda v: v == "both" ): ui.label("How did ARQ help you with this task?") ui.select(options=const.ARQ_HELPED, multiple=True).props("use-chips").classes( "w-full" ).bind_value(state.feature_feedback[feature], "arq_helped") with ui.column().classes("w-full").bind_visibility_from( p2q2_select, "value", backward=lambda v: v == "both" or v == "excel" ): ui.label("Which Excel features did you require to complete this task?") ui.select(options=const.EXCEL_FEATURE_LIST, multiple=True).props("use-chips").classes( "w-full" ).bind_value(state.feature_feedback[feature], "excel_features") with ui.column().classes("w-full").bind_visibility_from( p2q2_select, "value", backward=lambda v: v == "other" ): ui.input(label="Which other applications helped you with this task?").classes( "w-full" ).bind_value(state.feature_feedback[feature], "other_apps") with ui.column().classes("w-full").bind_visibility_from( p2q2_select, "value", backward=lambda v: v == "abandoned" ): ui.label("What were the reasons for abandoning this task?") p2q3_select = ( ui.select(options=const.ABANDONMENT_OPTIONS, multiple=True) .props("use-chips") .classes("w-full") .bind_value(state.feature_feedback[feature], "abandoned_why") ) with ui.column().classes("w-full").bind_visibility_from( p2q3_select, "value", backward=lambda v: "others" in v ): ui.input(label="Describe the reasons for abandoning this task?").classes("w-full").bind_value( state.feature_feedback[feature], "abandoned_why_others" ) with ui.row().classes("w-full max-w-lg justify-between mt-4"): ui.button("Back", on_click=show_page_1).props("outline") next_btn = ui.button("Next", on_click=show_page_3) ui.tooltip("Please answer all questions").bind_visibility_from( next_btn, "enabled", backward=lambda v: not v ) # Reactive validation timer ui.timer(0.5, lambda: next_btn.set_enabled(is_page_2_valid())) # --- PAGE 3: DEMOGRAPHICS --- def show_page_3(): container.clear() with container: ui.label("Final thoughts").classes("text-h4 mb-4") with ui.card().classes("w-full max-w-lg p-6"): suggestions = ( ui.textarea("Do you have any suggestions for the app") .classes("w-full") .bind_value(state, "suggestions") ) ui.label("Select exactly 2 features to prioritize:") priority_tasks = ( ui.select( options=const.PRIORITY_TASKS, multiple=True, validation={"Please select only 2 features": lambda value: len(value) <= 2}, ) .props("use-chips") .classes("w-full") .bind_value(state, "priority_tasks") ) with ui.row().classes("w-full justify-between mt-6"): ui.button("Back", on_click=show_page_2).props("outline") next_btn = ui.button("Review Survey", on_click=show_confirmation).classes("bg-blue") ui.tooltip("Please answer all questions").bind_visibility_from( next_btn, "enabled", backward=lambda v: not v ) ui.timer( 0.5, lambda: next_btn.set_enabled(len(state.suggestions) > 0 and len(state.priority_tasks) == 2), ) # --- PAGE 4: CONFIRMATION (SUMMARY) --- def show_confirmation(): container.clear() with container: ui.label("Review Your Answers").classes("text-h4 mb-4") with ui.card().classes("w-full max-w-2xl p-6"): with ui.column().classes("w-full gap-2"): ui.markdown(f"**Frequency**: {state.frequency}") if state.no_use_reason: ui.markdown(f"**Reasons for not using**: {state.no_use_reason}") feature_text = ", ".join(const.TASK_OPTIONS[i] for i in state.selected_features if i != "Others") feature_text += f", {state.other_tasks}" if state.other_tasks else "" ui.markdown(f"**Features**: {feature_text}") ui.markdown(f"**Suggestions**: {state.suggestions}") ui.markdown(f"**Priority**: {', '.join(state.priority_tasks)}") ui.separator().classes("my-2") with ui.column().classes("w-full gap-2"): ui.markdown("#### Your feedback for tasks") for fe, fd in state.feature_feedback.items(): ui.markdown(f"##### {const.TASK_OPTIONS[fe]}") if fd["completed_on_arq"] == "Yes": ui.markdown(f"**Rating**: {fd['rating']}") if fd["low_rating_suggestions"]: ui.markdown(f"**Suggestions**: {fd['low_rating_suggestions']}") if fd["arq_helped"]: ui.markdown(f"**Arq helped**: {', '.join(fd['arq_helped'])}") if fd["excel_features"]: ui.markdown(f"**Excel features used**: {', '.join(fd['excel_features'])}") if fd["other_apps"]: ui.markdown(f"**Other apps used**: {fd['other_apps']}") if fd["abandoned_why"]: ui.markdown(f"**Task abandoned becuase**: {', '.join(fd['abandoned_why'])}") if fd["abandoned_why_others"]: ui.markdown(f"**Task abandoned becuase**: {fd['abandoned_why_others']}") with ui.row().classes("w-full justify-between mt-8"): ui.button("Back to Edit", on_click=show_page_3).props("outline") ui.button("Confirm & Submit", on_click=handle_final_submit).classes("bg-green") # --- FINAL PAGE: ACKNOWLEDGMENT --- def handle_final_submit(): state.save_to_json() container.clear() with container: with ui.card().classes("w-full max-w-lg p-12 items-center text-center"): ui.icon("verified", color="green").classes("text-6xl") ui.label("Submission Successful").classes("text-h4 mt-2") ui.label("Thank you for your time. Your responses have been saved.").classes("text-grey-7") # No buttons here prevents the user from going back show_page_1() with ui.right_drawer(value=True, fixed=True).classes("bg-blue-50 p-4").props("elevated width=450") as right_drawer: ui.label("Live Response Tracker").classes("text-grey font-bold") debug_display = ui.markdown().classes("font-mono w-full") def update_debug(): # Convert dictionary to a pretty-printed JSON string wrapped in markdown code blocks data = state.convert_to_json() formatted_json = json.dumps(data, indent=2) debug_display.content = f"```json\n{formatted_json}\n```" create_survey() ui.timer(0.1, update_debug) ui.run(port=8081)