From a51fd848d671230ef3a9caf2c61e93f76efb09d2 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 12 Mar 2026 14:56:07 +0100 Subject: [PATCH] add intro questoinare to gather participant details --- cmd/pm/intro.go | 53 +++++++++---------- cmd/pm/intro_questionare.go | 96 +++++++++++++++++++++++++++++++++++ cmd/pm/runner.go | 12 +++++ internal/app/statistics.go | 26 ++++++++++ internal/models/app.go | 1 + internal/models/statistics.go | 5 +- 6 files changed, 166 insertions(+), 27 deletions(-) create mode 100644 cmd/pm/intro_questionare.go diff --git a/cmd/pm/intro.go b/cmd/pm/intro.go index 7bc2547..7d92203 100644 --- a/cmd/pm/intro.go +++ b/cmd/pm/intro.go @@ -38,34 +38,35 @@ type keyMap struct { Quit key.Binding } -var keys = keyMap{ - Start: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "start survey"), - ), - Continue: key.NewBinding( - key.WithKeys(" ", "j", "l", "down", "right"), - key.WithHelp("space", "continue"), - ), - Back: key.NewBinding( - key.WithKeys("b", "k", "h", "backspace", "up", "left"), - key.WithHelp("b", "back"), - ), - Quit: key.NewBinding( - key.WithKeys("esc", "ctrl+c", "q"), - key.WithHelp("esc", "quit"), - ), -} - type introModel struct { stage int width, height int userQuit bool + keys keyMap } func newIntroModel() introModel { + var keys = keyMap{ + Start: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "start survey"), + ), + Continue: key.NewBinding( + key.WithKeys(" ", "j", "l", "down", "right"), + key.WithHelp("space", "continue"), + ), + Back: key.NewBinding( + key.WithKeys("b", "k", "h", "backspace", "up", "left"), + key.WithHelp("b", "back"), + ), + Quit: key.NewBinding( + key.WithKeys("esc", "ctrl+c", "q"), + key.WithHelp("esc", "quit"), + ), + } return introModel{ stage: 1, + keys: keys, } } @@ -90,18 +91,18 @@ func (m introModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.SetSize(msg.Width, msg.Height) case tea.KeyMsg: switch { - case key.Matches(msg, keys.Start) && m.stage == stages: + case key.Matches(msg, m.keys.Start) && m.stage == stages: return m, tea.Quit - case key.Matches(msg, keys.Continue): + case key.Matches(msg, m.keys.Continue): if m.stage < stages { m.stage++ } - case key.Matches(msg, keys.Back): + case key.Matches(msg, m.keys.Back): if m.stage > 1 { m.stage-- } return m, nil - case key.Matches(msg, keys.Quit): + case key.Matches(msg, m.keys.Quit): m.userQuit = true return m, tea.Quit } @@ -139,11 +140,11 @@ func (m introModel) View() string { b.WriteString(boxStyle.Render(style.TextStyle.Render(content))) b.WriteString("\n") - helpText := "Press " + keys.Continue.Help().Key + " to continue • " + - keys.Back.Help().Key + " to go back • " + keys.Quit.Help().Key + " to quit" + helpText := "Press " + m.keys.Continue.Help().Key + " to continue • " + + m.keys.Back.Help().Key + " to go back • " + m.keys.Quit.Help().Key + " to quit" if m.stage == stages { - helpText += "\nPress " + keys.Start.Help().Key + " to start the survey" + helpText += "\nPress " + m.keys.Start.Help().Key + " to start the survey" } b.WriteString(style.HelpStyle.Render(helpText)) diff --git a/cmd/pm/intro_questionare.go b/cmd/pm/intro_questionare.go new file mode 100644 index 0000000..b1f5de5 --- /dev/null +++ b/cmd/pm/intro_questionare.go @@ -0,0 +1,96 @@ +package main + +import ( + "github.com/LazyBachelor/LazyPM/pkg/task" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" +) + +type IntroQuestionnaire struct{} + +func newIntroQuestionnaire() *IntroQuestionnaire { + return &IntroQuestionnaire{} +} + +func (iq *IntroQuestionnaire) Run() (map[string]any, error) { + model := task.NewQuestionnaireModel(iq.Questions(), iq.Keys()) + app := tea.NewProgram(model, tea.WithAltScreen()) + + if _, err := app.Run(); err != nil { + return nil, err + } + + return model.GetAnswers(), nil +} + +func (iq *IntroQuestionnaire) Questions() task.Questions { + return task.Questions{ + huh.NewGroup( + huh.NewSelect[string](). + Title("Which age group do you belong to?"). + Description("This helps us understand the background of participants."). + Options( + huh.NewOption("Under 18", "under_18"), + huh.NewOption("18–24", "18_24"), + huh.NewOption("25–34", "25_34"), + huh.NewOption("35–44", "35_44"), + huh.NewOption("45+", "45_plus"), + ). + Key("age_group"), + ), + huh.NewGroup( + huh.NewSelect[string](). + Title("Are you a student, employed, or both?"). + Description("This helps us understand your current situation."). + Options( + huh.NewOption("Student", "student"), + huh.NewOption("Employed", "employed"), + huh.NewOption("Both student and employed", "both"), + huh.NewOption("Neither", "neither"), + ). + Key("occupation_status"), + ), + huh.NewGroup( + huh.NewSelect[string](). + Title("How far along are you in your education?"). + Description("Select the option that best matches your current level."). + Options( + huh.NewOption("Primary school", "primary"), + huh.NewOption("Secondary school", "secondary"), + huh.NewOption("Bachelor's degree", "bachelor"), + huh.NewOption("Master's degree", "master"), + huh.NewOption("PhD / Doctorate", "phd"), + huh.NewOption("Other", "other"), + ). + Key("education_level"), + ), + huh.NewGroup( + huh.NewSelect[string](). + Title("How would you describe your experience with the command line?"). + Description("This helps us tailor the questions to your experience level."). + Options( + huh.NewOption("No experience", "none"), + huh.NewOption("Some experience", "some"), + huh.NewOption("Extensive experience", "extensive"), + ). + Key("cli_experience"), + ), + huh.NewGroup( + huh.NewSelect[string](). + Title("How often do you use the command line?"). + Description("Select the option that best describes your usage."). + Options( + huh.NewOption("Never", "never"), + huh.NewOption("Rarely", "rarely"), + huh.NewOption("Weekly", "weekly"), + huh.NewOption("Several times a week", "multiple_weekly"), + huh.NewOption("Daily", "daily"), + ). + Key("cli_frequency"), + ), + } +} + +func (iq *IntroQuestionnaire) Keys() []string { + return []string{"age_group", "occupation_status", "education_level", "cli_experience", "cli_frequency"} +} diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index c9def4a..7a96999 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -130,6 +130,18 @@ func runStartCmd(cmd *cobra.Command, args []string) error { return returnIfUserQuit(err, "failed to run intro") } } + + introAnswers, err := newIntroQuestionnaire().Run() + if err != nil { + return returnIfUserQuit(err, "failed to run intro questionnaire") + } + + if app != nil && app.Stats != nil && introAnswers != nil { + if err := app.Stats.RecordIntroQuestionnaireAnswers(introAnswers); err != nil { + cmd.Printf("Failed to record intro questionnaire answers: %v\n", err) + } + } + if err := taskLoop(cmd.Context(), app, surveyTasks, interfaces); err != nil { return returnIfUserQuit(err, "task loop failed") } diff --git a/internal/app/statistics.go b/internal/app/statistics.go index cdf27e4..abd12a6 100644 --- a/internal/app/statistics.go +++ b/internal/app/statistics.go @@ -131,3 +131,29 @@ func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRu return nil } + +func (s *StatisticsService) RecordIntroQuestionnaireAnswers(answers map[string]any) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.storage.Data == nil { + return fmt.Errorf("statistics data not initialized") + } + + s.storage.Data.IntroQuestionnaireAnswers = answers + + if err := s.storage.Save(); err != nil { + if s.logger != nil { + s.logger.Error("failed to save intro questionnaire answers", "error", err) + } + return err + } + + if s.logger != nil { + s.logger.Info("intro questionnaire answers saved", + "answers_count", len(answers), + ) + } + + return nil +} diff --git a/internal/models/app.go b/internal/models/app.go index 9b81226..e1e5603 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -61,4 +61,5 @@ type StatsService interface { GetStatistics() (Statistics, error) GetParticipantID() primitive.ObjectID RecordTaskRun(ctx context.Context, run TaskRunMetrics) error + RecordIntroQuestionnaireAnswers(answers map[string]any) error } diff --git a/internal/models/statistics.go b/internal/models/statistics.go index 41041ac..fe4c8df 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -20,7 +20,10 @@ type Statistics struct { TotalDurationMs int64 `bson:"total_duration_ms" json:"total_duration_ms"` AverageDurationMs int64 `bson:"average_duration_ms" json:"average_duration_ms"` - TotalUserActions int `bson:"total_user_actions" json:"total_user_actions"` + TotalUserActions int `bson:"total_user_actions" json:"total_user_actions"` + + IntroQuestionnaireAnswers map[string]any `bson:"intro_questionnaire_answers" json:"intro_questionnaire_answers"` + QuestionnairesCompleted int `bson:"questionnaires_completed" json:"questionnaires_completed"` QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"`