WIP components (as built)
This commit is contained in:
71 files changed
+3241
-468
No files matched your search
@@ -1,3 +1,258 @@
|
||||
package main
|
||||
|
||||
var motionPages = []page{}
|
||||
import mujica "mujicaui"
|
||||
|
||||
var motionPages = []page{
|
||||
{
|
||||
ID: "281", Name: "InterruptButton", ZhName: "中断按钮 停止智能体", Category: "Agent Ops & Motion",
|
||||
Demo: motionInterruptDemo,
|
||||
Code: `running, stopping := true, false
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.InterruptButton(c, running, mujica.InterruptButtonOptions{Confirm: true, Interrupting: stopping})
|
||||
if r.Changed() {
|
||||
stopping = true // cancel the run, then clear running and stopping
|
||||
}
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Tab focuses Stop; Enter or Space interrupts. With Confirm, the popup focuses Keep running; Tab reaches Stop; Escape cancels.",
|
||||
},
|
||||
{
|
||||
ID: "282", Name: "TokenUsageChart", ZhName: "Token 用量图 输入输出", Category: "Agent Ops & Motion",
|
||||
Demo: motionTokenUsageDemo,
|
||||
Code: `usage := []mujica.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.TokenUsageChart(c, usage, mujica.TokenUsageChartOptions{})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Tab focuses the chart; Left/Right move between bars or points and the readout is announced.",
|
||||
},
|
||||
{
|
||||
ID: "283", Name: "CostBreakdown", ZhName: "费用分解 成本 合计", Category: "Agent Ops & Motion",
|
||||
Demo: motionCostDemo,
|
||||
Code: `var state mujica.CostBreakdownState
|
||||
items := []mujica.CostItem{
|
||||
{Model: "opus", Tool: "search", InputTokens: 1000, OutputTokens: 200, Cost: 0.30},
|
||||
{Model: "haiku", Tool: "search", InputTokens: 4000, OutputTokens: 500, Cost: 0.05},
|
||||
}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CostBreakdown(c, &state, items, mujica.CostBreakdownOptions{GroupBy: mujica.CostByModel}).Element.Height(260)
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Up/Down choose a group; column headers sort with Enter; the column menu takes Tab.",
|
||||
},
|
||||
{
|
||||
ID: "284", Name: "TraceViewer", ZhName: "调用链 追踪 跨度 时间轴", Category: "Agent Ops & Motion",
|
||||
Demo: motionTraceDemo,
|
||||
Code: `var state mujica.TraceViewerState
|
||||
spans := []mujica.TraceSpan{
|
||||
{ID: "run", Name: "agent.run", End: 900 * time.Millisecond},
|
||||
{ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond,
|
||||
Attrs: []mujica.TraceAttr{{Key: "model", Value: "opus"}}},
|
||||
}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.TraceViewer(c, &state, spans, mujica.TraceViewerOptions{}).Element.Height(260)
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Up/Down choose a span, Right opens and Left closes it; typing jumps by name.",
|
||||
},
|
||||
{
|
||||
ID: "285", Name: "EvalResultTable", ZhName: "评测结果 得分 通过失败", Category: "Agent Ops & Motion",
|
||||
Demo: motionEvalDemo,
|
||||
Code: `var state mujica.DataTableState[string]
|
||||
cases := []mujica.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.EvalResultTable(c, &state, cases, mujica.EvalResultTableOptions{})
|
||||
r.Table.Element.Height(260)
|
||||
if id, ok := r.Diff(); ok {
|
||||
_ = id // open expected and actual output side by side
|
||||
}
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Up/Down choose a case; the diff button takes Tab, Enter or Space.",
|
||||
},
|
||||
{
|
||||
ID: "286", Name: "ToolRegistryPanel", ZhName: "工具注册 工具列表 启停", Category: "Agent Ops & Motion",
|
||||
Demo: motionToolsDemo,
|
||||
Code: `var state mujica.ListState[string]
|
||||
tools := []mujica.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true,
|
||||
Params: []mujica.ToolParam{{Name: "query", Type: "string", Required: true}}}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.ToolRegistryPanel(c, &state, tools, mujica.ToolRegistryPanelOptions{})
|
||||
r.Element.Height(260)
|
||||
if name, on, ok := r.Toggled(); ok && name == tools[0].Name {
|
||||
tools[0].Enabled = on
|
||||
}
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Up/Down choose a tool; its switch takes Tab and Space.",
|
||||
},
|
||||
{
|
||||
ID: "287", Name: "DotsLoader", ZhName: "三点加载 加载中", Category: "Agent Ops & Motion",
|
||||
Demo: motionLoaderDemo("287", mujica.DotsLoader),
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.DotsLoader(c, mujica.LoaderOptions{Label: "Thinking", ShowLabel: true})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as progress.",
|
||||
},
|
||||
{
|
||||
ID: "288", Name: "BarsLoader", ZhName: "条形加载", Category: "Agent Ops & Motion",
|
||||
Demo: motionLoaderDemo("288", mujica.BarsLoader),
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.BarsLoader(c, mujica.LoaderOptions{Size: 20, Label: "Indexing"})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as progress.",
|
||||
},
|
||||
{
|
||||
ID: "289", Name: "PulseLoader", ZhName: "呼吸加载 脉冲", Category: "Agent Ops & Motion",
|
||||
Demo: motionLoaderDemo("289", mujica.PulseLoader),
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.PulseLoader(c, mujica.LoaderOptions{Label: "Listening", ShowLabel: true})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as progress.",
|
||||
},
|
||||
{
|
||||
ID: "290", Name: "OrbitLoader", ZhName: "环绕加载 菱形", Category: "Agent Ops & Motion",
|
||||
Demo: motionLoaderDemo("290", mujica.OrbitLoader),
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.OrbitLoader(c, mujica.LoaderOptions{Size: 24, Label: "Connecting"})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as progress.",
|
||||
},
|
||||
{
|
||||
ID: "291", Name: "WaveLoader", ZhName: "波形加载", Category: "Agent Ops & Motion",
|
||||
Demo: motionLoaderDemo("291", mujica.WaveLoader),
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.WaveLoader(c, mujica.LoaderOptions{Label: "Speaking", ShowLabel: true})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as progress.",
|
||||
},
|
||||
{
|
||||
ID: "292", Name: "Shimmer", ZhName: "占位扫光 骨架 加载", Category: "Agent Ops & Motion",
|
||||
Demo: motionShimmerDemo,
|
||||
Code: `loading := true
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.Shimmer(c, loading, mujica.ShimmerOptions{Height: 48}, func() {
|
||||
ui.Text(c, "Report ready")
|
||||
})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as progress while loading.",
|
||||
},
|
||||
{
|
||||
ID: "293", Name: "CountUp", ZhName: "数字过渡 计数", Category: "Agent Ops & Motion",
|
||||
Demo: motionCountUpDemo,
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.CountUp(c, 12840, mujica.CountUpOptions{Duration: 800 * time.Millisecond})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; the final value is read out.",
|
||||
},
|
||||
{
|
||||
ID: "294", Name: "Typewriter", ZhName: "打字机 逐字显示", Category: "Agent Ops & Motion",
|
||||
Demo: motionTypewriterDemo,
|
||||
Code: `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.Typewriter(c, "The court assembles at dusk.", mujica.TypewriterOptions{Speed: 30, Skippable: true})
|
||||
if r.Changed() {
|
||||
// show the next message
|
||||
}
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Skip takes Tab, Enter or Space.",
|
||||
},
|
||||
{
|
||||
ID: "295", Name: "ShimmerText", ZhName: "文字扫光 思考中", Category: "Agent Ops & Motion",
|
||||
Demo: motionShimmerTextDemo,
|
||||
Code: `thinking := true
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.ShimmerText(c, "Thinking…", mujica.ShimmerTextOptions{Active: thinking})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; read as a status.",
|
||||
},
|
||||
{
|
||||
ID: "296", Name: "Stagger", ZhName: "依次进场 交错动画", Category: "Agent Ops & Motion",
|
||||
Demo: motionStaggerDemo,
|
||||
Code: `guests := []string{"Sakiko", "Uika", "Mutsumi"}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.Stagger(c, len(guests), mujica.StaggerOptions{}, func(i int) {
|
||||
ui.Text(c, guests[i])
|
||||
})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable.",
|
||||
},
|
||||
{
|
||||
ID: "297", Name: "ReorderTransition", ZhName: "重排过渡 排序动画", Category: "Agent Ops & Motion",
|
||||
Demo: motionReorderDemo,
|
||||
Code: `order := []string{"Sakiko", "Uika", "Mutsumi"}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.ReorderTransition(c, order, mujica.ReorderTransitionOptions{Gap: 4}, func(name string) {
|
||||
ui.Text(c, name)
|
||||
})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Focus inside an item follows it as it moves.",
|
||||
},
|
||||
{
|
||||
ID: "298", Name: "LayoutTransition", ZhName: "布局过渡 FLIP", Category: "Agent Ops & Motion",
|
||||
Demo: motionLayoutDemo,
|
||||
Code: `open := false
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.LayoutTransition(c, mujica.LayoutTransitionOptions{Clip: true}, func() {
|
||||
ui.Text(c, "Program")
|
||||
if open {
|
||||
ui.Text(c, "Overture · Masquerade · Finale")
|
||||
}
|
||||
})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable.",
|
||||
},
|
||||
{
|
||||
ID: "299", Name: "Presence", ZhName: "进出场 淡入淡出", Category: "Agent Ops & Motion",
|
||||
Demo: motionPresenceDemo,
|
||||
Code: `show := true
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.Presence(c, show, mujica.PresenceOptions{Kind: mujica.PresenceSlide}, func() {
|
||||
ui.Text(c, "Saved")
|
||||
})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Leaving content takes no focus.",
|
||||
},
|
||||
{
|
||||
ID: "300", Name: "BlinkHighlight", ZhName: "变化高亮 闪烁 涨跌", Category: "Agent Ops & Motion",
|
||||
Demo: motionBlinkDemo,
|
||||
Code: `price := 101.5
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.BlinkHighlight(c, price, mujica.BlinkHighlightOptions{})
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`,
|
||||
Keys: "Not focusable; the value and direction are read out.",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
// motionLoaderDemo shows a loader while loading.
|
||||
func motionLoaderDemo(id string, loader func(c *ui.Context, o mujica.LoaderOptions) *ui.Element) func(c *ui.Context, log func(string)) {
|
||||
return func(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
loading, label bool
|
||||
size float64
|
||||
}
|
||||
s := ui.Local(c.Root(), id, func() state { return state{loading: true, label: true, size: 20} })
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.loading, "Loading", log)
|
||||
navigationCheck(c, &s.label, "Show label", log)
|
||||
navigationSlider(c, &s.size, "Size", 12, 48, log)
|
||||
}, func() {
|
||||
ui.Row(c).Gap(24).AlignItems(ui.Center).Children(func() {
|
||||
if !s.loading {
|
||||
ui.Text(c, "Done.")
|
||||
return
|
||||
}
|
||||
loader(c, mujica.LoaderOptions{Size: float32(s.size), Label: "Thinking", ShowLabel: s.label})
|
||||
loader(c, mujica.LoaderOptions{Size: float32(s.size), Label: "Indexing", Color: mujica.Tokens(c).Info})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func motionShimmerDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ loading bool }
|
||||
s := ui.Local(c.Root(), "292", func() state { return state{loading: true} })
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.loading, "Loading", log)
|
||||
}, func() {
|
||||
mujica.Shimmer(c, s.loading, mujica.ShimmerOptions{Height: 56, Label: "Loading report"}, func() {
|
||||
ui.Text(c, "Quarterly report: 12 chapters, 340 pages.")
|
||||
})
|
||||
mujica.Shimmer(c, s.loading, mujica.ShimmerOptions{Width: 240, Height: 20}, func() {
|
||||
ui.Text(c, "Updated today").TextColor(mujica.Tokens(c).TextMuted)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionCountUpDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ value, duration, decimals float64 }
|
||||
s := ui.Local(c.Root(), "293", func() state { return state{value: 1280, duration: 120} })
|
||||
navigationDemo(c, func() {
|
||||
navigationSlider(c, &s.duration, "Duration (ms)", 120, 1200, log)
|
||||
navigationSlider(c, &s.decimals, "Decimals", 0, 4, log)
|
||||
if mujica.Button(c, "Add 500", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.value += 500
|
||||
log(fmt.Sprintf("Value %.0f", s.value))
|
||||
}
|
||||
if mujica.Button(c, "Random", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.value = float64(rand.Intn(100000))
|
||||
log(fmt.Sprintf("Value %.0f", s.value))
|
||||
}
|
||||
}, func() {
|
||||
mujica.CountUp(c, s.value, mujica.CountUpOptions{Duration: time.Duration(s.duration) * time.Millisecond, Decimals: int(s.decimals)}).FontSize(mujica.FontSize(c, 32))
|
||||
mujica.CountUp(c, s.value/1000, mujica.CountUpOptions{Decimals: 2, Duration: time.Second})
|
||||
})
|
||||
}
|
||||
|
||||
func motionTypewriterDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
text string
|
||||
speed float64
|
||||
round int
|
||||
}
|
||||
s := ui.Local(c.Root(), "294", func() state { return state{text: "The court assembles at dusk.", speed: 30} })
|
||||
navigationDemo(c, func() {
|
||||
navigationSlider(c, &s.speed, "Speed", 5, 120, log)
|
||||
if mujica.Button(c, "Append", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.text += " The music begins."
|
||||
log("Appended")
|
||||
}
|
||||
if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.text = "A new letter arrives from the duchess."
|
||||
s.round++
|
||||
log("Restarted")
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Key(s.round).Children(func() {
|
||||
if mujica.Typewriter(c, s.text, mujica.TypewriterOptions{Speed: s.speed, Skippable: true}).Changed() {
|
||||
log("Finished")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionShimmerTextDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ active bool }
|
||||
s := ui.Local(c.Root(), "295", func() state { return state{active: true} })
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.active, "Thinking", log)
|
||||
}, func() {
|
||||
mujica.ShimmerText(c, "Thinking about the program…", mujica.ShimmerTextOptions{Active: s.active})
|
||||
mujica.ShimmerText(c, "思考中…", mujica.ShimmerTextOptions{Active: s.active})
|
||||
})
|
||||
}
|
||||
|
||||
func motionStaggerDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
round int
|
||||
interval float64
|
||||
horizontal bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "296", func() state { return state{interval: 60} })
|
||||
guests := []string{"Sakiko", "Mutsumi", "Uika", "Umiri", "Nyamu"}
|
||||
navigationDemo(c, func() {
|
||||
navigationSlider(c, &s.interval, "Interval (ms)", 20, 300, log)
|
||||
navigationCheck(c, &s.horizontal, "Horizontal", log)
|
||||
if mujica.Button(c, "Replay", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.round++
|
||||
log(fmt.Sprintf("Replay %d", s.round))
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Key(s.round).Children(func() {
|
||||
mujica.Stagger(c, len(guests), mujica.StaggerOptions{Interval: time.Duration(s.interval) * time.Millisecond, Horizontal: s.horizontal}, func(i int) {
|
||||
ui.Text(c, guests[i]+" arrives").Padding(6, 10).Background(mujica.Tokens(c).Surface).Radius(4).Margin(0, 0, 4, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionReorderDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
order []string
|
||||
gap float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "297", func() state { return state{order: []string{"Overture", "Masquerade", "Interlude", "Finale"}, gap: 4} })
|
||||
navigationDemo(c, func() {
|
||||
navigationSlider(c, &s.gap, "Gap", 0, 24, log)
|
||||
if mujica.Button(c, "Shuffle", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
next := append([]string(nil), s.order...)
|
||||
rand.Shuffle(len(next), func(i, j int) { next[i], next[j] = next[j], next[i] })
|
||||
s.order = next
|
||||
log(fmt.Sprintf("Order %v", s.order))
|
||||
}
|
||||
if mujica.Button(c, "Reverse", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
next := make([]string, len(s.order))
|
||||
for i, v := range s.order {
|
||||
next[len(next)-1-i] = v
|
||||
}
|
||||
s.order = next
|
||||
log(fmt.Sprintf("Order %v", s.order))
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Width(280).Children(func() {
|
||||
mujica.ReorderTransition(c, s.order, mujica.ReorderTransitionOptions{Gap: float32(s.gap)}, func(name string) {
|
||||
ui.Text(c, name).Padding(8, 12).Background(mujica.Tokens(c).Surface).Radius(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionLayoutDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ open, clip bool }
|
||||
s := ui.Local(c.Root(), "298", func() state { return state{clip: true} })
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.open, "Expanded", log)
|
||||
navigationCheck(c, &s.clip, "Clip", log)
|
||||
}, func() {
|
||||
ui.Column(c).AlignItems(ui.Start).Children(func() {
|
||||
mujica.LayoutTransition(c, mujica.LayoutTransitionOptions{Clip: s.clip}, func() {
|
||||
ui.Column(c).Padding(12).Gap(6).Background(mujica.Tokens(c).Surface).Radius(4).Children(func() {
|
||||
ui.Text(c, "Program").FontWeight(600)
|
||||
if s.open {
|
||||
for _, p := range []string{"Overture", "Masquerade", "Interlude", "Finale"} {
|
||||
ui.Text(c, p)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionPresenceDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
show bool
|
||||
kind int
|
||||
}
|
||||
s := ui.Local(c.Root(), "299", func() state { return state{show: true} })
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.show, "Show", log)
|
||||
if mujica.SegmentedControl(c, &s.kind, []mujica.Segment{{Label: "Fade"}, {Label: "Slide"}, {Label: "Collapse"}}, mujica.SegmentedControlOptions{Label: "Kind"}).Changed() {
|
||||
log(fmt.Sprintf("Kind %d", s.kind))
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Width(320).Children(func() {
|
||||
mujica.Presence(c, s.show, mujica.PresenceOptions{Kind: mujica.PresenceKind(s.kind)}, func() {
|
||||
ui.Text(c, "Your changes were saved.").Padding(10, 12).Background(mujica.Tokens(c).Surface).Radius(4)
|
||||
})
|
||||
ui.Text(c, "Content below moves with it.").TextColor(mujica.Tokens(c).TextMuted).Padding(8, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionBlinkDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ price float64 }
|
||||
s := ui.Local(c.Root(), "300", func() state { return state{price: 101.5} })
|
||||
navigationDemo(c, func() {
|
||||
if mujica.Button(c, "Tick", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
|
||||
s.price += float64(rand.Intn(200)-100) / 50
|
||||
log(fmt.Sprintf("Price %.2f", s.price))
|
||||
}
|
||||
}, func() {
|
||||
mujica.BlinkHighlight(c, s.price, mujica.BlinkHighlightOptions{})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func motionInterruptDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
running, stopping, confirm bool
|
||||
until time.Time
|
||||
}
|
||||
s := ui.Local(c.Root(), "281", func() state { return state{running: true, confirm: true} })
|
||||
if s.stopping && c.Now().After(s.until) {
|
||||
s.running, s.stopping = false, false
|
||||
log("Run stopped")
|
||||
}
|
||||
if s.stopping {
|
||||
c.After(s.until.Sub(c.Now()))
|
||||
}
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.confirm, "Confirm", log)
|
||||
if mujica.Button(c, "Start run", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.running}).Clicked() {
|
||||
s.running = true
|
||||
log("Run started")
|
||||
}
|
||||
}, func() {
|
||||
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
|
||||
if s.running && !s.stopping {
|
||||
mujica.DotsLoader(c, mujica.LoaderOptions{Label: "Agent running", ShowLabel: true})
|
||||
}
|
||||
if mujica.InterruptButton(c, s.running, mujica.InterruptButtonOptions{Confirm: s.confirm, Interrupting: s.stopping}).Changed() {
|
||||
log("Interrupt requested")
|
||||
s.stopping, s.until = true, c.Now().Add(1500*time.Millisecond)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionTokenUsageDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ line, loading, empty, failed bool }
|
||||
s := ui.Local(c.Root(), "282", func() state { return state{} })
|
||||
usage := *ui.Local(c.Root(), "282.data", func() []mujica.TokenUsage {
|
||||
return []mujica.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Search", Input: 4800, Output: 650},
|
||||
{Label: "Read", Input: 6100, Output: 420}, {Label: "Write", Input: 2100, Output: 2900}, {Label: "Review", Input: 3300, Output: 800}}
|
||||
})
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.line, "Lines", log)
|
||||
navigationCheck(c, &s.loading, "Loading", log)
|
||||
navigationCheck(c, &s.empty, "Empty", log)
|
||||
navigationCheck(c, &s.failed, "Error", log)
|
||||
}, func() {
|
||||
st := mujica.DataReady
|
||||
if s.loading {
|
||||
st = mujica.DataLoading
|
||||
}
|
||||
if s.failed {
|
||||
st = mujica.DataFailed
|
||||
}
|
||||
if s.empty {
|
||||
usage = nil
|
||||
}
|
||||
r := mujica.TokenUsageChart(c, usage, mujica.TokenUsageChartOptions{Line: s.line, Frame: mujica.ChartFrame{Status: st, Error: "Usage unavailable"}})
|
||||
if r.Changed() && r.Point >= 0 {
|
||||
log(fmt.Sprintf("Readout: mark %d", r.Point))
|
||||
}
|
||||
if name, ok := r.Toggled(); ok {
|
||||
log("Toggled " + name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func motionCostDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
table mujica.CostBreakdownState
|
||||
by int
|
||||
}
|
||||
s := ui.Local(c.Root(), "283", func() state { return state{} })
|
||||
items := *ui.Local(c.Root(), "283.data", func() []mujica.CostItem {
|
||||
return []mujica.CostItem{
|
||||
{Model: "opus", Tool: "search", Step: "plan", InputTokens: 1000, OutputTokens: 200, Cost: 0.30},
|
||||
{Model: "haiku", Tool: "search", Step: "gather", InputTokens: 4000, OutputTokens: 500, Cost: 0.05},
|
||||
{Model: "opus", Tool: "write", Step: "write", InputTokens: 2000, OutputTokens: 3000, Cost: 1.15},
|
||||
{Model: "sonnet", Tool: "read", Step: "gather", InputTokens: 6000, OutputTokens: 400, Cost: 0.24},
|
||||
}
|
||||
})
|
||||
navigationDemo(c, func() {
|
||||
if mujica.SegmentedControl(c, &s.by, []mujica.Segment{{Label: "Model"}, {Label: "Tool"}, {Label: "Step"}}, mujica.SegmentedControlOptions{Label: "Group by"}).Changed() {
|
||||
log(fmt.Sprintf("Group by %d", s.by))
|
||||
}
|
||||
}, func() {
|
||||
v := mujica.CostBreakdown(c, &s.table, items, mujica.CostBreakdownOptions{GroupBy: mujica.CostGroup(s.by)})
|
||||
v.Element.Height(320)
|
||||
if v.SortChanged() {
|
||||
log("Sorted by " + s.table.Table.Sort.Column)
|
||||
}
|
||||
if key, ok := s.table.Table.Selected(); ok && v.Changed() {
|
||||
log("Selected " + key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func motionTraceDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
viewer mujica.TraceViewerState
|
||||
empty, loading, failed, disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "284", func() state { return state{} })
|
||||
spans := *ui.Local(c.Root(), "284.data", func() []mujica.TraceSpan {
|
||||
return []mujica.TraceSpan{
|
||||
{ID: "run", Name: "agent.run", End: 900 * time.Millisecond, Attrs: []mujica.TraceAttr{{Key: "model", Value: "opus"}, {Key: "session", Value: "s-42"}}},
|
||||
{ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, Attrs: []mujica.TraceAttr{{Key: "tokens", Value: "1200"}}},
|
||||
{ID: "search", Parent: "run", Name: "tool.search", Start: 220 * time.Millisecond, End: 520 * time.Millisecond, Failed: true, Attrs: []mujica.TraceAttr{{Key: "error", Value: "timeout"}}},
|
||||
{ID: "fetch", Parent: "search", Name: "http.get", Start: 230 * time.Millisecond, End: 500 * time.Millisecond},
|
||||
{ID: "write", Parent: "run", Name: "llm.write", Start: 530 * time.Millisecond, End: 880 * time.Millisecond},
|
||||
}
|
||||
})
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.empty, "Empty", log)
|
||||
navigationCheck(c, &s.loading, "Loading", log)
|
||||
navigationCheck(c, &s.failed, "Error", log)
|
||||
navigationCheck(c, &s.disabled, "Disabled", log)
|
||||
}, func() {
|
||||
status := mujica.DataReady
|
||||
if s.empty || s.loading || s.failed {
|
||||
spans = nil
|
||||
}
|
||||
if s.loading {
|
||||
status = mujica.DataLoading
|
||||
}
|
||||
if s.failed {
|
||||
status = mujica.DataFailed
|
||||
}
|
||||
ui.Column(c).Disabled(s.disabled).Children(func() {
|
||||
v := mujica.TraceViewer(c, &s.viewer, spans, mujica.TraceViewerOptions{Status: status, Error: "Trace unavailable"})
|
||||
v.Element.Height(440)
|
||||
if id, ok := s.viewer.Tree.Selected(); ok && v.Changed() {
|
||||
log("Selected " + id)
|
||||
}
|
||||
if v.SortChanged() {
|
||||
log("Sorted by " + s.viewer.Tree.Sort.Column)
|
||||
}
|
||||
if v.Retried() {
|
||||
log("Retry trace requested")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionEvalDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
table mujica.DataTableState[string]
|
||||
empty, loading, failed, disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "285", func() state { return state{} })
|
||||
cases := *ui.Local(c.Root(), "285.data", func() []mujica.EvalCase {
|
||||
return []mujica.EvalCase{
|
||||
{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true},
|
||||
{ID: "c2", Name: "Refuses forgery", Score: 0.41},
|
||||
{ID: "c3", Name: "Summarizes letter", Score: 0.87, Passed: true},
|
||||
{ID: "c4", Name: "Keeps the schedule", Score: 0.62},
|
||||
}
|
||||
})
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.empty, "Empty", log)
|
||||
navigationCheck(c, &s.loading, "Loading", log)
|
||||
navigationCheck(c, &s.failed, "Error", log)
|
||||
navigationCheck(c, &s.disabled, "Disabled", log)
|
||||
}, func() {
|
||||
status := mujica.DataReady
|
||||
if s.empty || s.loading || s.failed {
|
||||
cases = nil
|
||||
}
|
||||
if s.loading {
|
||||
status = mujica.DataLoading
|
||||
}
|
||||
if s.failed {
|
||||
status = mujica.DataFailed
|
||||
}
|
||||
ui.Column(c).Disabled(s.disabled).Children(func() {
|
||||
r := mujica.EvalResultTable(c, &s.table, cases, mujica.EvalResultTableOptions{Status: status, Error: "Evaluation unavailable"})
|
||||
r.Element.Height(320)
|
||||
if id, ok := r.Diff(); ok {
|
||||
log("Diff " + id)
|
||||
}
|
||||
if r.Table.Changed() {
|
||||
id, _ := s.table.Selected()
|
||||
log("Selected " + id)
|
||||
}
|
||||
if r.Table.SortChanged() {
|
||||
log("Sorted by " + s.table.Sort.Column)
|
||||
}
|
||||
if r.Table.Retried() {
|
||||
log("Retry evaluation requested")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func motionToolsDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
list mujica.ListState[string]
|
||||
tools []mujica.AgentTool
|
||||
disabled, empty bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "286", func() state {
|
||||
return state{tools: []mujica.AgentTool{
|
||||
{Name: "web_search", Description: "Search the web", Enabled: true, Params: []mujica.ToolParam{
|
||||
{Name: "query", Type: "string", Required: true, Description: "What to look for"}, {Name: "limit", Type: "integer"}}},
|
||||
{Name: "read_file", Description: "Read a local file", Params: []mujica.ToolParam{{Name: "path", Type: "string", Required: true}}},
|
||||
{Name: "run_shell", Description: "Run a shell command", Params: []mujica.ToolParam{{Name: "command", Type: "string", Required: true}, {Name: "timeout", Type: "number"}}},
|
||||
}}
|
||||
})
|
||||
navigationDemo(c, func() {
|
||||
navigationCheck(c, &s.disabled, "Disabled", log)
|
||||
navigationCheck(c, &s.empty, "Empty", log)
|
||||
}, func() {
|
||||
tools := s.tools
|
||||
if s.empty {
|
||||
tools = nil
|
||||
}
|
||||
r := mujica.ToolRegistryPanel(c, &s.list, tools, mujica.ToolRegistryPanelOptions{Disabled: s.disabled})
|
||||
r.Element.Height(320)
|
||||
if r.List.Changed() {
|
||||
name, _ := s.list.Selected()
|
||||
log("Selected " + name)
|
||||
}
|
||||
if name, on, ok := r.Toggled(); ok {
|
||||
log(fmt.Sprintf("%s enabled=%v", name, on))
|
||||
for i := range s.tools {
|
||||
if s.tools[i].Name == name {
|
||||
s.tools[i].Enabled = on
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func TestMotionPagesControlsAndEvents(t *testing.T) {
|
||||
controls := []string{"Confirm", "Lines", "Tool", "Empty", "Empty", "Disabled", "Loading", "Loading", "Loading", "Loading", "Loading", "Loading", "Add 500", "Append", "Thinking", "Replay", "Reverse", "Expanded", "Show", "Tick"}
|
||||
if len(motionPages) != 20 {
|
||||
t.Fatalf("motion pages %d", len(motionPages))
|
||||
}
|
||||
for i, p := range motionPages {
|
||||
t.Run(p.ID, func(t *testing.T) {
|
||||
if p.Category != "Agent Ops & Motion" || p.Code == "" || p.Keys == "" {
|
||||
t.Fatal("incomplete page metadata")
|
||||
}
|
||||
var events []string
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
ui.Column(c).Padding(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) })
|
||||
}, 1000, 700)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
tt.SetDark(true)
|
||||
events = nil
|
||||
if err := tt.Click(controls[i]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(events) == 0 {
|
||||
t.Fatalf("%s emitted no event", controls[i])
|
||||
}
|
||||
if p.ID == "294" && !strings.Contains(strings.Join(events, " "), "Finished") {
|
||||
t.Fatalf("completion events %q", events)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMotionTypewriterRestartReplays(t *testing.T) {
|
||||
finished := 0
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
motionTypewriterDemo(c, func(s string) {
|
||||
if s == "Finished" {
|
||||
finished++
|
||||
}
|
||||
})
|
||||
}, 1000, 700)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
before := finished
|
||||
_ = tt.Click("Restart")
|
||||
_ = tt.Click("Restart")
|
||||
if finished != before+2 {
|
||||
t.Fatalf("restart completions %d, want %d", finished, before+2)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user