diff --git a/constants.py b/constants.py index 3fdae3d..12308d6 100644 --- a/constants.py +++ b/constants.py @@ -1,6 +1,12 @@ PAGE_TITLE = "ARQ Usage Survey" -USAGE_FREQUENCY_OPTIONS = ["daily", "weekly", "monthly", "Never used it"] +USAGE_FREQUENCY_OPTIONS = [ + "Multiple times a day", + "Multiple times a week", + "A few times a month", + "Tried once or twice", + "Never used it", +] TASK_OPTIONS = { "analysis": "Stock Analysis", diff --git a/secret_survey.py b/secret_survey.py index ee823e7..2af1c28 100644 --- a/secret_survey.py +++ b/secret_survey.py @@ -1,45 +1,55 @@ +import datetime import json -import secrets +import os from nicegui import app, ui +import constants as const + # --- 1. User Database & Token Generation --- # In a real app, you'd save these to a file or DB. # Generating unique tokens for your three users: -USER_MAP = {"k9a2_xJv1": "Ken", "Lz78_pQn9": "Liz", "Rb55_mTk2": "Ruben"} +# USER_MAP = {"k9a2_xJv1": "Ken", "Lz78_pQn9": "Liz", "Rb55_mTk2": "Ruben"} -# --- Survey Options & State --- -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"] +with open("user_secrets.json") as f: + SECRETS = json.load(f) class SurveyState: + def __init__(self, name): self.user_name = name self.frequency = None + self.no_use_reason = "" self.selected_features = [] + self.other_tasks = "" self.feature_feedback = {} - self.age = None - self.gender = None - self.location = "" + self.suggestions = "" + self.priority_tasks = [] + + def convert_to_json(self): + return { + "name": self.user_name, + "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 = { - "user": self.user_name, - "usage": {"frequency": self.frequency, "features_used": self.selected_features}, - "feature_feedback": self.feature_feedback, - "demographics": {"age": self.age, "gender": self.gender, "location": self.location}, - } - filename = f"survey_{self.user_name.lower()}.json" - with open(filename, "w") as f: + 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) # --- 2. The Main Page Function --- @ui.page("/") -def main_page(token: str): +def main_page(token: str = None): # Extract 'id' from the URL query parameters # user_token = app.native.main_window.get_url() if hasattr(app, "native") else "" # Standard way in web mode: @@ -51,7 +61,7 @@ def main_page(token: str): ui.label("Please check your unique link and try again.").classes("text-grey") return - if token not in USER_MAP: + if token not in [i["token"] for i in SECRETS]: with ui.column().classes("w-full items-center mt-20"): ui.icon("error", color="red").classes("text-6xl") ui.label("User not found").classes("text-h4 text-negative") @@ -59,7 +69,20 @@ def main_page(token: str): return # Valid User Found - user_name = USER_MAP[token] + user_name = [i["username"] for i in SECRETS if i["token"] == token][0] + + # Check if user has already answered + files = os.listdir("results") + for file in files: + with open(f"results/{file}") as f: + response = json.load(f) + if response.get("name") == user_name: + with ui.column().classes("w-full items-center mt-20"): + ui.icon("check", color="green").classes("text-6xl") + ui.label("You have already filled the survey!").classes("text-h4 text-positive") + ui.label("Thank You!").classes("text-grey") + return + state = SurveyState(user_name) container = ui.column().classes("w-full items-center q-pa-md") @@ -68,84 +91,227 @@ def main_page(token: str): container.clear() with container: ui.label(f"Welcome, {state.user_name}!").classes("text-h3 text-primary mb-2") - ui.label("Facebook Usage Survey").classes("text-h5 mb-6 text-grey-7") + 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 Facebook?").classes("text-bold") - ui.radio(options=FREQUENCY_OPTIONS).bind_value(state, "frequency") + ui.label("How frequently do you use ARQ?").classes("text-bold") + p1_radio = ui.radio(options=const.USAGE_FREQUENCY_OPTIONS).bind_value(state, "frequency") - ui.label("What features do you use?").classes("text-bold mt-4") - ui.select(options=FEATURE_OPTIONS, multiple=True).classes("w-full").bind_value( - state, "selected_features" + 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") ) - next_btn = ui.button("Next", on_click=show_page_2).classes("mt-6 w-full") + 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") - ui.timer( - 0.5, lambda: next_btn.set_enabled(state.frequency is not None and len(state.selected_features) > 0) - ) + 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 (With fix for terminal errors) --- 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] = {"rating": 5, "likes": "", "other_apps": [], "improvements": ""} + 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(): - for f in state.selected_features: - d = state.feature_feedback[f] - if d["rating"] < 4 and len(d.get("improvements", "")) < 20: - return False - return True + 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"Rate the {feature}").classes("text-h6 text-primary") - stars = ( - ui.rating(value=5, icon="star") - .classes("text-3xl") - .bind_value(state.feature_feedback[feature], "rating") + 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") ) - with ui.column().classes("w-full").bind_visibility_from(stars, "value", backward=lambda v: v >= 4): - ui.textarea("What do you like about it?").classes("w-full").bind_value( - state.feature_feedback[feature], "likes" + # 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(stars, "value", backward=lambda v: v < 4): - ui.label("Other apps you use:").classes("text-caption") - ui.select(options=OTHER_APP_OPTIONS, multiple=True).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 == "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") ) - ui.textarea("Improvements (min 20 characters)").classes("w-full").bind_value( - state.feature_feedback[feature], "improvements" + 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 fully").bind_visibility_from( + 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("Demographics").classes("text-h4 mb-4") + ui.label("Final thoughts").classes("text-h4 mb-4") with ui.card().classes("w-full max-w-lg p-6"): - ui.number("Age", format="%d").classes("w-full").bind_value(state, "age") - ui.select(label="Gender", options=GENDER_OPTIONS).classes("w-full").bind_value(state, "gender") - ui.input("Location").classes("w-full").bind_value(state, "location") + 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") - ui.button("Review Survey", on_click=show_confirmation).classes("bg-blue") + 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 --- def show_confirmation(): @@ -154,14 +320,36 @@ def main_page(token: str): 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.label(f"**Name:** {state.user_name}") - ui.label(f"**Frequency:** {state.frequency}") - ui.separator() - for f in state.selected_features: - d = state.feature_feedback[f] - ui.label(f"**{f}**: {d['rating']} Stars") - ui.separator() - ui.label(f"**Location:** {state.location}") + 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") @@ -178,5 +366,18 @@ def main_page(token: str): 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```" + + ui.timer(0.1, update_debug) + ui.run() diff --git a/survey.py b/survey.py index bc3fe19..5fb2e77 100644 --- a/survey.py +++ b/survey.py @@ -26,11 +26,11 @@ class SurveyState: 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, - "Reason for not using": self.no_use_reason, "suggestions": self.suggestions, "priority_tasks": self.priority_tasks, } @@ -107,6 +107,7 @@ def create_survey(): state.feature_feedback[f] = { "completed_on_arq": None, "rating": 0, + "low_rating_suggestions": "", "completed_how": None, "arq_helped": [], "excel_features": [], @@ -163,6 +164,12 @@ def create_survey(): .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" @@ -273,13 +280,15 @@ def create_survey(): 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}").classes("text-md") + 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}").classes("text-md") + ui.markdown(f"**Features**: {feature_text}") - ui.markdown(f"**Suggestions**: {state.suggestions}").classes("text-md") - ui.markdown(f"**Priority**: {', '.join(state.priority_tasks)}").classes("text-md") + ui.markdown(f"**Suggestions**: {state.suggestions}") + ui.markdown(f"**Priority**: {', '.join(state.priority_tasks)}") ui.separator().classes("my-2") @@ -289,6 +298,8 @@ def create_survey(): 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"]: