Merge pull request #58 from LazyBachelor/LPM-114
LPM-114 - add intro questoinare to gather participant details
This commit is contained in:
@@ -38,6 +38,14 @@ type keyMap struct {
|
|||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type introModel struct {
|
||||||
|
stage int
|
||||||
|
width, height int
|
||||||
|
userQuit bool
|
||||||
|
keys keyMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIntroModel() introModel {
|
||||||
var keys = keyMap{
|
var keys = keyMap{
|
||||||
Start: key.NewBinding(
|
Start: key.NewBinding(
|
||||||
key.WithKeys("enter"),
|
key.WithKeys("enter"),
|
||||||
@@ -56,16 +64,9 @@ var keys = keyMap{
|
|||||||
key.WithHelp("esc", "quit"),
|
key.WithHelp("esc", "quit"),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
type introModel struct {
|
|
||||||
stage int
|
|
||||||
width, height int
|
|
||||||
userQuit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIntroModel() introModel {
|
|
||||||
return introModel{
|
return introModel{
|
||||||
stage: 1,
|
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)
|
m.SetSize(msg.Width, msg.Height)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, keys.Start) && m.stage == stages:
|
case key.Matches(msg, m.keys.Start) && m.stage == stages:
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case key.Matches(msg, keys.Continue):
|
case key.Matches(msg, m.keys.Continue):
|
||||||
if m.stage < stages {
|
if m.stage < stages {
|
||||||
m.stage++
|
m.stage++
|
||||||
}
|
}
|
||||||
case key.Matches(msg, keys.Back):
|
case key.Matches(msg, m.keys.Back):
|
||||||
if m.stage > 1 {
|
if m.stage > 1 {
|
||||||
m.stage--
|
m.stage--
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case key.Matches(msg, keys.Quit):
|
case key.Matches(msg, m.keys.Quit):
|
||||||
m.userQuit = true
|
m.userQuit = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
@@ -139,11 +140,11 @@ func (m introModel) View() string {
|
|||||||
b.WriteString(boxStyle.Render(style.TextStyle.Render(content)))
|
b.WriteString(boxStyle.Render(style.TextStyle.Render(content)))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
helpText := "Press " + keys.Continue.Help().Key + " to continue • " +
|
helpText := "Press " + m.keys.Continue.Help().Key + " to continue • " +
|
||||||
keys.Back.Help().Key + " to go back • " + keys.Quit.Help().Key + " to quit"
|
m.keys.Back.Help().Key + " to go back • " + m.keys.Quit.Help().Key + " to quit"
|
||||||
|
|
||||||
if m.stage == stages {
|
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))
|
b.WriteString(style.HelpStyle.Render(helpText))
|
||||||
|
|||||||
104
cmd/pm/intro_questionnare.go
Normal file
104
cmd/pm/intro_questionnare.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
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())
|
||||||
|
|
||||||
|
m, err := app.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if q, ok := m.(*task.QuestionnaireModel); ok {
|
||||||
|
if q.GetUserQuit() {
|
||||||
|
return nil, task.ErrUserQuit
|
||||||
|
}
|
||||||
|
return q.GetAnswers(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, 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"}
|
||||||
|
}
|
||||||
@@ -129,7 +129,19 @@ func runStartCmd(cmd *cobra.Command, args []string) error {
|
|||||||
if err := newIntroModel().Run(); err != nil {
|
if err := newIntroModel().Run(); err != nil {
|
||||||
return returnIfUserQuit(err, "failed to run intro")
|
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 {
|
if err := taskLoop(cmd.Context(), app, surveyTasks, interfaces); err != nil {
|
||||||
return returnIfUserQuit(err, "task loop failed")
|
return returnIfUserQuit(err, "task loop failed")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,3 +131,29 @@ func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRu
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,4 +61,5 @@ type StatsService interface {
|
|||||||
GetStatistics() (Statistics, error)
|
GetStatistics() (Statistics, error)
|
||||||
GetParticipantID() primitive.ObjectID
|
GetParticipantID() primitive.ObjectID
|
||||||
RecordTaskRun(ctx context.Context, run TaskRunMetrics) error
|
RecordTaskRun(ctx context.Context, run TaskRunMetrics) error
|
||||||
|
RecordIntroQuestionnaireAnswers(answers map[string]any) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ type Statistics struct {
|
|||||||
AverageDurationMs int64 `bson:"average_duration_ms" json:"average_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"`
|
QuestionnairesCompleted int `bson:"questionnaires_completed" json:"questionnaires_completed"`
|
||||||
QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"`
|
QuestionnairesAbandoned int `bson:"questionnaires_abandoned" json:"questionnaires_abandoned"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user