add metrics to tasks
This commit is contained in:
@@ -73,10 +73,12 @@ func BaseQuestions(interfaceType InterfaceType) Questions {
|
||||
return Questions{
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Key("task_completed").
|
||||
Title("Did you complete the task?"),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[int]().Value(&taskRating).
|
||||
Key("task_difficulty").
|
||||
Options(
|
||||
huh.NewOption("Very easy", 1),
|
||||
huh.NewOption("Easy", 2),
|
||||
|
||||
@@ -51,22 +51,35 @@ func (t *CodingTask) Questions(interfaceType InterfaceType) (questions Questions
|
||||
return BaseQuestions(interfaceType).
|
||||
With(
|
||||
ReplQuestion(interfaceType,
|
||||
huh.NewConfirm().Title("Question only for REPL interface")),
|
||||
huh.NewConfirm().Key("repl_experience").Title("Question only for REPL interface")),
|
||||
).
|
||||
With(
|
||||
WebQuestion(interfaceType,
|
||||
huh.NewInput().Title("Question only for Web interface")),
|
||||
huh.NewInput().Key("web_feedback").Title("Question only for Web interface")),
|
||||
).
|
||||
With(
|
||||
TUIQuestion(interfaceType,
|
||||
huh.NewConfirm().Title("Question only for TUI interface")),
|
||||
huh.NewConfirm().Key("tui_experience").Title("Question only for TUI interface")),
|
||||
).
|
||||
With(
|
||||
Question(
|
||||
huh.NewConfirm().Title("One last question for all interfaces!")),
|
||||
huh.NewConfirm().Key("final_confirmation").Title("One last question for all interfaces!")),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *CodingTask) QuestionnaireKeys(interfaceType InterfaceType) []string {
|
||||
keys := []string{"task_completed", "task_difficulty", "final_confirmation"}
|
||||
switch interfaceType {
|
||||
case InterfaceTypeREPL:
|
||||
keys = append(keys, "repl_experience")
|
||||
case InterfaceTypeWeb:
|
||||
keys = append(keys, "web_feedback")
|
||||
case InterfaceTypeTUI:
|
||||
keys = append(keys, "tui_experience")
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (t *CodingTask) Setup(ctx context.Context) error {
|
||||
if err := ClearIssues(t.app); err != nil {
|
||||
return err
|
||||
|
||||
@@ -42,6 +42,11 @@ func (t *CreateIssueTask) Questions(interfaceType InterfaceType) Questions {
|
||||
return BaseQuestions(interfaceType)
|
||||
}
|
||||
|
||||
func (t *CreateIssueTask) QuestionnaireKeys(interfaceType InterfaceType) []string {
|
||||
_ = interfaceType
|
||||
return []string{"task_completed", "task_difficulty"}
|
||||
}
|
||||
|
||||
func (t *CreateIssueTask) Setup(ctx context.Context) error {
|
||||
if err := ClearIssues(t.app); err != nil {
|
||||
return err
|
||||
|
||||
@@ -47,6 +47,7 @@ func (t *GitTask) Questions(interfaceType InterfaceType) Questions {
|
||||
return BaseQuestions(interfaceType).With(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().Title("What Git Interface did you use?").
|
||||
Key("git_interface_used").
|
||||
Options(
|
||||
huh.Option[string]{Value: "cli", Key: "Command Line Interface"},
|
||||
huh.Option[string]{Value: "tui", Key: "Terminal User Interface"},
|
||||
@@ -56,6 +57,11 @@ func (t *GitTask) Questions(interfaceType InterfaceType) Questions {
|
||||
)
|
||||
}
|
||||
|
||||
func (t *GitTask) QuestionnaireKeys(interfaceType InterfaceType) []string {
|
||||
_ = interfaceType
|
||||
return []string{"task_completed", "task_difficulty", "git_interface_used"}
|
||||
}
|
||||
|
||||
func (t *GitTask) Setup(ctx context.Context) error {
|
||||
if err := ClearIssues(t.app); err != nil {
|
||||
return err
|
||||
|
||||
@@ -13,17 +13,19 @@ type Questions = models.Questions
|
||||
type QuestionnaireModel struct {
|
||||
Questions
|
||||
form *huh.Form
|
||||
keys []string
|
||||
width, height int
|
||||
userQuit bool
|
||||
}
|
||||
|
||||
func NewQuestionnaireModel(questions Questions) *QuestionnaireModel {
|
||||
func NewQuestionnaireModel(questions Questions, keys []string) *QuestionnaireModel {
|
||||
form := huh.NewForm(questions...).
|
||||
WithTheme(style.HuhCenterTheme()).WithLayout(huh.LayoutGrid(1, 1))
|
||||
|
||||
return &QuestionnaireModel{
|
||||
Questions: questions,
|
||||
form: form,
|
||||
keys: keys,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,3 +74,26 @@ func (q *QuestionnaireModel) SetSize(width, height int) {
|
||||
func (q QuestionnaireModel) GetUserQuit() bool {
|
||||
return q.userQuit
|
||||
}
|
||||
|
||||
func (q QuestionnaireModel) GetCompleted() bool {
|
||||
return q.form != nil && q.form.State == huh.StateCompleted
|
||||
}
|
||||
|
||||
func (q QuestionnaireModel) GetAnswers() map[string]any {
|
||||
if q.form == nil || len(q.keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
answers := make(map[string]any)
|
||||
for _, key := range q.keys {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
answers[key] = q.form.Get(key)
|
||||
}
|
||||
if len(answers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package task
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
@@ -20,6 +21,10 @@ type InterfaceType = models.InterfaceType
|
||||
type ValidatedInterface = models.ValidatedInterface
|
||||
type ValidationFeedback = models.ValidationFeedback
|
||||
|
||||
type QuestionnaireKeysProvider interface {
|
||||
QuestionnaireKeys(InterfaceType) []string
|
||||
}
|
||||
|
||||
var ErrUserQuit = models.ErrUserQuit
|
||||
|
||||
// RunTask orchestrates the complete task execution flow:
|
||||
@@ -28,7 +33,53 @@ var ErrUserQuit = models.ErrUserQuit
|
||||
// 3. Run the interface
|
||||
// 4. Start validation loop in background
|
||||
// 5. Show questionnaire when done
|
||||
func RunTask(ctx context.Context, t Tasker, i Interface, iType InterfaceType) error {
|
||||
func RunTask(ctx context.Context, app *App, t Tasker, i Interface, iType InterfaceType) (runErr error) {
|
||||
config := t.Config()
|
||||
details := t.Details()
|
||||
|
||||
var logger *slog.Logger
|
||||
if app != nil {
|
||||
logger = app.Logger
|
||||
}
|
||||
|
||||
collector := newTaskRunCollector(details.Title, iType, logger)
|
||||
collector.log("info", "task run started")
|
||||
|
||||
config = config.WithActionLogger(func(action string) {
|
||||
collector.recordUserAction(action)
|
||||
})
|
||||
|
||||
defer func() {
|
||||
if runErr != nil {
|
||||
collector.setError(runErr)
|
||||
collector.log("error", runErr.Error())
|
||||
}
|
||||
|
||||
run := collector.finalize()
|
||||
|
||||
if err := appendTaskMetrics(config.StatisticsStoragePath, details.Title, run, logger); err != nil {
|
||||
if runErr == nil {
|
||||
runErr = fmt.Errorf("failed to persist task metrics: %w", err)
|
||||
return
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Warn("failed to persist task metrics", "error", err, "task", details.Title)
|
||||
}
|
||||
}
|
||||
|
||||
if app != nil && app.Stats != nil {
|
||||
if err := app.Stats.RecordTaskRun(ctx, run); err != nil {
|
||||
if runErr == nil {
|
||||
runErr = fmt.Errorf("failed to update global statistics: %w", err)
|
||||
return
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Warn("failed to update global statistics", "error", err, "task", details.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
doneChan := make(chan bool, 1)
|
||||
quitChan := make(chan bool, 1)
|
||||
feedbackChan := make(chan ValidationFeedback, 10)
|
||||
@@ -38,60 +89,94 @@ func RunTask(ctx context.Context, t Tasker, i Interface, iType InterfaceType) er
|
||||
}
|
||||
|
||||
// Setup task
|
||||
collector.log("info", "setting up task")
|
||||
if err := t.Setup(ctx); err != nil {
|
||||
return fmt.Errorf("failed to setup task: %w", err)
|
||||
}
|
||||
collector.log("info", "task setup completed")
|
||||
|
||||
// Show task intro
|
||||
detailsScreen := NewTaskModel(t.Details())
|
||||
collector.log("info", "showing task intro")
|
||||
detailsScreen := NewTaskModel(details)
|
||||
model, err := tea.NewProgram(detailsScreen, tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
|
||||
collector.log("info", "user quit during task intro")
|
||||
return ErrUserQuit
|
||||
}
|
||||
collector.log("info", "task intro completed")
|
||||
|
||||
// Start validation loop
|
||||
go startValidationLoop(ctx, t, feedbackChan, doneChan, quitChan)
|
||||
collector.log("info", "starting validation loop")
|
||||
go startValidationLoop(ctx, t, feedbackChan, doneChan, quitChan, collector.recordValidation)
|
||||
|
||||
// Run interface
|
||||
collector.log("info", "starting task interface")
|
||||
interfaceDone := make(chan error, 1)
|
||||
go func() {
|
||||
interfaceDone <- i.Run(ctx, t.Config())
|
||||
interfaceDone <- i.Run(ctx, config)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-doneChan:
|
||||
close(quitChan)
|
||||
collector.setCompleted(true)
|
||||
collector.log("info", "task validation completed")
|
||||
if err := <-interfaceDone; err != nil {
|
||||
fmt.Printf("warning: interface error after task completion: %v\n", err)
|
||||
if logger != nil {
|
||||
logger.Warn("interface error after task completion", "error", err, "task", details.Title)
|
||||
}
|
||||
collector.log("warn", fmt.Sprintf("interface error after task completion: %v", err))
|
||||
}
|
||||
fmt.Println("Task completed successfully!")
|
||||
|
||||
case err := <-interfaceDone:
|
||||
close(quitChan)
|
||||
if err != nil {
|
||||
collector.log("error", fmt.Sprintf("task interface failed: %v", err))
|
||||
return fmt.Errorf("failed to start task interface: %w", err)
|
||||
}
|
||||
collector.log("info", "task interface exited before completion")
|
||||
fmt.Println("Task incomplete - you exited early")
|
||||
}
|
||||
|
||||
// Show questionnaire
|
||||
collector.log("info", "showing post-task questionnaire")
|
||||
questions := t.Questions(iType)
|
||||
questionare := NewQuestionnaireModel(questions)
|
||||
questionnaireKeys := []string{}
|
||||
if provider, ok := t.(QuestionnaireKeysProvider); ok {
|
||||
questionnaireKeys = provider.QuestionnaireKeys(iType)
|
||||
}
|
||||
questionare := NewQuestionnaireModel(questions, questionnaireKeys)
|
||||
model, err = tea.NewProgram(questionare, tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
questionnaireCompleted := false
|
||||
questionnaireAnswers := map[string]any(nil)
|
||||
if m, ok := model.(interface {
|
||||
GetCompleted() bool
|
||||
GetAnswers() map[string]any
|
||||
}); ok {
|
||||
questionnaireCompleted = m.GetCompleted()
|
||||
questionnaireAnswers = m.GetAnswers()
|
||||
}
|
||||
|
||||
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
|
||||
collector.recordQuestionnaire(questionnaireCompleted, true, questionnaireAnswers)
|
||||
collector.log("info", "user quit during questionnaire")
|
||||
return ErrUserQuit
|
||||
}
|
||||
collector.recordQuestionnaire(questionnaireCompleted, false, questionnaireAnswers)
|
||||
collector.log("info", "task run finished")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool) {
|
||||
func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool, onFeedback func(ValidationFeedback)) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -99,6 +184,9 @@ func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan Valida
|
||||
select {
|
||||
case <-ticker.C:
|
||||
feedback := t.Validate(ctx)
|
||||
if onFeedback != nil {
|
||||
onFeedback(feedback)
|
||||
}
|
||||
if feedback.Success {
|
||||
feedback.Message = "Task completed successfully!"
|
||||
feedbackChan <- feedback
|
||||
|
||||
Reference in New Issue
Block a user