Files
MujicaUI/eval_result_table.go
T
2026-10-05 14:49:07 -04:00

116 lines
3.8 KiB
Go

package mujica
import (
"cmp"
"fmt"
"log/slog"
"math"
"strconv"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
)
// EvalCase is one evaluation case and its outcome.
type EvalCase struct {
ID string
Name string
Score float64
Passed bool
}
// EvalResultTableOptions configure an EvalResultTable.
type EvalResultTableOptions struct {
// Decimals of scores; zero means 2.
Decimals int
Status DataStatus
Error string
}
// EvalResultTableResult is a built EvalResultTable and its events.
type EvalResultTableResult struct {
Table DataTableView
diff string
}
// Diff returns the case whose differences the user asked to see this frame.
func (r EvalResultTableResult) Diff() (string, bool) { return r.diff, r.diff != "" }
// EvalResultTable lists eval cases with score and pass or fail, sortable, with a pass summary; give it a height.
func EvalResultTable(c *ui.Context, s *DataTableState[string], cases []EvalCase, opts EvalResultTableOptions) EvalResultTableResult {
if opts.Decimals < 0 {
panic("mujica: EvalResultTable Decimals is negative")
}
dec := opts.Decimals
if dec == 0 {
dec = 2
}
passed := 0
for _, ec := range cases {
if ec.ID == "" || math.IsNaN(ec.Score) || math.IsInf(ec.Score, 0) {
panic("mujica: EvalCase needs an ID and a finite Score")
}
if ec.Passed {
passed++
}
}
k := Tokens(c)
u := Density(c).Unit()
var res EvalResultTableResult
verdict := func(ec EvalCase) string {
if ec.Passed {
return msg(c, "eval_result_table.pass")
}
return msg(c, "eval_result_table.fail")
}
cols := []DataColumn[EvalCase]{
{ID: "name", Title: msg(c, "eval_result_table.case"), Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(a.Name, b.Name) }, Text: func(e EvalCase) string { return e.Name }},
{ID: "score", Title: msg(c, "eval_result_table.score"), Width: 90, Align: ui.End, Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(a.Score, b.Score) },
Text: func(e EvalCase) string { return strconv.FormatFloat(e.Score, 'f', dec, 64) }},
{ID: "result", Title: msg(c, "eval_result_table.result"), Width: 110, Sortable: true,
Compare: func(a, b EvalCase) int { return cmp.Compare(evalRank(a), evalRank(b)) }, Text: verdict,
Cell: func(e EvalCase) {
icon, col := "circle-check", k.Success
if !e.Passed {
icon, col = "circle-x", k.Danger
}
ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(col).Children(func() {
ui.Icon(c, icons.Must(icon)).Size(14, 14)
ui.Text(c, verdict(e)).FontWeight(600)
})
}},
{ID: "diff", Title: "", Width: 48, Text: func(EvalCase) string { return "" },
Cell: func(e EvalCase) {
b := iconAction(c, icons.Must("arrow-left-right"), fmt.Sprintf(msg(c, "eval_result_table.diff"), e.Name)).Tooltip(msg(c, "eval_result_table.view"))
if b.Clicked() && !b.IsDisabled() {
res.diff = e.ID
slog.Debug("mujica: eval result diff", "case", e.ID)
}
}},
}
col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 2)
col.Children(func() {
if len(cases) > 0 {
ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Role(ui.RoleStatus).Children(func() {
ui.Text(c, fmt.Sprintf(msg(c, "eval_result_table.summary"), passed, len(cases))).FontWeight(600)
ui.Box(c).Height(6).Width(160).Radius(1).Background(k.Danger.Mix(k.Surface, 0.6)).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * float32(passed) / float32(len(cases)), H: r.H}, k.Success, 1)
})
})
}
res.Table = DataTable(c, s, cases, DataTableOptions[EvalCase, string]{Columns: cols, Key: func(e EvalCase) string { return e.ID },
Empty: msg(c, "eval_result_table.empty"), Status: opts.Status, Error: opts.Error})
res.Table.Element.Grow(1)
})
res.Table.Element = col
return res
}
func evalRank(e EvalCase) int {
if e.Passed {
return 1
}
return 0
}