nicegui-survey/survey.py

351 lines
16 KiB
Python

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)