Files
MujicaUI/human_input_request.go
T

127 lines
4.0 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/theme"
)
// HumanInputRequestOptions configure a HumanInputRequest. Question is required.
type HumanInputRequestOptions struct {
Question string
// Options are suggested answers; AllowFree adds a free one.
Options []string
AllowFree bool
// Deadline is when the agent stops waiting; zero waits forever.
Deadline time.Time
Disabled bool
}
// HumanInputResult reports this frame's events.
type HumanInputResult struct {
Element *ui.Element
Submitted bool
// TimedOut reports the frame the deadline passed.
TimedOut bool
}
type humanInputState struct {
draft string
expired bool
}
// HumanInputRequest asks the user a question before a deadline.
func HumanInputRequest(c *ui.Context, answer *string, opts HumanInputRequestOptions) HumanInputResult {
if answer == nil || opts.Question == "" {
panic("mujica: HumanInputRequest needs an answer and a Question")
}
if len(opts.Options) == 0 && !opts.AllowFree {
panic("mujica: HumanInputRequest needs Options or AllowFree")
}
k := Tokens(c)
u := Density(c).Unit()
var r HumanInputResult
r.Element = agentCard(c).Border(theme.BorderWidth, k.Info).Role(ui.RoleGroup).Label(opts.Question)
st := ui.Local(r.Element, "mujica.humaninput", func() humanInputState { return humanInputState{} })
left := time.Duration(0)
if !opts.Deadline.IsZero() {
left = opts.Deadline.Sub(c.Now())
if left <= 0 && !st.expired {
st.expired, r.TimedOut = true, true
}
}
r.Element.Disabled(opts.Disabled || st.expired)
locked := r.Element.IsDisabled()
r.Element.Children(func() {
ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Children(func() {
ui.Icon(c, icons.Must("user")).Size(18, 18).TextColor(k.Info).Shrink(0).Margin(2, 0, 0, 0)
ui.Text(c, opts.Question).FontWeight(600).Grow(1).Shrink(1)
})
if len(opts.Options) > 0 {
items := make([]RadioItem[string], len(opts.Options))
for i, o := range opts.Options {
items[i] = RadioItem[string]{Value: o, Label: o}
}
chosen := *answer
if RadioGroup(c, &chosen, items, RadioGroupOptions{Label: opts.Question, Disabled: locked}).Changed() && !locked {
*answer, st.draft = chosen, ""
}
}
submit := false
if opts.AllowFree {
in := TextInput(c, &st.draft, TextInputOptions{Placeholder: msg(c, "human_input_request.free"),
Label: msg(c, "human_input_request.free"), Disabled: locked})
if in.Changed() && !locked {
*answer = st.draft
}
submit = in.Submitted()
}
ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() {
humanInputClock(c, opts.Deadline, left, st.expired)
blank := strings.TrimSpace(*answer) == ""
if Button(c, msg(c, "human_input_request.send"), ButtonOptions{Disabled: locked || blank}).Clicked() {
submit = true
}
r.Submitted = submit && !locked && !blank
})
})
if len(opts.Options) > 0 && !opts.AllowFree && *answer != "" && !slices.Contains(opts.Options, *answer) {
panic(fmt.Sprintf("mujica: HumanInputRequest answer %q is not an option", *answer))
}
if r.Submitted || r.TimedOut {
slog.Debug("mujica: human_input_request event", "submitted", r.Submitted, "timed_out", r.TimedOut)
}
return r
}
// humanInputClock shows the time left, or that it ran out.
func humanInputClock(c *ui.Context, deadline time.Time, left time.Duration, expired bool) {
k := Tokens(c)
text, tone := "", k.TextMuted
switch {
case expired:
text, tone = msg(c, "human_input_request.timeout"), k.Warning
case !deadline.IsZero():
text = fmt.Sprintf(msg(c, "human_input_request.left"), agentDuration(left.Truncate(time.Second)))
wait := left % time.Second
if wait == 0 {
wait = time.Second
}
c.After(wait)
}
ui.Row(c).Grow(1).Gap(Density(c).Unit()).AlignItems(ui.Center).Role(ui.RoleStatus).Label(text).Children(func() {
if text == "" {
return
}
ui.Icon(c, icons.Must("clock")).Size(14, 14).TextColor(tone)
ui.Text(c, text).TextColor(tone).FontSize(FontSize(c, theme.CaptionSize+1))
})
}