diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2026-01-13 15:00:42 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-01-13 15:34:45 +0000 |
| commit | b99f5fb3c93022696e64cea9303e64539da1cdf6 (patch) | |
| tree | 97570378ba229eb411a26697bc0cabc7e1393fad | |
| parent | 644a6a79695f8fdc1897dce1d29a15f51fbf2b7f (diff) | |
dashboard/app: add manual AI job triage
Allow to set the Correct flag for completed AI jobs.
| -rw-r--r-- | dashboard/app/ai.go | 44 | ||||
| -rw-r--r-- | dashboard/app/templates/ai_job.html | 15 | ||||
| -rw-r--r-- | dashboard/app/templates/templates.html | 2 |
3 files changed, 59 insertions, 2 deletions
diff --git a/dashboard/app/ai.go b/dashboard/app/ai.go index 9f620384f..4ea52bd7e 100644 --- a/dashboard/app/ai.go +++ b/dashboard/app/ai.go @@ -29,7 +29,9 @@ type uiAIJobsPage struct { } type uiAIJobPage struct { - Header *uiHeader + Header *uiHeader + Job *uiAIJob + // The slice contains the same single Job, just for HTML templates convenience. Jobs []*uiAIJob Results []*uiAIResult Trajectory []*uiAITrajectorySpan @@ -49,6 +51,7 @@ type uiAIJob struct { CodeRevision string CodeRevisionLink string Error string + Correct string } type uiAIResult struct { @@ -103,6 +106,22 @@ func handleAIJobPage(ctx context.Context, w http.ResponseWriter, r *http.Request if err != nil { return err } + if correct := r.FormValue("correct"); correct != "" { + if !job.Finished.Valid || job.Error != "" { + return fmt.Errorf("job is in wrong state to set correct status") + } + switch correct { + case aiCorrectnessCorrect: + job.Correct = spanner.NullBool{Bool: true, Valid: true} + case aiCorrectnessIncorrect: + job.Correct = spanner.NullBool{Bool: false, Valid: true} + default: + job.Correct = spanner.NullBool{} + } + if err := aidb.UpdateJob(ctx, job); err != nil { + return err + } + } trajectory, err := aidb.LoadTrajectory(ctx, job.ID) if err != nil { return err @@ -111,9 +130,11 @@ func handleAIJobPage(ctx context.Context, w http.ResponseWriter, r *http.Request if err != nil { return err } + uiJob := makeUIAIJob(job) page := &uiAIJobPage{ Header: hdr, - Jobs: []*uiAIJob{makeUIAIJob(job)}, + Job: uiJob, + Jobs: []*uiAIJob{uiJob}, Trajectory: makeUIAITrajectory(trajectory), } if m, ok := job.Results.Value.(map[string]any); ok && job.Results.Valid { @@ -131,6 +152,16 @@ func handleAIJobPage(ctx context.Context, w http.ResponseWriter, r *http.Request } func makeUIAIJob(job *aidb.Job) *uiAIJob { + correct := aiCorrectnessIncorrect + if !job.Finished.Valid { + correct = aiCorrectnessPending + } else if job.Error != "" { + correct = aiCorrectnessErrored + } else if !job.Correct.Valid { + correct = aiCorrectnessUnset + } else if job.Correct.Bool { + correct = aiCorrectnessCorrect + } return &uiAIJob{ ID: job.ID, Link: fmt.Sprintf("/ai_job?id=%v", job.ID), @@ -144,6 +175,7 @@ func makeUIAIJob(job *aidb.Job) *uiAIJob { CodeRevision: job.CodeRevision, CodeRevisionLink: vcs.LogLink(vcs.SyzkallerRepo, job.CodeRevision), Error: job.Error, + Correct: correct, } } @@ -447,6 +479,14 @@ func workflowsForBug(bug *Bug, manual bool) map[ai.WorkflowType]bool { return workflows } +const ( + aiCorrectnessCorrect = "β
" + aiCorrectnessIncorrect = "β" + aiCorrectnessUnset = "β" + aiCorrectnessPending = "β³" + aiCorrectnessErrored = "π₯" +) + func nullTime(v spanner.NullTime) time.Time { if !v.Valid { return time.Time{} diff --git a/dashboard/app/templates/ai_job.html b/dashboard/app/templates/ai_job.html index 3aaad429a..5a45654f8 100644 --- a/dashboard/app/templates/ai_job.html +++ b/dashboard/app/templates/ai_job.html @@ -15,6 +15,21 @@ Detailed info on a single AI job execution. {{template "header" .Header}} {{template "ai_job_list" .Jobs}} + {{if and (ne .Job.Correct "β³") (ne .Job.Correct "π₯")}} + <form method="POST"> + <fieldset> + <legend>Result correct:</legend> + <input type="radio" name="correct" id="β
" value="β
" {{if eq .Job.Correct "β
"}}checked{{end}}> + <label for="β
">β
</label> + <input type="radio" name="correct" id="β" value="β" {{if eq .Job.Correct "β"}}checked{{end}}> + <label for="β">β</label> + <input type="radio" name="correct" id="β" value="β" {{if eq .Job.Correct "β"}}checked{{end}}> + <label for="β">β</label> + <button type="submit">Set</button> + </fieldset> + </form> + {{end}} + {{range $res := .Results}} <br><b>{{$res.Name}}:</b><br> <div id="ai_result_div"><pre>{{$res.Value}}</pre></div><br> diff --git a/dashboard/app/templates/templates.html b/dashboard/app/templates/templates.html index a50197664..15f50a287 100644 --- a/dashboard/app/templates/templates.html +++ b/dashboard/app/templates/templates.html @@ -683,6 +683,7 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the <thead><tr> <th><a onclick="return sortTable(this, 'ID', textSort)" href="#">ID</a></th> <th><a onclick="return sortTable(this, 'Workflow', textSort)" href="#">Workflow</a></th> + <th><a onclick="return sortTable(this, 'Correct', textSort)" href="#">Correct</a></th> <th><a onclick="return sortTable(this, 'Description', textSort)" href="#">Description</a></th> <th><a onclick="return sortTable(this, 'Created', textSort)" href="#">Created</a></th> <th><a onclick="return sortTable(this, 'Started', textSort)" href="#">Started</a></th> @@ -696,6 +697,7 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the <tr> <td>{{link $job.Link $job.ID}}</td> <td>{{$job.Workflow}}</td> + <td>{{$job.Correct}}</td> <td>{{link $job.DescriptionLink $job.Description}}</td> <td>{{formatTime $job.Created}}</td> <td>{{formatTime $job.Started}}</td> |
