aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2026-01-13 15:00:42 +0100
committerDmitry Vyukov <dvyukov@google.com>2026-01-13 15:34:45 +0000
commitb99f5fb3c93022696e64cea9303e64539da1cdf6 (patch)
tree97570378ba229eb411a26697bc0cabc7e1393fad
parent644a6a79695f8fdc1897dce1d29a15f51fbf2b7f (diff)
dashboard/app: add manual AI job triage
Allow to set the Correct flag for completed AI jobs.
-rw-r--r--dashboard/app/ai.go44
-rw-r--r--dashboard/app/templates/ai_job.html15
-rw-r--r--dashboard/app/templates/templates.html2
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>