Merge Chat Input (as built)
This commit is contained in:
70 files changed
+2599
-228
No files matched your search
@@ -1,3 +1,66 @@
|
||||
package main
|
||||
|
||||
var chatinputPages = []page{}
|
||||
const chatinputCategory = "Chat Input"
|
||||
|
||||
var chatinputPages = []page{
|
||||
{ID: "221", Name: "PromptComposer", ZhName: "提示编辑器", Category: chatinputCategory, Demo: chatinputDemoPromptComposer,
|
||||
Keys: "Enter 发送,Shift+Enter 换行;CmdEnterSends 下使用 Command/Ctrl+Enter;组合保护期间 Enter 留给输入法。",
|
||||
Code: "var prompt string\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tr := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{Placeholder: \"Ask the herald\"})\n\tif r.Submitted() {\n\t\tfmt.Println(\"send\", prompt)\n\t\tprompt = \"\"\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "222", Name: "AttachmentChip", ZhName: "附件标签", Category: chatinputCategory, Demo: chatinputDemoAttachmentChip,
|
||||
Keys: "Tab 到移除与重试;Enter、Space 激活。",
|
||||
Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tr := mujica.AttachmentChip(c, mujica.Attachment{Name: \"guests.csv\", Size: 4096, Uploading: true, Progress: 0.4})\n\tif r.Removed() {\n\t\tfmt.Println(\"cancel the upload\")\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "223", Name: "PasteImagePreview", ZhName: "图片预览", Category: chatinputCategory, Demo: chatinputDemoPasteImagePreview,
|
||||
Keys: "Tab 到移除按钮;Enter、Space 移除。",
|
||||
Code: "pic := ui.MustParseSVG([]byte(`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"4\" height=\"3\"/>`))\nimages := []mujica.PastedImage{{ID: \"p1\", Source: pic, Name: \"screenshot\"}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif id, ok := mujica.PasteImagePreview(c, images).Removed(); ok {\n\t\tfmt.Println(\"remove\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "224", Name: "DragDropOverlay", ZhName: "拖放遮罩", Category: chatinputCategory, Demo: chatinputDemoDragDropOverlay,
|
||||
Keys: "内容中的操作按其自身键盘规则访问;文件拖入由操作系统驱动。",
|
||||
Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\timages := func(path string) bool { return strings.HasSuffix(path, \".png\") }\n\tr := mujica.DragDropOverlay(c, mujica.DragDropOverlayOptions{Accept: images}, func() {\n\t\tui.Text(c, \"Drag images into the chat\")\n\t})\n\tfor _, path := range r.Accepted() {\n\t\tfmt.Println(\"attach\", path)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "225", Name: "VoiceInputButton", ZhName: "语音输入按钮", Category: chatinputCategory, Demo: chatinputDemoVoiceInputButton,
|
||||
Keys: "Tab 到按钮;Enter、Space 开始或停止。",
|
||||
Code: "var rec mujica.VoiceInputOptions\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.VoiceInputButton(c, rec).Changed() {\n\t\trec = mujica.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()}\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "226", Name: "VoiceWaveform", ZhName: "输入波形", Category: chatinputCategory, Demo: chatinputDemoVoiceWaveform,
|
||||
Keys: "展示组件,沿用外层 Tab 顺序。",
|
||||
Code: "levels := []float32{0.2, 0.6, 0.9, 0.4, 0.1}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.VoiceWaveform(c, levels, mujica.VoiceWaveformOptions{})\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "227", Name: "SlashCommandMenu", ZhName: "斜杠命令菜单", Category: chatinputCategory, Demo: chatinputDemoSlashCommandMenu,
|
||||
Keys: "编辑器内 Up/Down 选择,Enter 选中,Tab 补全,Escape 关闭。",
|
||||
Code: "var prompt string\ncommands := []mujica.SlashCommand{{Name: \"summarize\", Description: \"Summarize the thread\"}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tcomposer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{})\n\tif cmd, ok := mujica.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok {\n\t\tfmt.Println(\"run\", cmd.Name)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "228", Name: "ContextMentionMenu", ZhName: "上下文提及菜单", Category: chatinputCategory, Demo: chatinputDemoContextMentionMenu,
|
||||
Keys: "编辑器内 Up/Down 选择,Enter 或 Tab 附加,Escape 关闭。",
|
||||
Code: "var prompt string\nvar found []mujica.ContextItem\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tcomposer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{})\n\tr := mujica.ContextMentionMenu(c, composer, &prompt, found, mujica.ContextMentionOptions{})\n\tif r.QueryChanged() {\n\t\tfound = []mujica.ContextItem{{ID: \"f1\", Label: r.Query() + \".md\", Kind: mujica.ContextFile}}\n\t}\n\tif item, ok := r.Chosen(); ok {\n\t\tfmt.Println(\"attach\", item.ID)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "229", Name: "ContextChips", ZhName: "上下文标签", Category: chatinputCategory, Demo: chatinputDemoContextChips,
|
||||
Keys: "Tab 到移除与展开;Enter、Space 激活。",
|
||||
Code: "items := []mujica.ContextItem{{ID: \"f1\", Label: \"guests.csv\", Kind: mujica.ContextFile}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif id, ok := mujica.ContextChips(c, items, mujica.ContextChipsOptions{}).Removed(); ok {\n\t\tfmt.Println(\"detach\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "230", Name: "ModelSelector", ZhName: "模型选择器", Category: chatinputCategory, Demo: chatinputDemoModelSelector,
|
||||
Keys: "Tab 到选择器;Up/Down、Home/End 选择,Enter/Space 确认,Escape 关闭。",
|
||||
Code: "model := \"herald\"\nmodels := []mujica.ModelOption{{ID: \"herald\", Name: \"Herald 2\", Capabilities: []string{\"Vision\"}, ContextTokens: 200000}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.ModelSelector(c, &model, models, mujica.ModelSelectorOptions{}).Changed() {\n\t\tfmt.Println(\"model\", model)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "231", Name: "ModeSelector", ZhName: "模式选择器", Category: chatinputCategory, Demo: chatinputDemoModeSelector,
|
||||
Keys: "Tab 进入模式组;Left/Right 切换模式。",
|
||||
Code: "mode := mujica.ModeChat\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.ModeSelector(c, &mode, mujica.ModeSelectorOptions{}).Changed() {\n\t\tfmt.Println(\"mode\", mode)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "232", Name: "ToolToggleMenu", ZhName: "工具开关菜单", Category: chatinputCategory, Demo: chatinputDemoToolToggleMenu,
|
||||
Keys: "Tab 到菜单按钮;Enter/Space 打开;Tab 到开关与授权;Escape 关闭。",
|
||||
Code: "web := true\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\ttools := []mujica.ToolItem{{ID: \"web\", Label: \"Web search\", On: &web}}\n\tif id, ok := mujica.ToolToggleMenu(c, tools).Changed(); ok {\n\t\tfmt.Println(\"toggled\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "233", Name: "ParameterPanel", ZhName: "参数面板", Category: chatinputCategory, Demo: chatinputDemoParameterPanel,
|
||||
Keys: "Tab 到滑块与重置;方向键按 Step 调整,Home/End 到范围端点;Enter/Space 重置。",
|
||||
Code: "temperature := 1.0\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tparams := []mujica.ModelParam{{Label: \"Temperature\", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}}\n\tmujica.ParameterPanel(c, params)\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "234", Name: "TokenCounter", ZhName: "Token计数", Category: chatinputCategory, Demo: chatinputDemoTokenCounter,
|
||||
Keys: "展示组件,沿用外层 Tab 顺序。",
|
||||
Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.TokenCounter(c, 1234, 8000)\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "235", Name: "ContextWindowMeter", ZhName: "上下文容量", Category: chatinputCategory, Demo: chatinputDemoContextWindowMeter,
|
||||
Keys: "展示组件,沿用外层 Tab 顺序。",
|
||||
Code: "segments := []mujica.ContextSegment{{Label: \"System\", Tokens: 1200}, {Label: \"History\", Tokens: 58000}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.ContextWindowMeter(c, segments, 128000, mujica.ContextWindowMeterOptions{})\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "236", Name: "SendButton", ZhName: "发送按钮", Category: chatinputCategory, Demo: chatinputDemoSendButton,
|
||||
Keys: "Tab 到按钮;Enter、Space 激活;快捷键提示由调用方绑定。",
|
||||
Code: "streaming := false\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tr := mujica.SendButton(c, mujica.SendButtonOptions{Streaming: streaming, Shortcut: \"Enter\"})\n\tif r.Sent() || r.Stopped() {\n\t\tstreaming = r.Sent()\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "237", Name: "CitationBadge", ZhName: "引用编号", Category: chatinputCategory, Demo: chatinputDemoCitationBadge,
|
||||
Keys: "Tab 到引用;Enter、Space 定位;悬停或键盘焦点 500 毫秒后预览;Escape 关闭预览。",
|
||||
Code: "var cites mujica.CitationState\nsrc := mujica.Source{Title: \"The Palace of Versailles\", URL: \"https://example.com/versailles\"}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.CitationBadge(c, 1, src, &cites).Changed() {\n\t\tfmt.Println(\"show source\", cites.Active)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "238", Name: "SourceCard", ZhName: "来源卡片", Category: chatinputCategory, Demo: chatinputDemoSourceCard,
|
||||
Keys: "Tab 到卡片;Enter、Space 请求打开。",
|
||||
Code: "var cites mujica.CitationState\nsrc := mujica.Source{Title: \"Hall of Mirrors\", URL: \"https://example.org/hall\", Snippet: \"Seventeen arches.\"}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.SourceCard(c, 1, src, &cites).Clicked() {\n\t\tfmt.Println(\"open\", src.URL)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "239", Name: "SourcesPanel", ZhName: "来源面板", Category: chatinputCategory, Demo: chatinputDemoSourcesPanel,
|
||||
Keys: "Tab 到可见来源;Enter、Space 请求打开;滚轮滚动;Active 变化定位来源。",
|
||||
Code: "var cites mujica.CitationState\nsources := []mujica.Source{{Title: \"Hall of Mirrors\", URL: \"https://example.org/hall\"}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif n, ok := mujica.SourcesPanel(c, sources, &cites).Opened(); ok {\n\t\tfmt.Println(\"open\", sources[n-1].URL)\n\t}\n}\nui.Render(view, 400, 300, 1)"},
|
||||
{ID: "240", Name: "SearchProgress", ZhName: "搜索进度", Category: chatinputCategory, Demo: chatinputDemoSearchProgress,
|
||||
Keys: "Tab 到折叠标题;Enter、Space 展开或收起。",
|
||||
Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tstages := []mujica.SearchStage{{Label: \"Searching the web\", Done: true}, {Label: \"Reading sources\"}}\n\tmujica.SearchProgress(c, mujica.SearchProgressOptions{Query: \"Versailles\", Read: 3, Total: 8, Stages: stages})\n}\nui.Render(view, 400, 300, 1)"},
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func chatinputDemoPromptComposer(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
text string
|
||||
disabled, cmd, composing, attached, web, streaming bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "221", func() state { return state{attached: true} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
choiceFlag(c, &s.cmd, "Cmd+Enter sends", log)
|
||||
choiceFlag(c, &s.composing, "Composition guard", log)
|
||||
choiceFlag(c, &s.attached, "Attachment", log)
|
||||
}, func() {
|
||||
ui.Column(c).Width(520).Children(func() {
|
||||
r := mujica.PromptComposer(c, &s.text, mujica.PromptComposerOptions{Label: "Prompt", Placeholder: "Ask the herald", Disabled: s.disabled, CmdEnterSends: s.cmd, Composing: s.composing,
|
||||
Attachments: func() {
|
||||
if s.attached && mujica.AttachmentChip(c, mujica.Attachment{Name: "guests.csv", Size: 4096}).Removed() {
|
||||
s.attached = false
|
||||
log("remove attachment")
|
||||
}
|
||||
},
|
||||
Tools: func() {
|
||||
if _, ok := mujica.ToolToggleMenu(c, []mujica.ToolItem{{ID: "web", Label: "Web search", On: &s.web}}).Changed(); ok {
|
||||
log("toggle web search")
|
||||
}
|
||||
},
|
||||
Actions: func() {
|
||||
r := mujica.SendButton(c, mujica.SendButtonOptions{Streaming: s.streaming, Disabled: strings.TrimSpace(s.text) == "" || s.composing, Shortcut: "Enter"})
|
||||
if r.Sent() {
|
||||
s.streaming = true
|
||||
s.text = ""
|
||||
log("send")
|
||||
}
|
||||
if r.Stopped() {
|
||||
s.streaming = false
|
||||
log("stop")
|
||||
}
|
||||
},
|
||||
})
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("edit %d characters", len(s.text)))
|
||||
}
|
||||
if r.Submitted() {
|
||||
s.text = ""
|
||||
s.streaming = true
|
||||
log("submit")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoAttachmentChip(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
upload, fail, disabled, removed bool
|
||||
progress float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "222", func() state { return state{upload: true, progress: 0.4} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.upload, "Uploading", log)
|
||||
choiceFlag(c, &s.fail, "Failed", log)
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
if mujica.Slider(c, &s.progress, mujica.SliderOptions{Min: 0, Max: 1, Step: 0.1, Label: "Upload progress"}).Changed() {
|
||||
log(fmt.Sprint("progress ", s.progress))
|
||||
}
|
||||
if mujica.Button(c, "Restore attachment", mujica.ButtonOptions{}).Clicked() {
|
||||
s.removed = false
|
||||
log("restore")
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Disabled(s.disabled).Children(func() {
|
||||
if s.removed {
|
||||
ui.Text(c, "Attachment removed")
|
||||
return
|
||||
}
|
||||
a := mujica.Attachment{Name: "guests.csv", Size: 4096, Uploading: s.upload, Progress: s.progress}
|
||||
if s.fail {
|
||||
a.Err = errors.New("upload rejected")
|
||||
}
|
||||
r := mujica.AttachmentChip(c, a)
|
||||
if r.Removed() {
|
||||
s.removed = true
|
||||
log("remove")
|
||||
}
|
||||
if r.Retried() {
|
||||
s.fail = false
|
||||
s.upload = true
|
||||
s.progress = 0
|
||||
log("retry")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputPictures() []mujica.PastedImage {
|
||||
var out []mujica.PastedImage
|
||||
for i, col := range []string{"#7D2034", "#355E80"} {
|
||||
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72"><rect width="72" height="72" fill="%s"/><circle cx="36" cy="36" r="18" fill="#F2EBDD"/></svg>`, col)
|
||||
out = append(out, mujica.PastedImage{ID: fmt.Sprint(i), Name: fmt.Sprintf("Picture %d", i+1), Source: ui.MustParseSVG([]byte(svg))})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func chatinputDemoPasteImagePreview(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
images []mujica.PastedImage
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "223", func() state { return state{images: chatinputPictures()} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
if mujica.Button(c, "Restore images", mujica.ButtonOptions{}).Clicked() {
|
||||
s.images = chatinputPictures()
|
||||
log("restore images")
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Padding(12).Disabled(s.disabled).Children(func() {
|
||||
if id, ok := mujica.PasteImagePreview(c, s.images).Removed(); ok {
|
||||
for i, im := range s.images {
|
||||
if im.ID == id {
|
||||
s.images = append(s.images[:i], s.images[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
log("remove image " + id)
|
||||
}
|
||||
})
|
||||
ui.Text(c, "Decoded images arrive from the caller's paste or drop handler.")
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoDragDropOverlay(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
imagesOnly, disabled bool
|
||||
accepted, rejected int
|
||||
}
|
||||
s := ui.Local(c.Root(), "224", func() state { return state{imagesOnly: true} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.imagesOnly, "PNG only", log)
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
}, func() {
|
||||
ui.Column(c).Width(520).Height(180).Disabled(s.disabled).Children(func() {
|
||||
r := mujica.DragDropOverlay(c, mujica.DragDropOverlayOptions{Accept: func(p string) bool { return !s.imagesOnly || strings.EqualFold(filepath.Ext(p), ".png") }}, func() {
|
||||
ui.Box(c).Fill().Border(1, mujica.Tokens(c).ControlBorder).Center().Children(func() { ui.Text(c, "Drop files here") })
|
||||
})
|
||||
r.Element.Fill()
|
||||
if n := len(r.Accepted()); n > 0 {
|
||||
s.accepted += n
|
||||
log(fmt.Sprintf("accept %d files", n))
|
||||
}
|
||||
if n := len(r.Rejected()); n > 0 {
|
||||
s.rejected += n
|
||||
log(fmt.Sprintf("reject %d files", n))
|
||||
}
|
||||
})
|
||||
ui.Text(c, fmt.Sprintf("Accepted %d · Rejected %d", s.accepted, s.rejected))
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoVoiceInputButton(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "225", func() mujica.VoiceInputOptions { return mujica.VoiceInputOptions{} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.Disabled, "Disabled", log)
|
||||
if mujica.Button(c, "Permission failure", mujica.ButtonOptions{}).Clicked() {
|
||||
s.Recording = false
|
||||
s.Denied = true
|
||||
log("permission denied")
|
||||
}
|
||||
}, func() {
|
||||
if mujica.VoiceInputButton(c, *s).Changed() {
|
||||
s.Recording = !s.Recording
|
||||
s.Denied = false
|
||||
s.Started = c.Now()
|
||||
log(fmt.Sprintf("recording %v", s.Recording))
|
||||
}
|
||||
ui.Text(c, "The caller records audio; this demo changes recording state.")
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoVoiceWaveform(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
silent bool
|
||||
level float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "226", func() state { return state{level: 0.6} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.silent, "Silent", log)
|
||||
if mujica.Slider(c, &s.level, mujica.SliderOptions{Min: 0, Max: 1, Step: 0.1, Label: "Input level"}).Changed() {
|
||||
log(fmt.Sprint("level ", s.level))
|
||||
}
|
||||
}, func() {
|
||||
levels := make([]float32, 40)
|
||||
for i := range levels {
|
||||
if !s.silent {
|
||||
levels[i] = float32(s.level) * float32(i%7+1) / 7
|
||||
}
|
||||
}
|
||||
mujica.VoiceWaveform(c, levels, mujica.VoiceWaveformOptions{})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func chatinputDemoSlashCommandMenu(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
text string
|
||||
disabled, empty bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "227", func() state { return state{text: "/"} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
choiceFlag(c, &s.empty, "Empty commands", log)
|
||||
}, func() {
|
||||
ui.Column(c).Width(520).Children(func() {
|
||||
p := mujica.PromptComposer(c, &s.text, mujica.PromptComposerOptions{Label: "Command prompt", Disabled: s.disabled})
|
||||
commands := []mujica.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}, {Name: "search", Description: "Search the web"}, {Name: "translate", Description: "Translate the message"}}
|
||||
if s.empty {
|
||||
commands = nil
|
||||
}
|
||||
before := s.text
|
||||
if cmd, ok := mujica.SlashCommandMenu(c, p, &s.text, commands).Chosen(); ok {
|
||||
log("command " + cmd.Name)
|
||||
} else if before != s.text {
|
||||
log("complete command")
|
||||
}
|
||||
if p.Changed() {
|
||||
log("edit command query")
|
||||
}
|
||||
if p.Submitted() {
|
||||
log("submit prompt")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputContextItems() []mujica.ContextItem {
|
||||
return []mujica.ContextItem{{ID: "file", Label: "guests.csv", Kind: mujica.ContextFile}, {ID: "doc", Label: "Court protocol", Kind: mujica.ContextDoc}, {ID: "web", Label: "Versailles", Detail: "example.com", Kind: mujica.ContextWeb}}
|
||||
}
|
||||
|
||||
func chatinputDemoContextMentionMenu(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
text, query string
|
||||
items []mujica.ContextItem
|
||||
loading, disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "228", func() state { return state{text: "@", items: chatinputContextItems()} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.loading, "Loading results", log)
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
}, func() {
|
||||
ui.Column(c).Width(520).Children(func() {
|
||||
p := mujica.PromptComposer(c, &s.text, mujica.PromptComposerOptions{Label: "Context prompt", Disabled: s.disabled})
|
||||
items := s.items
|
||||
if s.loading {
|
||||
items = nil
|
||||
}
|
||||
r := mujica.ContextMentionMenu(c, p, &s.text, items, mujica.ContextMentionOptions{Loading: s.loading})
|
||||
if r.QueryChanged() {
|
||||
s.query = r.Query()
|
||||
s.items = nil
|
||||
for _, it := range chatinputContextItems() {
|
||||
if strings.Contains(strings.ToLower(it.Label), strings.ToLower(s.query)) {
|
||||
s.items = append(s.items, it)
|
||||
}
|
||||
}
|
||||
c.Invalidate()
|
||||
log("context query changed")
|
||||
}
|
||||
if it, ok := r.Chosen(); ok {
|
||||
log("attach context " + it.ID)
|
||||
}
|
||||
if p.Changed() {
|
||||
log("edit context prompt")
|
||||
}
|
||||
if p.Submitted() {
|
||||
log("submit prompt")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoContextChips(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
items []mujica.ContextItem
|
||||
max float64
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "229", func() state { return state{items: chatinputContextItems(), max: 2} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible chips"}).Changed() {
|
||||
log(fmt.Sprint("max ", s.max))
|
||||
}
|
||||
if mujica.Button(c, "Restore context", mujica.ButtonOptions{}).Clicked() {
|
||||
s.items = chatinputContextItems()
|
||||
log("restore context")
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Width(520).Disabled(s.disabled).Children(func() {
|
||||
if id, ok := mujica.ContextChips(c, s.items, mujica.ContextChipsOptions{Max: int(s.max)}).Removed(); ok {
|
||||
for i, it := range s.items {
|
||||
if it.ID == id {
|
||||
s.items = append(s.items[:i], s.items[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
log("remove context " + id)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoModelSelector(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
model string
|
||||
disabled, unavailable bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "230", func() state { return state{model: "herald", unavailable: true} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
choiceFlag(c, &s.unavailable, "Restricted model", log)
|
||||
}, func() {
|
||||
models := []mujica.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision", "Tools"}, ContextTokens: 200000}, {ID: "page", Name: "Page Mini", Capabilities: []string{"Fast"}, ContextTokens: 32000}, {ID: "oracle", Name: "Oracle", ContextTokens: 1000000}}
|
||||
if s.unavailable {
|
||||
models[2].Unavailable = "Needs the Court plan"
|
||||
}
|
||||
if mujica.ModelSelector(c, &s.model, models, mujica.ModelSelectorOptions{Disabled: s.disabled}).Changed() {
|
||||
log("model " + s.model)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoModeSelector(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
mode mujica.ChatMode
|
||||
icons, disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "231", func() state { return state{} })
|
||||
choiceDemo(c, func() { choiceFlag(c, &s.icons, "Icons only", log); choiceFlag(c, &s.disabled, "Disabled", log) }, func() {
|
||||
if mujica.ModeSelector(c, &s.mode, mujica.ModeSelectorOptions{IconsOnly: s.icons, Disabled: s.disabled}).Changed() {
|
||||
log(fmt.Sprint("mode ", s.mode))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoToolToggleMenu(c *ui.Context, log func(string)) {
|
||||
type state struct{ web, code, mail, authorized, disabled bool }
|
||||
s := ui.Local(c.Root(), "232", func() state { return state{web: true} })
|
||||
choiceDemo(c, func() { choiceFlag(c, &s.authorized, "Authorized", log); choiceFlag(c, &s.disabled, "Disabled", log) }, func() {
|
||||
ui.Column(c).Disabled(s.disabled).Children(func() {
|
||||
r := mujica.ToolToggleMenu(c, []mujica.ToolItem{{ID: "web", Label: "Web search", On: &s.web}, {ID: "code", Label: "Code execution", On: &s.code}, {ID: "mail", Label: "Mail", On: &s.mail, NeedsAuth: true, Authorized: s.authorized}})
|
||||
if id, ok := r.Changed(); ok {
|
||||
log("toggle " + id)
|
||||
}
|
||||
if id, ok := r.Authorize(); ok {
|
||||
s.authorized = true
|
||||
log("authorize " + id)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoParameterPanel(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
temp, top, length float64
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "233", func() state { return state{temp: 0.7, top: 1, length: 2048} })
|
||||
choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) }, func() {
|
||||
ui.Column(c).Width(420).Disabled(s.disabled).Children(func() {
|
||||
if mujica.ParameterPanel(c, []mujica.ModelParam{{Label: "Temperature", Value: &s.temp, Min: 0, Max: 2, Step: 0.1, Default: 1}, {Label: "Top-p", Value: &s.top, Min: 0, Max: 1, Step: 0.05, Default: 1}, {Label: "Max tokens", Value: &s.length, Min: 256, Max: 8192, Step: 256, Default: 2048}}).Changed() {
|
||||
log(fmt.Sprintf("parameters %.1f / %.2f / %.0f", s.temp, s.top, s.length))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
"time"
|
||||
)
|
||||
|
||||
func chatinputDemoTokenCounter(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "234", func() float64 { return 1234 })
|
||||
choiceDemo(c, func() {
|
||||
if mujica.NumberInput(c, s, mujica.NumberInputOptions{Min: 0, Max: 12000, Step: 100, Label: "Estimated tokens"}).Changed() {
|
||||
log(fmt.Sprint("estimate ", *s))
|
||||
}
|
||||
}, func() {
|
||||
mujica.TokenCounter(c, int(*s), 8000)
|
||||
mujica.TokenCounter(c, 7500, 8000)
|
||||
mujica.TokenCounter(c, 9100, 8000)
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoContextWindowMeter(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "235", func() float64 { return 58000 })
|
||||
choiceDemo(c, func() {
|
||||
if mujica.NumberInput(c, s, mujica.NumberInputOptions{Min: 0, Max: 150000, Step: 1000, Label: "History tokens"}).Changed() {
|
||||
log(fmt.Sprint("history ", *s))
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Width(520).Children(func() {
|
||||
mujica.ContextWindowMeter(c, []mujica.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: int(*s)}, {Label: "Attachments", Tokens: 24000}}, 128000, mujica.ContextWindowMeterOptions{})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoSendButton(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "236", func() mujica.SendButtonOptions { return mujica.SendButtonOptions{Shortcut: "Enter"} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.Streaming, "Streaming", log)
|
||||
choiceFlag(c, &s.Disabled, "Disabled send", log)
|
||||
}, func() {
|
||||
r := mujica.SendButton(c, *s)
|
||||
if r.Sent() {
|
||||
s.Streaming = true
|
||||
log("send")
|
||||
}
|
||||
if r.Stopped() {
|
||||
s.Streaming = false
|
||||
log("stop")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputSources() []mujica.Source {
|
||||
return []mujica.Source{{Title: "The Palace of Versailles", URL: "https://example.com/versailles", Site: "example.com", Snippet: "Seat of the court from 1682.", Published: time.Date(2025, 5, 4, 0, 0, 0, 0, time.UTC)}, {Title: "Hall of Mirrors", URL: "https://example.org/hall", Site: "example.org", Snippet: "Seventeen arches face the garden."}, {Title: "Court etiquette", URL: "https://example.net/etiquette", Snippet: "The ranks of the court."}}
|
||||
}
|
||||
|
||||
func chatinputDemoCitationBadge(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
cites mujica.CitationState
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "237", func() state { return state{} })
|
||||
choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) }, func() {
|
||||
ui.Row(c).Disabled(s.disabled).Gap(8).AlignItems(ui.Center).Children(func() {
|
||||
ui.Text(c, "The court moved in 1682")
|
||||
for i, src := range chatinputSources() {
|
||||
if mujica.CitationBadge(c, i+1, src, &s.cites).Changed() {
|
||||
log(fmt.Sprint("locate source ", s.cites.Active))
|
||||
}
|
||||
}
|
||||
})
|
||||
ui.Column(c).Width(360).Height(260).Disabled(s.disabled).Children(func() {
|
||||
if n, ok := mujica.SourcesPanel(c, chatinputSources(), &s.cites).Opened(); ok {
|
||||
log(fmt.Sprint("open source ", n))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoSourceCard(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
cites mujica.CitationState
|
||||
selected, disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "238", func() state { return state{} })
|
||||
choiceDemo(c, func() { choiceFlag(c, &s.selected, "Selected", log); choiceFlag(c, &s.disabled, "Disabled", log) }, func() {
|
||||
s.cites.Active = 0
|
||||
if s.selected {
|
||||
s.cites.Active = 1
|
||||
}
|
||||
ui.Column(c).Width(400).Disabled(s.disabled).Children(func() {
|
||||
if mujica.SourceCard(c, 1, chatinputSources()[0], &s.cites).Clicked() {
|
||||
log("open source 1")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoSourcesPanel(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
cites mujica.CitationState
|
||||
sources []mujica.Source
|
||||
disabled, empty, many bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "239", func() state { return state{sources: chatinputSources()} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
choiceFlag(c, &s.empty, "Empty", log)
|
||||
if mujica.Switch(c, &s.many, "100,000 sources", mujica.SwitchOptions{}).Changed() {
|
||||
s.sources = chatinputSources()
|
||||
s.cites.Active = 0
|
||||
if s.many {
|
||||
s.sources = make([]mujica.Source, 100000)
|
||||
for i := range s.sources {
|
||||
s.sources[i] = mujica.Source{Title: fmt.Sprintf("Archive %d", i+1), URL: "https://example.com/archive"}
|
||||
}
|
||||
}
|
||||
log(fmt.Sprint("sources ", len(s.sources)))
|
||||
}
|
||||
if mujica.Button(c, "Locate last source", mujica.ButtonOptions{Disabled: s.empty}).Clicked() {
|
||||
s.cites.Active = len(s.sources)
|
||||
log("locate last source")
|
||||
}
|
||||
}, func() {
|
||||
sources := s.sources
|
||||
if s.empty {
|
||||
sources = nil
|
||||
s.cites.Active = 0
|
||||
}
|
||||
ui.Column(c).Width(400).Height(320).Disabled(s.disabled).Children(func() {
|
||||
if n, ok := mujica.SourcesPanel(c, sources, &s.cites).Opened(); ok {
|
||||
log(fmt.Sprint("open source ", n))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func chatinputDemoSearchProgress(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
done, disabled bool
|
||||
read float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "240", func() state { return state{read: 3} })
|
||||
choiceDemo(c, func() {
|
||||
choiceFlag(c, &s.done, "Done", log)
|
||||
choiceFlag(c, &s.disabled, "Disabled", log)
|
||||
if mujica.NumberInput(c, &s.read, mujica.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Read sources"}).Changed() {
|
||||
log(fmt.Sprint("read ", s.read))
|
||||
}
|
||||
}, func() {
|
||||
ui.Column(c).Width(440).Disabled(s.disabled).Children(func() {
|
||||
if mujica.SearchProgress(c, mujica.SearchProgressOptions{Query: "Versailles 1682", Read: int(s.read), Total: 8, Done: s.done, Stages: []mujica.SearchStage{{Label: "Planning queries", Done: true}, {Label: "Searching the web", Done: true}, {Label: "Reading sources", Done: s.done}}}).Changed() {
|
||||
log("toggle search details")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func chatinputDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) {
|
||||
t.Helper()
|
||||
var p *page
|
||||
for i := range chatinputPages {
|
||||
if chatinputPages[i].ID == id {
|
||||
p = &chatinputPages[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if p == nil {
|
||||
t.Fatalf("missing page %s", id)
|
||||
}
|
||||
events := new([]string)
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{Mode: mode})
|
||||
ui.Scroll(c).Fill().Padding(24).Gap(16).Children(func() { p.Demo(c, func(s string) { *events = append(*events, s) }) })
|
||||
}, 900, 800)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
return tt, events
|
||||
}
|
||||
|
||||
func TestChatinputDemosRender(t *testing.T) {
|
||||
if len(chatinputPages) != 20 {
|
||||
t.Fatalf("pages %d, want 20", len(chatinputPages))
|
||||
}
|
||||
for _, p := range chatinputPages {
|
||||
if p.Category != "Chat Input" || p.Code == "" || p.Keys == "" {
|
||||
t.Errorf("incomplete page %s", p.ID)
|
||||
}
|
||||
for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} {
|
||||
tt, _ := chatinputDemoTester(t, p.ID, mode)
|
||||
if tt.HasText("panic") {
|
||||
t.Errorf("%s: panic shown", p.ID)
|
||||
}
|
||||
if dir := os.Getenv("CHATINPUT_DEMO_DIR"); dir != "" {
|
||||
f, err := os.Create(filepath.Join(dir, p.ID+"-"+mode.String()+".png"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := png.Encode(f, tt.Image()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatinputDemosEvents(t *testing.T) {
|
||||
for _, tc := range []struct{ id, click string }{
|
||||
{"221", "Prompt"}, {"222", "Remove guests.csv"}, {"223", "Remove image Picture 1"}, {"225", "Start voice input"},
|
||||
{"227", "Command prompt"}, {"228", "Context prompt"}, {"229", "Remove guests.csv"}, {"230", "Model"},
|
||||
{"231", "Deep research"}, {"232", "Tools, 1 on"}, {"233", "Reset Temperature"}, {"236", "Send"},
|
||||
{"237", "Source 1: The Palace of Versailles"}, {"238", "Open The Palace of Versailles"},
|
||||
{"239", "Locate last source"}, {"240", "Searching for “Versailles 1682”"},
|
||||
} {
|
||||
tt, events := chatinputDemoTester(t, tc.id, mujica.Light)
|
||||
if err := tt.Click(tc.click); err != nil {
|
||||
t.Fatalf("%s: %v", tc.id, err)
|
||||
}
|
||||
switch tc.id {
|
||||
case "221":
|
||||
tt.Type("hello")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
case "227":
|
||||
tt.Type("s")
|
||||
tt.Key(0, ui.KeyTab)
|
||||
case "228":
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
case "230":
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
case "232":
|
||||
if err := tt.Click("Code execution"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(*events) == 0 {
|
||||
t.Errorf("%s: action logged no event", tc.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user