From 9677fad17c39c2ed27a2be51ac92ac1c0ac813b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:25:10 -0400 Subject: [PATCH] Chat Messages: components 201-220 --- audio_message.go | 112 +++++++ audio_message_test.go | 92 ++++++ branch_navigator.go | 52 +++ branch_navigator_test.go | 64 ++++ chat_container.go | 48 +++ chat_container_test.go | 64 ++++ code_block.go | 158 +++++++++ code_block_test.go | 79 +++++ docs/components/201-chat-container.md | 43 +++ docs/components/202-message-list.md | 42 +++ docs/components/203-message-bubble.md | 41 +++ docs/components/204-message-header.md | 37 +++ docs/components/205-streaming-text.md | 38 +++ docs/components/206-markdown-view.md | 39 +++ docs/components/207-code-block.md | 39 +++ docs/components/208-table-block.md | 39 +++ docs/components/209-image-grid.md | 40 +++ docs/components/210-file-message.md | 40 +++ docs/components/211-audio-message.md | 41 +++ docs/components/212-link-preview-card.md | 42 +++ docs/components/213-quote-reply.md | 38 +++ docs/components/214-message-actions.md | 38 +++ docs/components/215-message-editor.md | 38 +++ docs/components/216-branch-navigator.md | 38 +++ docs/components/217-error-message.md | 40 +++ docs/components/218-thinking-block.md | 40 +++ .../components/219-scroll-to-bottom-button.md | 40 +++ docs/components/220-typing-indicator.md | 35 ++ error_message.go | 72 +++++ error_message_test.go | 69 ++++ example_chatmsg_test.go | 222 +++++++++++++ examples/gallery/pages_chatmsg.go | 260 ++++++++++++++- examples/gallery/pages_chatmsg_actions.go | 292 +++++++++++++++++ examples/gallery/pages_chatmsg_blocks.go | 198 ++++++++++++ examples/gallery/pages_chatmsg_test.go | 72 +++++ file_message.go | 123 +++++++ file_message_test.go | 82 +++++ icons/svg/brain.svg | 21 ++ icons/svg/download.svg | 16 + icons/svg/file-archive.svg | 19 ++ icons/svg/file-code.svg | 17 + icons/svg/pause.svg | 15 + icons/svg/pencil.svg | 15 + icons/svg/play.svg | 14 + icons/svg/share-2.svg | 18 ++ icons/svg/thumbs-down.svg | 15 + icons/svg/thumbs-up.svg | 15 + icons/svg/volume-2.svg | 16 + icons/svg/wrap-text.svg | 17 + image_grid.go | 103 ++++++ image_grid_test.go | 95 ++++++ internal/markdown.go | 305 ++++++++++++++++++ internal/markdown_inline.go | 128 ++++++++ internal/markdown_test.go | 108 +++++++ link_preview_card.go | 49 +++ link_preview_card_test.go | 62 ++++ markdown_view.go | 126 ++++++++ markdown_view_test.go | 64 ++++ message_actions.go | 111 +++++++ message_actions_test.go | 93 ++++++ message_bubble.go | 115 +++++++ message_bubble_test.go | 60 ++++ message_editor.go | 57 ++++ message_editor_test.go | 62 ++++ message_header.go | 70 ++++ message_header_test.go | 55 ++++ message_list.go | 108 +++++++ message_list_test.go | 127 ++++++++ messages_chatmsg.go | 57 ++++ quote_reply.go | 33 ++ quote_reply_test.go | 53 +++ scroll_to_bottom_button.go | 47 +++ scroll_to_bottom_button_test.go | 51 +++ streaming_text.go | 77 +++++ streaming_text_test.go | 80 +++++ table_block.go | 125 +++++++ table_block_test.go | 58 ++++ .../snapshots/darwin/audio_message-dark.png | Bin 0 -> 2809 bytes .../snapshots/darwin/audio_message-light.png | Bin 0 -> 2802 bytes .../darwin/branch_navigator-dark.png | Bin 0 -> 1179 bytes .../darwin/branch_navigator-light.png | Bin 0 -> 1124 bytes .../snapshots/darwin/chat_container-dark.png | Bin 0 -> 11397 bytes .../snapshots/darwin/chat_container-light.png | Bin 0 -> 10862 bytes testdata/snapshots/darwin/code_block-dark.png | Bin 0 -> 11803 bytes .../snapshots/darwin/code_block-light.png | Bin 0 -> 11234 bytes .../snapshots/darwin/error_message-dark.png | Bin 0 -> 13200 bytes .../snapshots/darwin/error_message-light.png | Bin 0 -> 12731 bytes .../snapshots/darwin/file_message-dark.png | Bin 0 -> 8517 bytes .../snapshots/darwin/file_message-light.png | Bin 0 -> 8395 bytes testdata/snapshots/darwin/image_grid-dark.png | Bin 0 -> 3862 bytes .../snapshots/darwin/image_grid-light.png | Bin 0 -> 3925 bytes .../darwin/link_preview_card-dark.png | Bin 0 -> 10676 bytes .../darwin/link_preview_card-light.png | Bin 0 -> 10454 bytes .../snapshots/darwin/markdown_view-dark.png | Bin 0 -> 29310 bytes .../snapshots/darwin/markdown_view-light.png | Bin 0 -> 28190 bytes .../snapshots/darwin/message_actions-dark.png | Bin 0 -> 2824 bytes .../darwin/message_actions-light.png | Bin 0 -> 2783 bytes .../snapshots/darwin/message_bubble-dark.png | Bin 0 -> 17251 bytes .../snapshots/darwin/message_bubble-light.png | Bin 0 -> 16414 bytes .../snapshots/darwin/message_editor-dark.png | Bin 0 -> 8615 bytes .../snapshots/darwin/message_editor-light.png | Bin 0 -> 8260 bytes .../snapshots/darwin/message_header-dark.png | Bin 0 -> 7421 bytes .../snapshots/darwin/message_header-light.png | Bin 0 -> 7437 bytes .../snapshots/darwin/message_list-dark.png | Bin 0 -> 12244 bytes .../snapshots/darwin/message_list-light.png | Bin 0 -> 11756 bytes .../snapshots/darwin/quote_reply-dark.png | Bin 0 -> 5002 bytes .../snapshots/darwin/quote_reply-light.png | Bin 0 -> 4881 bytes .../darwin/scroll_to_bottom_button-dark.png | Bin 0 -> 2053 bytes .../darwin/scroll_to_bottom_button-light.png | Bin 0 -> 2677 bytes .../snapshots/darwin/streaming_text-dark.png | Bin 0 -> 4383 bytes .../snapshots/darwin/streaming_text-light.png | Bin 0 -> 4207 bytes .../snapshots/darwin/table_block-dark.png | Bin 0 -> 11282 bytes .../snapshots/darwin/table_block-light.png | Bin 0 -> 10722 bytes .../snapshots/darwin/thinking_block-dark.png | Bin 0 -> 9981 bytes .../snapshots/darwin/thinking_block-light.png | Bin 0 -> 9494 bytes .../darwin/typing_indicator-dark.png | Bin 0 -> 2445 bytes .../darwin/typing_indicator-light.png | Bin 0 -> 2381 bytes thinking_block.go | 50 +++ thinking_block_test.go | 71 ++++ typing_indicator.go | 44 +++ typing_indicator_test.go | 49 +++ 121 files changed, 5737 insertions(+), 1 deletion(-) create mode 100644 audio_message.go create mode 100644 audio_message_test.go create mode 100644 branch_navigator.go create mode 100644 branch_navigator_test.go create mode 100644 chat_container.go create mode 100644 chat_container_test.go create mode 100644 code_block.go create mode 100644 code_block_test.go create mode 100644 docs/components/201-chat-container.md create mode 100644 docs/components/202-message-list.md create mode 100644 docs/components/203-message-bubble.md create mode 100644 docs/components/204-message-header.md create mode 100644 docs/components/205-streaming-text.md create mode 100644 docs/components/206-markdown-view.md create mode 100644 docs/components/207-code-block.md create mode 100644 docs/components/208-table-block.md create mode 100644 docs/components/209-image-grid.md create mode 100644 docs/components/210-file-message.md create mode 100644 docs/components/211-audio-message.md create mode 100644 docs/components/212-link-preview-card.md create mode 100644 docs/components/213-quote-reply.md create mode 100644 docs/components/214-message-actions.md create mode 100644 docs/components/215-message-editor.md create mode 100644 docs/components/216-branch-navigator.md create mode 100644 docs/components/217-error-message.md create mode 100644 docs/components/218-thinking-block.md create mode 100644 docs/components/219-scroll-to-bottom-button.md create mode 100644 docs/components/220-typing-indicator.md create mode 100644 error_message.go create mode 100644 error_message_test.go create mode 100644 example_chatmsg_test.go create mode 100644 examples/gallery/pages_chatmsg_actions.go create mode 100644 examples/gallery/pages_chatmsg_blocks.go create mode 100644 examples/gallery/pages_chatmsg_test.go create mode 100644 file_message.go create mode 100644 file_message_test.go create mode 100644 icons/svg/brain.svg create mode 100644 icons/svg/download.svg create mode 100644 icons/svg/file-archive.svg create mode 100644 icons/svg/file-code.svg create mode 100644 icons/svg/pause.svg create mode 100644 icons/svg/pencil.svg create mode 100644 icons/svg/play.svg create mode 100644 icons/svg/share-2.svg create mode 100644 icons/svg/thumbs-down.svg create mode 100644 icons/svg/thumbs-up.svg create mode 100644 icons/svg/volume-2.svg create mode 100644 icons/svg/wrap-text.svg create mode 100644 image_grid.go create mode 100644 image_grid_test.go create mode 100644 internal/markdown.go create mode 100644 internal/markdown_inline.go create mode 100644 internal/markdown_test.go create mode 100644 link_preview_card.go create mode 100644 link_preview_card_test.go create mode 100644 markdown_view.go create mode 100644 markdown_view_test.go create mode 100644 message_actions.go create mode 100644 message_actions_test.go create mode 100644 message_bubble.go create mode 100644 message_bubble_test.go create mode 100644 message_editor.go create mode 100644 message_editor_test.go create mode 100644 message_header.go create mode 100644 message_header_test.go create mode 100644 message_list.go create mode 100644 message_list_test.go create mode 100644 messages_chatmsg.go create mode 100644 quote_reply.go create mode 100644 quote_reply_test.go create mode 100644 scroll_to_bottom_button.go create mode 100644 scroll_to_bottom_button_test.go create mode 100644 streaming_text.go create mode 100644 streaming_text_test.go create mode 100644 table_block.go create mode 100644 table_block_test.go create mode 100644 testdata/snapshots/darwin/audio_message-dark.png create mode 100644 testdata/snapshots/darwin/audio_message-light.png create mode 100644 testdata/snapshots/darwin/branch_navigator-dark.png create mode 100644 testdata/snapshots/darwin/branch_navigator-light.png create mode 100644 testdata/snapshots/darwin/chat_container-dark.png create mode 100644 testdata/snapshots/darwin/chat_container-light.png create mode 100644 testdata/snapshots/darwin/code_block-dark.png create mode 100644 testdata/snapshots/darwin/code_block-light.png create mode 100644 testdata/snapshots/darwin/error_message-dark.png create mode 100644 testdata/snapshots/darwin/error_message-light.png create mode 100644 testdata/snapshots/darwin/file_message-dark.png create mode 100644 testdata/snapshots/darwin/file_message-light.png create mode 100644 testdata/snapshots/darwin/image_grid-dark.png create mode 100644 testdata/snapshots/darwin/image_grid-light.png create mode 100644 testdata/snapshots/darwin/link_preview_card-dark.png create mode 100644 testdata/snapshots/darwin/link_preview_card-light.png create mode 100644 testdata/snapshots/darwin/markdown_view-dark.png create mode 100644 testdata/snapshots/darwin/markdown_view-light.png create mode 100644 testdata/snapshots/darwin/message_actions-dark.png create mode 100644 testdata/snapshots/darwin/message_actions-light.png create mode 100644 testdata/snapshots/darwin/message_bubble-dark.png create mode 100644 testdata/snapshots/darwin/message_bubble-light.png create mode 100644 testdata/snapshots/darwin/message_editor-dark.png create mode 100644 testdata/snapshots/darwin/message_editor-light.png create mode 100644 testdata/snapshots/darwin/message_header-dark.png create mode 100644 testdata/snapshots/darwin/message_header-light.png create mode 100644 testdata/snapshots/darwin/message_list-dark.png create mode 100644 testdata/snapshots/darwin/message_list-light.png create mode 100644 testdata/snapshots/darwin/quote_reply-dark.png create mode 100644 testdata/snapshots/darwin/quote_reply-light.png create mode 100644 testdata/snapshots/darwin/scroll_to_bottom_button-dark.png create mode 100644 testdata/snapshots/darwin/scroll_to_bottom_button-light.png create mode 100644 testdata/snapshots/darwin/streaming_text-dark.png create mode 100644 testdata/snapshots/darwin/streaming_text-light.png create mode 100644 testdata/snapshots/darwin/table_block-dark.png create mode 100644 testdata/snapshots/darwin/table_block-light.png create mode 100644 testdata/snapshots/darwin/thinking_block-dark.png create mode 100644 testdata/snapshots/darwin/thinking_block-light.png create mode 100644 testdata/snapshots/darwin/typing_indicator-dark.png create mode 100644 testdata/snapshots/darwin/typing_indicator-light.png create mode 100644 thinking_block.go create mode 100644 thinking_block_test.go create mode 100644 typing_indicator.go create mode 100644 typing_indicator_test.go diff --git a/audio_message.go b/audio_message.go new file mode 100644 index 0000000..a97bb08 --- /dev/null +++ b/audio_message.go @@ -0,0 +1,112 @@ +package mujica + +import ( + "fmt" + "log/slog" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// AudioMessageOptions describe a voice message the caller plays. +type AudioMessageOptions struct { + // Duration is the length; required. + Duration time.Duration + // Position is how far playback has gone, 0 to Duration. + Position time.Duration + Playing bool + // Waveform holds loudness samples from 0 to 1; empty draws a flat line. + Waveform []float32 +} + +// AudioMessageResult is an AudioMessage's element and the user's requests this frame. +type AudioMessageResult struct { + Element *ui.Element + toggled bool + seek time.Duration + seeked bool +} + +// Toggled reports a press of play or pause. +func (r AudioMessageResult) Toggled() bool { return r.toggled } + +// Seek returns the position the user moved to by click, drag or keys. +func (r AudioMessageResult) Seek() (time.Duration, bool) { return r.seek, r.seeked } + +// audioBarWidth and audioBarGap space the waveform's bars. +const audioBarWidth, audioBarGap float32 = 2, 2 + +// AudioMessage shows a voice bar: play button, waveform with progress, and times. +func AudioMessage(c *ui.Context, opts AudioMessageOptions) AudioMessageResult { + if opts.Duration <= 0 || opts.Position < 0 || opts.Position > opts.Duration { + panic("mujica: AudioMessage needs 0 <= Position <= Duration and Duration > 0") + } + for _, v := range opts.Waveform { + if !(v >= 0 && v <= 1) { + panic("mujica: AudioMessage Waveform samples must be from 0 to 1") + } + } + k := Tokens(c) + u := Density(c).Unit() + var r AudioMessageResult + r.Element = ui.Row(c).Gap(u*2).AlignItems(ui.Center).Padding(u, u*3, u, u).Width(300). + Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border) + disabled := r.Element.IsDisabled() + r.Element.Children(func() { + icon, name := "play", msg(c, "audio_message.play") + if opts.Playing { + icon, name = "pause", msg(c, "audio_message.pause") + } + if iconAction(c, icons.Must(icon), name).Clicked() && !disabled { + r.toggled = true + slog.Debug("mujica: audio_message toggle", "playing", !opts.Playing) + } + pos := opts.Position.Seconds() + s := ui.SliderBase(c, &pos, 0, opts.Duration.Seconds()).Grow(1).Height(theme.MinHitSize). + Label(msg(c, "audio_message.seek")).Description(audioClock(opts.Position) + " / " + audioClock(opts.Duration)) + internal.Ring(s, k.Focus, theme.ControlRadius) + if s.Changed() && !disabled { + r.seek, r.seeked = time.Duration(pos*float64(time.Second)), true + slog.Debug("mujica: audio_message seek", "at", r.seek) + } + frac := float32(opts.Position.Seconds() / opts.Duration.Seconds()) + s.Draw(func(p *ui.Painter, b ui.Rect) { audioWave(p, b, opts.Waveform, frac, k) }) + ui.Text(c, audioClock(opts.Position)+" / "+audioClock(opts.Duration)).FontFeatures("tnum"). + FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Shrink(0) + }) + return r +} + +// audioWave draws bars, played ones in AccentText, with a position line so progress is not color alone. +func audioWave(p *ui.Painter, b ui.Rect, wave []float32, frac float32, k theme.Tokens) { + n := int((b.W + audioBarGap) / (audioBarWidth + audioBarGap)) + if n <= 0 { + return + } + cy := b.Y + b.H/2 + for i := range n { + v := float32(0.12) + if len(wave) > 0 { + v = max(v, wave[i*len(wave)/n]) + } + h := max(2, v*(b.H-8)) + x := b.X + float32(i)*(audioBarWidth+audioBarGap) + col := k.TextMuted.Alpha(0.55) + if (x-b.X)/b.W < frac { + col = k.AccentText + } + p.Fill(ui.Rect{X: x, Y: cy - h/2, W: audioBarWidth, H: h}, col, 1) + } + at := internal.Snap(p, b.X+b.W*frac) + p.Fill(ui.Rect{X: at - 1, Y: b.Y + 2, W: 2, H: b.H - 4}, k.Text, 1) +} + +// audioClock formats d as m:ss. +func audioClock(d time.Duration) string { + s := int(d.Round(time.Second).Seconds()) + return fmt.Sprintf("%d:%02d", s/60, s%60) +} diff --git a/audio_message_test.go b/audio_message_test.go new file mode 100644 index 0000000..682e821 --- /dev/null +++ b/audio_message_test.go @@ -0,0 +1,92 @@ +package mujica + +import ( + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +var audioMessageWave = []float32{0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8, 0.6, 0.2, 0.5} + +func TestAudioMessagePlaySeekAndKeys(t *testing.T) { + playing, pos := false, 10*time.Second + var seeks []time.Duration + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := AudioMessage(c, AudioMessageOptions{Duration: time.Minute, Position: pos, Playing: playing, Waveform: audioMessageWave}) + if r.Toggled() { + playing = !playing + } + if d, ok := r.Seek(); ok { + pos = d + seeks = append(seeks, d) + } + }, 400, 200) + if !tt.HasText("0:10 / 1:00") { + t.Fatalf("times %q", tt.Texts()) + } + _ = tt.Click("Play") + if !playing { + t.Fatal("play not reported") + } + if _, ok := tt.Find("Pause"); !ok { + t.Fatal("button did not turn to pause") + } + r, _ := tt.Find("Playback position") + tt.ClickAt(r.X+r.W*0.75, r.Y+r.H/2) + if len(seeks) == 0 || pos < 40*time.Second || pos > 50*time.Second { + t.Fatalf("click at 3/4 seeked to %v", pos) + } + tt.Key(0, ui.KeyHome) + if pos != 0 { + t.Fatalf("Home gave %v", pos) + } + tt.Key(0, ui.KeyEnd) + if pos != time.Minute { + t.Fatalf("End gave %v", pos) + } +} + +func TestAudioMessageDisabled(t *testing.T) { + events := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Disabled(true).Children(func() { + r := AudioMessage(c, AudioMessageOptions{Duration: time.Minute}) + if _, ok := r.Seek(); ok || r.Toggled() { + events++ + } + }) + }, 400, 200) + _ = tt.Click("Play") + r, _ := tt.Find("Playback position") + tt.ClickAt(r.X+r.W/2, r.Y+r.H/2) + if events != 0 { + t.Fatalf("disabled audio reported %d events", events) + } +} + +func TestAudioMessageBadOptions(t *testing.T) { + for _, o := range []AudioMessageOptions{{}, {Duration: time.Second, Position: 2 * time.Second}, {Duration: time.Second, Waveform: []float32{2}}} { + func() { + defer func() { + if recover() == nil { + t.Errorf("%+v: no panic", o) + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + AudioMessage(c, o) + }, 100, 100) + }() + } +} + +func TestAudioMessageSnapshot(t *testing.T) { + snapshot(t, "audio_message", 340, 80, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { + AudioMessage(c, AudioMessageOptions{Duration: 47 * time.Second, Position: 18 * time.Second, Playing: true, Waveform: audioMessageWave}) + }) + }) +} diff --git a/branch_navigator.go b/branch_navigator.go new file mode 100644 index 0000000..b08f1b3 --- /dev/null +++ b/branch_navigator.go @@ -0,0 +1,52 @@ +package mujica + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// BranchNavigator switches among count branches of a message, showing "< 2 / 3 >"; Left and Right step. +func BranchNavigator(c *ui.Context, index *int, count int) ChoiceResult { + if count < 1 || *index < 0 || *index >= count { + panic("mujica: BranchNavigator needs 0 <= index < count") + } + k := Tokens(c) + label := fmt.Sprintf(msg(c, "branch_navigator.label"), *index+1, count) + var r ChoiceResult + r.Element = ui.Row(c).Focusable().AlignItems(ui.Center).Radius(theme.ControlRadius).Role(ui.RoleGroup).Label(label) + internal.Ring(r.Element, k.Focus, theme.ControlRadius) + disabled := r.Element.IsDisabled() + step := func(d int) { + if i := *index + d; i >= 0 && i < count && !disabled { + *index, r.changed = i, true + c.Announce(fmt.Sprintf(msg(c, "branch_navigator.label"), i+1, count)) + slog.Debug("mujica: branch_navigator change", "index", i) + } + } + switch { + case r.Element.Shortcut(0, ui.KeyLeft): + step(-1) + case r.Element.Shortcut(0, ui.KeyRight): + step(1) + case r.Element.Shortcut(0, ui.KeyHome): + step(-*index) + case r.Element.Shortcut(0, ui.KeyEnd): + step(count - 1 - *index) + } + r.Element.Children(func() { + if numberInputButton(c, "chevron-left", msg(c, "branch_navigator.previous"), *index > 0) { + step(-1) + } + ui.Text(c, fmt.Sprintf("%d / %d", *index+1, count)).FontFeatures("tnum").FontSize(FontSize(c, theme.CaptionSize)). + TextColor(k.TextMuted).SingleLine() + if numberInputButton(c, "chevron-right", msg(c, "branch_navigator.next"), *index < count-1) { + step(1) + } + }) + return r +} diff --git a/branch_navigator_test.go b/branch_navigator_test.go new file mode 100644 index 0000000..eee74eb --- /dev/null +++ b/branch_navigator_test.go @@ -0,0 +1,64 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestBranchNavigatorButtonsAndKeys(t *testing.T) { + i, changes, off := 1, 0, false + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Disabled(off).Children(func() { + if BranchNavigator(c, &i, 3).Changed() { + changes++ + } + }) + }, 300, 100) + if !tt.HasText("2 / 3") { + t.Fatalf("texts %q", tt.Texts()) + } + _ = tt.Click("Next branch") + _ = tt.Click("Next branch") + if i != 2 || changes != 1 { + t.Fatalf("index %d changes %d at the end", i, changes) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyLeft) + tt.Key(0, ui.KeyHome) + tt.Key(0, ui.KeyLeft) + if i != 0 || changes != 3 { + t.Fatalf("keys: index %d changes %d", i, changes) + } + tt.Key(0, ui.KeyEnd) + if i != 2 { + t.Fatalf("End gave %d", i) + } + off = true + tt.Key(0, ui.KeyLeft) + _ = tt.Click("Previous branch") + if i != 2 { + t.Fatalf("disabled navigator moved to %d", i) + } +} + +func TestBranchNavigatorBounds(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic for an index past count") + } + }() + i := 3 + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + BranchNavigator(c, &i, 3) + }, 100, 100) +} + +func TestBranchNavigatorSnapshot(t *testing.T) { + i := 1 + snapshot(t, "branch_navigator", 200, 60, func(c *ui.Context) { + ui.Column(c).Padding(12).Children(func() { BranchNavigator(c, &i, 3) }) + }) +} diff --git a/chat_container.go b/chat_container.go new file mode 100644 index 0000000..a3ad762 --- /dev/null +++ b/chat_container.go @@ -0,0 +1,48 @@ +package mujica + +import ( + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// ChatContainerOptions configure a ChatContainer. +type ChatContainerOptions struct { + // List configures the message area; its ID is required. + List MessageListOptions + // Input builds the input area under the messages. + Input func() + // Sidebar builds a side column, as a session list; nil shows none. + Sidebar func() + // SidebarWidth is the side column's width in DIPs; zero means 260. + SidebarWidth float32 +} + +// ChatContainer lays out a chat: sidebar, messages that follow the newest, and the input. +func ChatContainer(c *ui.Context, s *MessageListState, n int, opts ChatContainerOptions, row func(i int)) MessageListResult { + if opts.SidebarWidth < 0 { + panic("mujica: ChatContainer SidebarWidth must not be negative") + } + w := opts.SidebarWidth + if w == 0 { + w = 260 + } + k := Tokens(c) + u := Density(c).Unit() + var r MessageListResult + shell := ui.Row(c).Fill().AlignItems(ui.Stretch).Background(k.Background) + shell.Children(func() { + if opts.Sidebar != nil { + ui.Column(c).Width(w).Shrink(0).MinHeight(0).Background(k.Surface). + BorderWidth(0, theme.BorderWidth, 0, 0).BorderColor(k.Border).Children(opts.Sidebar) + } + ui.Column(c).Grow(1).MinWidth(0).MinHeight(0).Children(func() { + r = MessageList(c, s, n, opts.List, row) + if opts.Input != nil { + ui.Column(c).Shrink(0).Padding(u*3, u*4).BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border).Children(opts.Input) + } + }) + }) + r.Element = shell + return r +} diff --git a/chat_container_test.go b/chat_container_test.go new file mode 100644 index 0000000..ac9f9ca --- /dev/null +++ b/chat_container_test.go @@ -0,0 +1,64 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +// chatContainerApp is a chat with a sidebar and an input. +type chatContainerApp struct { + msgs []string + state MessageListState + draft string +} + +func (a *chatContainerApp) view(c *ui.Context) { + Use(c, Settings{}) + ChatContainer(c, &a.state, len(a.msgs), ChatContainerOptions{ + List: MessageListOptions{ID: func(i int) string { return a.msgs[i] }}, + Sidebar: func() { ui.Text(c, "Sessions").Padding(12) }, + Input: func() { TextInput(c, &a.draft, TextInputOptions{Label: "Prompt"}) }, + }, func(i int) { + MessageBubble(c, MessageAssistant, MessageBubbleOptions{}, func() { ui.Text(c, a.msgs[i]) }) + }) +} + +func TestChatContainerSlotsAndFollow(t *testing.T) { + a := &chatContainerApp{} + for i := range 40 { + a.msgs = append(a.msgs, "reply "+string(rune('A'+i%26))+string(rune('a'+i/26))) + } + tt := ui.NewTester(a.view, 600, 400) + tt.Frame() + if !tt.HasText("Sessions") || !tt.HasText("reply Nb") { + t.Fatalf("slots or newest message missing: %q", tt.Texts()) + } + _ = tt.Click("Prompt") + tt.Type("hi") + if a.draft != "hi" { + t.Fatalf("input slot draft %q", a.draft) + } + a.msgs = append(a.msgs, "fresh") + tt.Frame() + tt.Frame() + if !tt.HasText("fresh") { + t.Fatal("new message not followed") + } + tt.Scroll(400, 150, 0, -3000) + tt.Frame() + a.msgs = append(a.msgs, "later") + tt.Frame() + if tt.HasText("later") { + t.Fatal("followed after the user scrolled up") + } +} + +func TestChatContainerSnapshot(t *testing.T) { + snapshot(t, "chat_container", 560, 300, func(c *ui.Context) { + a := ui.Local(c.Root(), "app", func() *chatContainerApp { + return &chatContainerApp{msgs: []string{"Bonjour, Madame.", "How may I serve the court today?"}} + }) + (*a).view(c) + }) +} diff --git a/code_block.go b/code_block.go new file mode 100644 index 0000000..3143923 --- /dev/null +++ b/code_block.go @@ -0,0 +1,158 @@ +package mujica + +import ( + "fmt" + "log/slog" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// CopyResult is a block's element and whether its copy action ran this frame. +type CopyResult struct { + Element *ui.Element + copied bool +} + +// Copied reports that the block wrote its content to the clipboard this frame. +func (r CopyResult) Copied() bool { return r.copied } + +// CodeBlockOptions configure a CodeBlock. +type CodeBlockOptions struct { + // Language picks the highlighting: go, javascript, typescript, python, json, shell, sql. + Language string + LineNumbers bool + // Wrap and Collapsed set the toggles' first state. + Wrap, Collapsed bool +} + +// codeBlockState holds the user's toggles and the copy confirmation. +type codeBlockState struct { + wrap, collapsed bool + copiedAt time.Time +} + +// codeBlockCopiedFor is how long the copy button shows its check. +const codeBlockCopiedFor = 2 * time.Second + +// CodeBlock shows code with a language label, highlighting, copy, collapse and wrap toggles. +func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult { + k := Tokens(c) + u := Density(c).Unit() + var r CopyResult + r.Element = ui.Column(c).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border).Background(k.Surface).Clip().MinWidth(0) + st := ui.Local(r.Element, "code", func() codeBlockState { return codeBlockState{wrap: opts.Wrap, collapsed: opts.Collapsed} }) + disabled := r.Element.IsDisabled() + lines := strings.Split(strings.TrimSuffix(code, "\n"), "\n") + r.Element.Children(func() { + ui.Row(c).Padding(0, u, 0, u*3).Gap(u).AlignItems(ui.Center).Background(k.SurfaceHover).Children(func() { + lang := opts.Language + if lang == "" { + lang = msg(c, "code_block.plain") + } + ui.Text(c, lang).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Grow(1) + if codeBlockToggle(c, "wrap-text", msg(c, "code_block.wrap"), &st.wrap) { + slog.Debug("mujica: code_block wrap", "on", st.wrap) + } + icon, name := "copy", msg(c, "code_block.copy") + if left := codeBlockCopiedFor - c.Now().Sub(st.copiedAt); left > 0 { + icon, name = "check", msg(c, "code_block.copied") + c.After(left) + } + if iconAction(c, icons.Must(icon), name).Clicked() && !disabled { + c.WriteClipboard(code) + st.copiedAt, r.copied = c.Now(), true + c.Announce(msg(c, "code_block.copied")) + slog.Debug("mujica: code_block copy", "lines", len(lines)) + } + fold, foldName := "chevron-up", msg(c, "code_block.collapse") + if st.collapsed { + fold, foldName = "chevron-down", fmt.Sprintf(msg(c, "code_block.expand"), len(lines)) + } + if iconAction(c, icons.Must(fold), foldName).Clicked() && !disabled { + st.collapsed = !st.collapsed + slog.Debug("mujica: code_block collapse", "on", st.collapsed) + } + }) + if st.collapsed { + return + } + var body *ui.Element + if st.wrap { + body = ui.Column(c).Padding(u*2, u*3).MinWidth(0) + } else { + body = ui.ScrollHorizontal(c).Padding(u*2, u*3).AlignItems(ui.Start) + } + body.Font(theme.MonoFont).FontSize(FontSize(c, 13)).Children(func() { + ui.Column(c).MinWidth(0).Grow(1).Children(func() { codeBlockLines(c, lines, opts, st.wrap) }) + }) + }) + return r +} + +// codeBlockLines builds numbered, highlighted lines. +func codeBlockLines(c *ui.Context, lines []string, opts CodeBlockOptions, wrap bool) { + k := Tokens(c) + u := Density(c).Unit() + digits := len(fmt.Sprint(len(lines))) + inBlock := false + for i, line := range lines { + var toks []internal.Token + toks, inBlock = internal.Highlight(opts.Language, line, inBlock) + ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Children(func() { + if opts.LineNumbers { + ui.Text(c, fmt.Sprintf("%*d", digits, i+1)).TextColor(k.TextMuted).Shrink(0).NoWrap() + } + spans := make([]ui.Span, len(toks)) + for j, t := range toks { + spans[j] = ui.Span{Text: t.Text, Color: codeBlockColor(k, t.Kind), Italic: t.Kind == internal.TokComment} + } + if len(spans) == 0 { + spans = []ui.Span{{Text: " "}} + } + t := ui.RichText(c, spans...).MinWidth(0) + if wrap { + t.Grow(1) + } else { + t.NoWrap() + } + }) + } +} + +// codeBlockColor colors a token from the theme; comments are also italic. +func codeBlockColor(k theme.Tokens, kind internal.TokenKind) ui.Color { + switch kind { + case internal.TokKeyword: + return k.AccentText + case internal.TokString: + return k.Success + case internal.TokNumber: + return k.Warning + case internal.TokComment, internal.TokPunct: + return k.TextMuted + case internal.TokType: + return k.Info + } + return k.Text +} + +// codeBlockToggle is a pressed-state icon button; on shows a Selection face and frame. +func codeBlockToggle(c *ui.Context, icon, label string, on *bool) bool { + k := Tokens(c) + b := ui.ToggleBase(c, on).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label).TextColor(k.TextMuted) + internal.Ring(b, k.Focus, theme.ControlRadius) + switch { + case *on: + b.Background(k.Selection).Border(theme.BorderWidth, k.AccentText).TextColor(k.AccentText) + case b.Hovered(): + b.Background(k.SurfaceHover).TextColor(k.Text) + } + b.Children(func() { ui.Icon(c, icons.Must(icon)).Size(16, 16) }) + return b.Changed() +} diff --git a/code_block_test.go b/code_block_test.go new file mode 100644 index 0000000..59d1995 --- /dev/null +++ b/code_block_test.go @@ -0,0 +1,79 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +const codeBlockSample = "// greet the court\nfunc greet(name string) int {\n\treturn len(\"hi \" + name) + 42\n}" + +func codeBlockTester(opts CodeBlockOptions, copies *int, disabled *bool) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(360).Disabled(*disabled).Children(func() { + if CodeBlock(c, codeBlockSample, opts).Copied() { + *copies++ + } + }) + }, 400, 300) +} + +func TestCodeBlockLabelNumbersAndCopy(t *testing.T) { + copies, off := 0, false + tt := codeBlockTester(CodeBlockOptions{Language: "go", LineNumbers: true}, &copies, &off) + for _, s := range []string{"go", "1", "4", "func", "greet", "42"} { + if !tt.HasText(s) { + t.Errorf("missing %q in %q", s, tt.Texts()) + } + } + if err := tt.Click("Copy code"); err != nil { + t.Fatal(err) + } + if tt.Clipboard() != codeBlockSample || copies != 1 { + t.Fatalf("copied %d, clipboard matches %v", copies, tt.Clipboard() == codeBlockSample) + } + if _, ok := tt.Find("Copied"); !ok { + t.Fatal("no copied confirmation") + } +} + +func TestCodeBlockCollapseAndWrapByKeyboard(t *testing.T) { + copies, off := 0, false + tt := codeBlockTester(CodeBlockOptions{Language: "go"}, &copies, &off) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if !tt.Focused("Wrap lines") { + t.Fatal("Tab did not reach the wrap toggle") + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if tt.HasText("greet") { + t.Fatal("collapse left the code built") + } + if err := tt.Click("Show 4 lines"); err != nil { + t.Fatal(err) + } + if !tt.HasText("greet") { + t.Fatal("expand did not show the code") + } +} + +func TestCodeBlockDisabledBlocksActions(t *testing.T) { + copies, off := 0, true + tt := codeBlockTester(CodeBlockOptions{Collapsed: true}, &copies, &off) + _ = tt.Click("Copy code") + _ = tt.Click("Show 4 lines") + if copies != 0 || tt.Clipboard() != "" || tt.HasText("greet") { + t.Fatalf("disabled block acted: copies %d", copies) + } +} + +func TestCodeBlockSnapshot(t *testing.T) { + snapshot(t, "code_block", 440, 200, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { + CodeBlock(c, codeBlockSample, CodeBlockOptions{Language: "go", LineNumbers: true}) + }) + }) +} diff --git a/docs/components/201-chat-container.md b/docs/components/201-chat-container.md new file mode 100644 index 0000000..2c5b04d --- /dev/null +++ b/docs/components/201-chat-container.md @@ -0,0 +1,43 @@ +# 201 ChatContainer 对话容器 + +## 用途 + +整页对话布局:左侧可选侧栏插槽,右侧为消息区与底部输入区。消息区就是 MessageList:新消息到来时自动滚到底部,用户向上翻看时停止跟随,并在底部显示回到最新按钮。 + +## 最小示例 + +```go +mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{ + List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }}, + Input: func() { ui.Text(c, "Composer goes here") }, +}, func(i int) { ui.Text(c, msgs[i]) }) +``` + +签名:`func ChatContainer(c *ui.Context, s *MessageListState, n int, opts ChatContainerOptions, row func(i int)) MessageListResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `s` | `*MessageListState` | 必填 | 消息区的位置与已读状态,由调用方持有。 | +| `n / row` | `int / func(i int)` | 必填 | 消息数量与第 i 条消息的构建函数;只构建可见消息。 | +| `List` | `MessageListOptions` | `ID` 必填 | 传给消息列表的选项,见 202。 | +| `Input` | `func()` | nil | 输入区插槽,位于消息区下方,带上边线。 | +| `Sidebar` | `func()` | nil | 侧栏插槽;nil 时不显示侧栏。 | +| `SidebarWidth` | `float32` | 260 | 侧栏宽度 DIP;负数 panic。 | + +## 状态 + +消息区跟随底部 / 用户上翻后停止跟随;离开底部时出现带未读数的回到最新按钮。 + +## 事件 + +返回的 `MessageListResult.Unread()` 给出离开底部后新到的消息数。 + +## 键盘操作 + +Tab 在侧栏、消息与输入区之间移动;消息区获得焦点后滚轮与 PageUp/PageDown(Linux、Windows)滚动。 + +## 限制 + +- 侧栏宽度固定,不提供拖动调整;需要时用 SplitPane(008)包裹。 diff --git a/docs/components/202-message-list.md b/docs/components/202-message-list.md new file mode 100644 index 0000000..0e2bc60 --- /dev/null +++ b/docs/components/202-message-list.md @@ -0,0 +1,42 @@ +# 202 MessageList 消息列表 + +## 用途 + +虚拟化的消息列表,基于 MyGo `ui.List`:只构建可见消息,以稳定消息 ID 保持位置;从底部开始并跟随新消息,用户上翻后停止跟随,并显示新消息提示;可按日期插入分隔。 + +## 最小示例 + +```go +r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{ + ID: func(i int) string { return ids[i] }, +}, func(i int) { ui.Text(c, "Message "+ids[i]) }) +fmt.Println("unread", r.Unread()) +``` + +签名:`func MessageList(c *ui.Context, s *MessageListState, n int, opts MessageListOptions, row func(i int)) MessageListResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `ID` | `func(i int) string` | 必填 | 消息的稳定 ID;在上方插入旧消息时视野中的消息保持不动。ID 须唯一。 | +| `Date` | `func(i int) time.Time` | nil | 设置后在每个新日期的第一条消息前显示“今天 / 昨天 / 日期”分隔。 | +| `Label` | `func(i int) string` | nil | 消息的无障碍名称,也用于键入定位。 | +| `n` | `int` | 必填 | 消息数;负数 panic。 | + +## 状态 + +跟随底部;离开底部时底部居中出现 ScrollToBottomButton(含未读数);点击后回到底部并清零。 + +## 事件 + +`Unread()`:离开底部后新增的消息数。`MessageListState.ScrollToEnd()` 与 `AtEnd()` 供调用方控制。 + +## 键盘操作 + +列表获得焦点后,滚轮、方向键与 PageUp/PageDown 滚动;回到最新按钮可用 Tab 聚焦,Enter 或 Space 触发。 + +## 限制 + +- 不检查 ID 是否重复(逐帧检查 10 万条代价过高);重复 ID 会让两条消息共享状态。 +- 消息高度由 MyGo 在显示时测量,未显示消息按已测高度估算,长距离跳转时滚动条可能微调。 diff --git a/docs/components/203-message-bubble.md b/docs/components/203-message-bubble.md new file mode 100644 index 0000000..96349f0 --- /dev/null +++ b/docs/components/203-message-bubble.md @@ -0,0 +1,41 @@ +# 203 MessageBubble 消息气泡 + +## 用途 + +按角色框住一条消息:用户靠右、Selection 底色;助手靠左、Surface 底与细边;系统居中、灰色小字;错误带 Danger 边框与警示图标。头像与名称为插槽,宽度可限制。 + +## 最小示例 + +```go +mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() { + ui.Text(c, "The ball begins at nine.") +}) +``` + +签名:`func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, content func()) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `role` | `MessageRole` | 必填 | `MessageUser`、`MessageAssistant`、`MessageSystem`、`MessageError`。 | +| `Name` | `string` | "" | 显示在气泡上方的名称。 | +| `Avatar` | `func()` | nil | 头像插槽,用户在右侧、其他角色在左侧;系统消息不显示。 | +| `MaxWidth` | `float32` | 560 | 气泡最大宽度 DIP;负数 panic。 | +| `content` | `func()` | 必填 | 气泡内容,如 MarkdownView 或 StreamingText。 | + +## 状态 + +四种角色外观;错误角色同时使用图标、边框与文字,不只依赖颜色。 + +## 事件 + +无;交互由内容与 MessageActions 提供。 + +## 键盘操作 + +气泡本身不获取焦点;内容中的链接、按钮按 Tab 顺序访问。辅助技术读到“角色,名称”分组。 + +## 限制 + +- 气泡不自带操作栏;把 MessageActions 放在 content 中或气泡下方。 diff --git a/docs/components/204-message-header.md b/docs/components/204-message-header.md new file mode 100644 index 0000000..f545f94 --- /dev/null +++ b/docs/components/204-message-header.md @@ -0,0 +1,37 @@ +# 204 MessageHeader 消息头 + +## 用途 + +在消息上方显示模型名、时间与状态(发送中、生成中、已发送、失败),状态同时用图标和文字表达。 + +## 最小示例 + +```go +mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent}) +``` + +签名:`func MessageHeader(c *ui.Context, opts MessageHeaderOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Model` | `string` | "" | 模型名,加粗;空则不显示。 | +| `Time` | `time.Time` | 零值 | 时间,按 `15:04` 显示;零值不显示。 | +| `Status` | `MessageStatus` | `MessageNoStatus` | `MessageSending`、`MessageStreaming`、`MessageSent`、`MessageFailed`;未知值 panic。 | + +## 状态 + +生成中显示菱形加载标记(减少动画时静止);失败用 Danger 色并带警示图标。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。 + +## 限制 + +- 时间格式固定为 24 小时制时分,日期由 MessageList 的日期分隔显示。 diff --git a/docs/components/205-streaming-text.md b/docs/components/205-streaming-text.md new file mode 100644 index 0000000..552c2cf --- /dev/null +++ b/docs/components/205-streaming-text.md @@ -0,0 +1,38 @@ +# 205 StreamingText 流式文本 + +## 用途 + +显示随流式输出不断增长的文本。可平滑出字(按速率逐字显示),可显示流式光标;减少动画时直接显示全部文本。文本被替换为不以旧文本开头的内容时,从共同前缀继续。 + +## 最小示例 + +```go +mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true}) +``` + +签名:`func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `text` | `string` | 必填 | 当前收到的全部文本。 | +| `Streaming` | `bool` | false | 在文本末尾显示 AccentText 色的静态光标 ▍。 | +| `Smooth` | `bool` | false | 新到的文本按 Rate 逐字显示;关闭时立即显示。 | +| `Rate` | `float64` | 80 | 平滑出字速度,字符/秒;负数 panic。 | + +## 状态 + +出字中 / 完成;减少动画时始终为完成。光标不闪烁,避免装饰性循环。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。辅助技术读到的名称始终是完整文本。 + +## 限制 + +- 只渲染纯文本;Markdown 内容请交给 MarkdownView(206),它的增量解析同样适合流式输出。 diff --git a/docs/components/206-markdown-view.md b/docs/components/206-markdown-view.md new file mode 100644 index 0000000..e1b35b0 --- /dev/null +++ b/docs/components/206-markdown-view.md @@ -0,0 +1,39 @@ +# 206 MarkdownView Markdown 视图 + +## 用途 + +渲染 Markdown 子集:标题、段落、粗体、斜体、删除线、行内代码、有序与无序列表(含缩进层级)、引用、链接、围栏代码块(交给 CodeBlock)、表格(交给 TableBlock)与分隔线(菱形饰线)。解析器在 `internal/markdown.go`,按块增量解析:新文本只是在旧文本后追加时,只重新解析最后一块,已完成的块保持不变。 + +## 最小示例 + +```go +if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { + fmt.Println("open", url) +} +``` + +签名:`func MarkdownView(c *ui.Context, src string) MarkdownResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `src` | `string` | 必填 | Markdown 源文本;流式输出时每帧传入增长后的全文即可。 | + +## 状态 + +围栏未闭合的代码块按已收到的内容显示;未闭合的强调标记按字面显示,闭合后再加样式。 + +## 事件 + +`Link()` 返回本帧被点击或用 Enter 激活的链接地址;打开链接由调用方决定。 + +## 键盘操作 + +链接可用 Tab 聚焦,Enter 激活;代码块与表格的按钮按 Tab 顺序访问。 + +## 限制 + +- 只支持上述子集:不支持 HTML、图片、脚注、任务列表、Setext 标题、链接引用定义,列表项内不支持多段落。 +- 表格单元格按纯文本显示,不解析行内样式。 +- 源文本被整体替换(不是在末尾追加)时重新解析全部块。 diff --git a/docs/components/207-code-block.md b/docs/components/207-code-block.md new file mode 100644 index 0000000..87b6375 --- /dev/null +++ b/docs/components/207-code-block.md @@ -0,0 +1,39 @@ +# 207 CodeBlock 代码块 + +## 用途 + +显示代码:语言标签、可选行号、基础语法着色(`internal.Highlight`,支持 go、javascript、typescript、python、json、shell、sql),复制、折叠与自动换行切换。 + +## 最小示例 + +```go +mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true}) +``` + +签名:`func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Language` | `string` | ""(显示“文本”) | 着色语言;未知语言按纯文本显示。 | +| `LineNumbers` | `bool` | false | 显示右对齐的行号。 | +| `Wrap` | `bool` | false | 自动换行的初始状态;关闭时横向滚动。 | +| `Collapsed` | `bool` | false | 折叠的初始状态。 | + +## 状态 + +换行开关以 Selection 底色加边框表示开启;复制后按钮在 2 秒内显示对勾“已复制”;折叠时只显示标题栏,展开按钮写明行数。关键字、字符串、数字、类型使用不同颜色,注释同时使用斜体。 + +## 事件 + +`Copied()`:本帧把代码写入了剪贴板。 + +## 键盘操作 + +Tab 依次到换行开关、复制、折叠按钮;Space 或 Enter 触发。 + +## 限制 + +- 着色是逐行的词法着色,不做语法分析;块注释可跨行。 +- 不做虚拟化,适合消息中的代码片段,不适合上万行文件。 diff --git a/docs/components/208-table-block.md b/docs/components/208-table-block.md new file mode 100644 index 0000000..5891e29 --- /dev/null +++ b/docs/components/208-table-block.md @@ -0,0 +1,39 @@ +# 208 TableBlock 表格块 + +## 用途 + +在消息中显示小型表格:表头加粗,列按内容宽度排列并铺满消息宽度,过宽时横向滚动;可复制为 TSV。 + +## 最小示例 + +```go +if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() { + fmt.Println("copied as TSV") +} +``` + +签名:`func TableBlock(c *ui.Context, header []string, rows [][]string, opts TableBlockOptions) CopyResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `header` | `[]string` | 必填 | 表头;为空 panic。 | +| `rows` | `[][]string` | nil | 数据行;每行列数必须与表头相同,否则 panic。 | +| `Align` | `[]ui.Align` | 全部 `ui.Start` | 各列对齐:`ui.Start`、`ui.Center`、`ui.End`;多于列数 panic。 | + +## 状态 + +复制后按钮 2 秒内显示对勾。 + +## 事件 + +`Copied()`:本帧写入了 TSV(制表符分列、换行分行;单元格内的制表符与换行替换为空格)。 + +## 键盘操作 + +Tab 到复制按钮,Enter 或 Space 复制。 + +## 限制 + +- 不排序、不选择;需要这些功能请使用 DataTable(069 起)。 diff --git a/docs/components/209-image-grid.md b/docs/components/209-image-grid.md new file mode 100644 index 0000000..ae3ffa4 --- /dev/null +++ b/docs/components/209-image-grid.md @@ -0,0 +1,40 @@ +# 209 ImageGrid 图片网格 + +## 用途 + +在消息中排列 1 至 9 张图片:1 张大图,2 与 4 张两列,其余三列。点击已加载的图片打开 ImageViewer;加载中与失败状态复用 Image 组件,失败时可重试。 + +## 最小示例 + +```go +if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { + fmt.Println("reload", i) +} +``` + +签名:`func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `images` | `[]GridImage` | 必填 | 1 至 9 张,否则 panic。 | +| `Source` | `ui.ImageSource` | nil | 已加载的位图或 SVG。 | +| `Alt` | `string` | "" | 替代说明,也是按钮名称与查看器标题。 | +| `Loading / Err` | `bool / error` | false / nil | 调用方的加载中与失败状态。 | + +## 状态 + +已加载、加载中(菱形加载标记)、失败(重试按钮);只有已加载的图片可点击。 + +## 事件 + +`Retried()` 返回本帧点击了重试的图片下标。查看器在组件内部打开与关闭。 + +## 键盘操作 + +Tab 聚焦图片,Enter 或 Space 打开查看器;查看器内的键盘操作见 ImageViewer。 + +## 限制 + +- 查看器只包含已加载的图片,序号按已加载图片计算。 diff --git a/docs/components/210-file-message.md b/docs/components/210-file-message.md new file mode 100644 index 0000000..5bfc0f5 --- /dev/null +++ b/docs/components/210-file-message.md @@ -0,0 +1,40 @@ +# 210 FileMessage 文件消息 + +## 用途 + +显示消息中的文件:按类型的图标、文件名、大小,以及打开与下载回调。 + +## 最小示例 + +```go +r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) +if r.Downloaded() { + fmt.Println("save treaty.pdf") +} +``` + +签名:`func FileMessage(c *ui.Context, opts FileMessageOptions) FileMessageResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Name` | `string` | 必填 | 文件名;为空 panic。 | +| `Size` | `int64` | 0 | 字节数,按 B、KB、MB、GB 显示(1024 进制);负数 panic。 | +| `Type` | `string` | "" | MIME 类型;为空时按扩展名选择图标。 | + +## 状态 + +悬停时文件区域加深;禁用时两个操作都不响应。 + +## 事件 + +`Opened()`:点击或在文件区按 Enter。`Downloaded()`:点击下载按钮。 + +## 键盘操作 + +Tab 依次到文件区与下载按钮,Enter 或 Space 触发。 + +## 限制 + +- 组件不读取文件、不下载:打开与下载由调用方完成。 diff --git a/docs/components/211-audio-message.md b/docs/components/211-audio-message.md new file mode 100644 index 0000000..f8c8879 --- /dev/null +++ b/docs/components/211-audio-message.md @@ -0,0 +1,41 @@ +# 211 AudioMessage 语音消息 + +## 用途 + +语音条:播放/暂停按钮、波形、播放进度与时长。播放由调用方驱动:组件只报告播放切换与跳转请求,调用方更新 Position 与 Playing。波形基于 MyGo `ui.SliderBase`,可拖动与键盘跳转。 + +## 最小示例 + +```go +r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos}) +if d, ok := r.Seek(); ok { + pos = d +} +``` + +签名:`func AudioMessage(c *ui.Context, opts AudioMessageOptions) AudioMessageResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Duration` | `time.Duration` | 必填 | 时长,必须大于 0。 | +| `Position` | `time.Duration` | 0 | 当前进度,0 到 Duration 之间,否则 panic。 | +| `Playing` | `bool` | false | 按钮显示暂停(播放中)或播放。 | +| `Waveform` | `[]float32` | nil | 0 到 1 的响度采样,按宽度重采样为竖条;为空画平线;越界 panic。 | + +## 状态 + +已播放的竖条为 AccentText,未播放为半透明 TextMuted,当前位置另有一条竖线,进度不只靠颜色表达;右侧显示 `m:ss / m:ss`。 + +## 事件 + +`Toggled()`:按下播放或暂停。`Seek()`:点击、拖动或按键请求的新位置。 + +## 键盘操作 + +Tab 到播放按钮(Enter 或 Space)与波形;波形上 Left/Right(Up/Down)按时长的 1% 移动,Home、End 到开头与结尾。 + +## 限制 + +- 不解码音频、不生成波形:采样由调用方提供。 diff --git a/docs/components/212-link-preview-card.md b/docs/components/212-link-preview-card.md new file mode 100644 index 0000000..c96bc48 --- /dev/null +++ b/docs/components/212-link-preview-card.md @@ -0,0 +1,42 @@ +# 212 LinkPreviewCard 链接预览卡片 + +## 用途 + +显示链接的站点、标题、描述与缩略图;元数据由调用方获取。整张卡片是一个按钮,悬停显示完整地址。 + +## 最小示例 + +```go +p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} +if mujica.LinkPreviewCard(c, p).Clicked() { + fmt.Println("open", p.URL) +} +``` + +签名:`func LinkPreviewCard(c *ui.Context, p LinkPreview) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `URL` | `string` | 必填 | 链接地址,作为悬停提示。 | +| `Title` | `string` | 必填 | 标题,最多两行,AccentText 色。 | +| `Description` | `string` | "" | 描述,最多两行。 | +| `Site` | `string` | URL | 站点名;为空时显示 URL。 | +| `Thumbnail` | `ui.ImageSource` | nil | 缩略图,64×64 裁切显示。 | + +## 状态 + +悬停时底色加深;聚焦时显示焦点环。 + +## 事件 + +`Clicked()`:点击或 Enter、Space;打开链接由调用方完成。 + +## 键盘操作 + +Tab 聚焦,Enter 或 Space 触发。 + +## 限制 + +- 组件不联网:标题、描述与缩略图都由调用方提供。 diff --git a/docs/components/213-quote-reply.md b/docs/components/213-quote-reply.md new file mode 100644 index 0000000..6f7857b --- /dev/null +++ b/docs/components/213-quote-reply.md @@ -0,0 +1,38 @@ +# 213 QuoteReply 引用回复 + +## 用途 + +显示被引用消息的作者与单行摘要(多余空白折叠,过长省略);点击跳转到原消息。 + +## 最小示例 + +```go +if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { + fmt.Println("jump to the quoted message") +} +``` + +签名:`func QuoteReply(c *ui.Context, author, excerpt string) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `author` | `string` | 必填 | 原消息作者;为空 panic。 | +| `excerpt` | `string` | "" | 原消息摘要;换行与连续空白合并为一个空格。 | + +## 状态 + +左侧 3 DIP AccentText 竖线标明引用;悬停时底色变为 Selection。 + +## 事件 + +`Clicked()`:点击或 Enter、Space;跳转由调用方完成(如 MessageList 的 ScrollTo)。 + +## 键盘操作 + +Tab 聚焦,Enter 或 Space 触发。辅助技术读到“回复 作者:摘要”。 + +## 限制 + +- 只显示一行摘要,不渲染原消息的格式。 diff --git a/docs/components/214-message-actions.md b/docs/components/214-message-actions.md new file mode 100644 index 0000000..6828031 --- /dev/null +++ b/docs/components/214-message-actions.md @@ -0,0 +1,38 @@ +# 214 MessageActions 消息操作 + +## 用途 + +消息下方的操作工具栏:复制、重新生成、编辑、赞、踩、朗读、分享,按角色显示:用户消息为复制、编辑;助手消息为复制、重新生成、赞、踩、朗读、分享;系统消息为复制;错误消息为复制、重新生成。 + +## 最小示例 + +```go +if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok { + fmt.Println("action", a) +} +``` + +签名:`func MessageActions(c *ui.Context, role MessageRole, opts MessageActionsOptions) MessageActionsResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `role` | `MessageRole` | 必填 | 决定显示哪些操作。 | +| `Feedback` | `*MessageFeedback` | nil | 赞踩状态;nil 时不显示赞与踩。再次点击已选的赞或踩会取消。 | + +## 状态 + +赞或踩选中时为按下状态:Selection 底色、AccentText 边框与图标;辅助技术读到切换按钮的按下状态。 + +## 事件 + +`Action()` 返回本帧的操作:`ActionCopy`、`ActionRegenerate`、`ActionEdit`、`ActionLike`、`ActionDislike`、`ActionSpeak`、`ActionShare`。复制、朗读与分享由调用方执行。 + +## 键盘操作 + +整个工具栏是一个 Tab 停靠点,Left/Right 在操作间移动,Home、End 到首末;Enter 或 Space 触发。 + +## 限制 + +- 不内置复制内容与朗读:调用方收到操作后自行处理。 diff --git a/docs/components/215-message-editor.md b/docs/components/215-message-editor.md new file mode 100644 index 0000000..5d6b0d1 --- /dev/null +++ b/docs/components/215-message-editor.md @@ -0,0 +1,38 @@ +# 215 MessageEditor 消息编辑器 + +## 用途 + +编辑已发送的消息:多行文本框加取消与保存。保存时调用方据草稿产生新分支(配合 BranchNavigator)。 + +## 最小示例 + +```go +if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() { + fmt.Println("new branch from", draft) +} +``` + +签名:`func MessageEditor(c *ui.Context, draft *string, opts MessageEditorOptions) MessageEditorResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `draft` | `*string` | 必填 | 正在编辑的文本。 | +| `Original` | `string` | "" | 原文;草稿与原文相同或只有空白时不能保存。 | + +## 状态 + +保存按钮在内容无效时禁用;文本框保留输入法、撤销与剪贴板行为。 + +## 事件 + +`Saved()`:点击保存或 Cmd/Ctrl+Enter。`Canceled()`:点击取消或在文本框中按 Escape。 + +## 键盘操作 + +Cmd/Ctrl+Enter 保存,Escape 取消;Tab 依次到文本框、取消、保存。 + +## 限制 + +- 不保存历史:分支与原文由调用方保存。 diff --git a/docs/components/216-branch-navigator.md b/docs/components/216-branch-navigator.md new file mode 100644 index 0000000..8ab7bcc --- /dev/null +++ b/docs/components/216-branch-navigator.md @@ -0,0 +1,38 @@ +# 216 BranchNavigator 分支切换 + +## 用途 + +在一条消息的多个分支间切换,显示“< 2 / 3 >”。 + +## 最小示例 + +```go +if mujica.BranchNavigator(c, &branch, 3).Changed() { + fmt.Println("show branch", branch) +} +``` + +签名:`func BranchNavigator(c *ui.Context, index *int, count int) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `index` | `*int` | 必填 | 当前分支,0 起;不在 0 到 count−1 之间时 panic。 | +| `count` | `int` | 必填 | 分支数,至少 1。 | + +## 状态 + +首末分支时对应箭头禁用;切换时向辅助技术播报“第 n 个分支,共 m 个”。 + +## 事件 + +`Changed()`:分支改变。 + +## 键盘操作 + +整个控件是一个 Tab 停靠点:Left/Right 切换,Home、End 到第一个与最后一个;箭头按钮只响应指针。 + +## 限制 + +- 箭头按钮不进入 Tab 序列,避免到达首末分支时焦点停在禁用按钮上。 diff --git a/docs/components/217-error-message.md b/docs/components/217-error-message.md new file mode 100644 index 0000000..441891c --- /dev/null +++ b/docs/components/217-error-message.md @@ -0,0 +1,40 @@ +# 217 ErrorMessage 错误消息 + +## 用途 + +显示失败的回复:标题、原始错误文本(等宽、可选择复制,不改写、不截断)与重试按钮;限流时显示倒计时,倒计时结束前不能重试。 + +## 最小示例 + +```go +opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} +if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { + fmt.Println("send again") +} +``` + +签名:`func ErrorMessage(c *ui.Context, text string, opts ErrorMessageOptions) ErrorMessageResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `text` | `string` | 必填 | 原始错误文本,原样显示;为空 panic。 | +| `Title` | `string` | “出错了” | 概括失败原因。 | +| `RetryAt` | `time.Time` | 零值 | 限流解除时间;之前重试按钮禁用并显示“n 秒后重试”与“已限流”。 | + +## 状态 + +可重试 / 限流倒计时;Danger 边框、图标与标题文字共同表示错误。倒计时只在秒数变化时重绘。 + +## 事件 + +`Retried()`:按下重试。 + +## 键盘操作 + +Tab 到错误文本(可用 Shift+方向键选择、Cmd/Ctrl+C 复制)与重试按钮。 + +## 限制 + +- 倒计时依据窗口时钟;调用方的 RetryAt 应来自服务端的 Retry-After。 diff --git a/docs/components/218-thinking-block.md b/docs/components/218-thinking-block.md new file mode 100644 index 0000000..fb2ca98 --- /dev/null +++ b/docs/components/218-thinking-block.md @@ -0,0 +1,40 @@ +# 218 ThinkingBlock 思考过程 + +## 用途 + +可折叠的思考过程:思考中显示加载标记与实时秒数,结束后显示“思考了 n 秒”。默认折叠或展开由调用方传入的 `*open` 初值决定。基于 Collapsible。 + +## 最小示例 + +```go +mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { + ui.Text(c, "Weigh the guest list.") +}) +``` + +签名:`func ThinkingBlock(c *ui.Context, open *bool, opts ThinkingBlockOptions, content func()) ChoiceResult` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `open` | `*bool` | 必填 | 是否展开;其初值即默认状态。 | +| `Thinking` | `bool` | false | 思考中:显示加载标记与从 Started 起的秒数。 | +| `Started` | `time.Time` | 零值 | 思考开始时间;Thinking 时必填,否则 panic。 | +| `Elapsed` | `time.Duration` | 0 | 思考结束后的总耗时;负数 panic。 | + +## 状态 + +思考中 / 已完成;展开时内容带左侧细线、TextMuted 色。减少动画时加载标记静止,秒数仍每秒更新。 + +## 事件 + +`Changed()`:展开状态改变。 + +## 键盘操作 + +Tab 聚焦标题,Enter 或 Space 展开与折叠。 + +## 限制 + +- 思考中每秒重绘一次以更新秒数;折叠且完成后不再重绘。 diff --git a/docs/components/219-scroll-to-bottom-button.md b/docs/components/219-scroll-to-bottom-button.md new file mode 100644 index 0000000..1cd7550 --- /dev/null +++ b/docs/components/219-scroll-to-bottom-button.md @@ -0,0 +1,40 @@ +# 219 ScrollToBottomButton 回到底部按钮 + +## 用途 + +离开底部时出现在容器底部居中的圆角按钮,带未读计数;点击回到底部。MessageList 已内置,也可单独用于其他滚动区域。 + +## 最小示例 + +```go +ui.Box(c).Fill().Children(func() { + if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { + list.ScrollToEnd() + } +}) +``` + +签名:`func ScrollToBottomButton(c *ui.Context, away bool, unread int) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `away` | `bool` | 必填 | 是否已离开底部;false 时返回不可见的空元素,其 Clicked 为 false。 | +| `unread` | `int` | 0 | 未读数,大于 0 时显示;负数 panic。 | + +## 状态 + +出现时 160 毫秒淡入(减少动画时直接显示);悬停时底色加深。 + +## 事件 + +`Clicked()`:点击或 Enter、Space;滚动由调用方完成。 + +## 键盘操作 + +Tab 聚焦,Enter 或 Space 触发。辅助技术读到“回到最新,n 条新消息”。 + +## 限制 + +- 按钮以绝对定位贴在父元素底部,父元素需要占满滚动区域。 diff --git a/docs/components/220-typing-indicator.md b/docs/components/220-typing-indicator.md new file mode 100644 index 0000000..b0094d6 --- /dev/null +++ b/docs/components/220-typing-indicator.md @@ -0,0 +1,35 @@ +# 220 TypingIndicator 正在输入 + +## 用途 + +对方正在输入的提示:三个依次明暗变化的圆点;减少动画时改为静态文字“某某 正在输入…”。 + +## 最小示例 + +```go +mujica.TypingIndicator(c, "Herald") +``` + +签名:`func TypingIndicator(c *ui.Context, name string) *ui.Element` + +## 参数 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `name` | `string` | 必填 | 正在输入的一方;为空 panic。 | + +## 状态 + +动画 / 静态文字;状态角色,辅助技术读到“某某 正在输入…”。 + +## 事件 + +无。 + +## 键盘操作 + +不获取焦点。 + +## 限制 + +- 动画只在显示时请求下一帧,移出界面后停止重绘。 diff --git a/error_message.go b/error_message.go new file mode 100644 index 0000000..64847d3 --- /dev/null +++ b/error_message.go @@ -0,0 +1,72 @@ +package mujica + +import ( + "fmt" + "log/slog" + "math" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// ErrorMessageOptions configure an ErrorMessage. +type ErrorMessageOptions struct { + // Title sums the failure up; empty uses the built-in "Something went wrong". + Title string + // RetryAt is when a rate limit lifts; until then retry waits with a countdown. + RetryAt time.Time +} + +// ErrorMessageResult is the error's element and a retry the user asked for. +type ErrorMessageResult struct { + Element *ui.Element + retried bool +} + +// Retried reports a press of retry. +func (r ErrorMessageResult) Retried() bool { return r.retried } + +// ErrorMessage shows a failed reply with the original error text, selectable, and retry. +func ErrorMessage(c *ui.Context, text string, opts ErrorMessageOptions) ErrorMessageResult { + if text == "" { + panic("mujica: ErrorMessage needs the error text") + } + k := Tokens(c) + u := Density(c).Unit() + title := opts.Title + if title == "" { + title = msg(c, "error_message.title") + } + wait := opts.RetryAt.Sub(c.Now()) + var r ErrorMessageResult + r.Element = ui.Column(c).Gap(u*2).Padding(u*3, u*4).MaxWidth(560).Radius(theme.CardRadius). + Background(k.Surface).Border(theme.BorderWidth, k.Danger).Role(ui.RoleGroup).Label(title) + disabled := r.Element.IsDisabled() + r.Element.Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { + ui.Icon(c, icons.Must("circle-alert")).Size(16, 16).Shrink(0).TextColor(k.Danger) + ui.Text(c, title).FontWeight(600).TextColor(k.Danger) + }) + ui.Text(c, text).Selectable().Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text) + ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { + label := msg(c, "error_message.retry") + if wait > 0 { + secs := int(math.Ceil(wait.Seconds())) + label = fmt.Sprintf(msg(c, "error_message.wait"), secs) + // Wake when the shown second changes. + c.After(wait - time.Duration(secs-1)*time.Second) + } + if Button(c, label, ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw"), Disabled: wait > 0}).Clicked() && !disabled { + r.retried = true + slog.Debug("mujica: error_message retry") + } + if wait > 0 { + ui.Text(c, msg(c, "error_message.limited")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + } + }) + }) + return r +} diff --git a/error_message_test.go b/error_message_test.go new file mode 100644 index 0000000..08afeac --- /dev/null +++ b/error_message_test.go @@ -0,0 +1,69 @@ +package mujica + +import ( + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +const errorMessageText = `429 Too Many Requests: {"error":"rate_limit","retry_after":30}` + +func TestErrorMessageKeepsTextAndRetries(t *testing.T) { + retries := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if ErrorMessage(c, errorMessageText, ErrorMessageOptions{}).Retried() { + retries++ + } + }, 500, 200) + if !tt.HasText(errorMessageText) || !tt.HasText("Something went wrong") { + t.Fatalf("texts %q", tt.Texts()) + } + _ = tt.Click("Retry") + if retries != 1 { + t.Fatalf("retries %d", retries) + } +} + +func TestErrorMessageRateLimitCountdown(t *testing.T) { + retries := 0 + until := time.Now().Add(2500 * time.Millisecond) + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if ErrorMessage(c, errorMessageText, ErrorMessageOptions{Title: "Rate limited by the API", RetryAt: until}).Retried() { + retries++ + } + }, 500, 200) + if !tt.HasText("Retry in 3 s") { + t.Fatalf("countdown missing: %q", tt.Texts()) + } + _ = tt.Click("Retry in 3 s") + if retries != 0 { + t.Fatal("retried during the rate limit") + } + until = time.Now().Add(-time.Second) + tt.Frame() + _ = tt.Click("Retry") + if retries != 1 { + t.Fatalf("retries %d after the limit lifted", retries) + } +} + +func TestErrorMessageNeedsText(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic without error text") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ErrorMessage(c, "", ErrorMessageOptions{}) + }, 100, 100) +} + +func TestErrorMessageSnapshot(t *testing.T) { + snapshot(t, "error_message", 520, 150, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { ErrorMessage(c, errorMessageText, ErrorMessageOptions{}) }) + }) +} diff --git a/example_chatmsg_test.go b/example_chatmsg_test.go new file mode 100644 index 0000000..63b8ed2 --- /dev/null +++ b/example_chatmsg_test.go @@ -0,0 +1,222 @@ +package mujica_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func ExampleChatContainer() { + var chat mujica.MessageListState + msgs := []string{"Hello", "Bonjour"} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{ + List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }}, + Input: func() { ui.Text(c, "Composer goes here") }, + }, func(i int) { ui.Text(c, msgs[i]) }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMessageList() { + var list mujica.MessageListState + ids := []string{"m1", "m2", "m3"} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{ + ID: func(i int) string { return ids[i] }, + }, func(i int) { ui.Text(c, "Message "+ids[i]) }) + fmt.Println("unread", r.Unread()) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMessageBubble() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() { + ui.Text(c, "The ball begins at nine.") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMessageHeader() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStreamingText() { + reply := "The decree reads" + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMarkdownView() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { + fmt.Println("open", url) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCodeBlock() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTableBlock() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() { + fmt.Println("copied as TSV") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImageGrid() { + pic := ui.MustParseSVG([]byte(``)) + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { + fmt.Println("reload", i) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFileMessage() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) + if r.Downloaded() { + fmt.Println("save treaty.pdf") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAudioMessage() { + pos := 5 * time.Second + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos}) + if d, ok := r.Seek(); ok { + pos = d + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLinkPreviewCard() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} + if mujica.LinkPreviewCard(c, p).Clicked() { + fmt.Println("open", p.URL) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleQuoteReply() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { + fmt.Println("jump to the quoted message") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMessageActions() { + rating := mujica.FeedbackNone + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok { + fmt.Println("action", a) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMessageEditor() { + draft := "Hello" + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() { + fmt.Println("new branch from", draft) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBranchNavigator() { + branch := 1 + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.BranchNavigator(c, &branch, 3).Changed() { + fmt.Println("show branch", branch) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleErrorMessage() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} + if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { + fmt.Println("send again") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleThinkingBlock() { + open := false + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { + ui.Text(c, "Weigh the guest list.") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleScrollToBottomButton() { + var list mujica.MessageListState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Box(c).Fill().Children(func() { + if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { + list.ScrollToEnd() + } + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTypingIndicator() { + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.TypingIndicator(c, "Herald") + } + ui.Render(view, 400, 300, 1) +} diff --git a/examples/gallery/pages_chatmsg.go b/examples/gallery/pages_chatmsg.go index 0172452..10d6b85 100644 --- a/examples/gallery/pages_chatmsg.go +++ b/examples/gallery/pages_chatmsg.go @@ -1,3 +1,261 @@ package main -var chatmsgPages = []page{} +import ( + "fmt" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +const chatmsgCategory = "Chat Messages" + +var chatmsgPages = append(append(chatmsgListPages, chatmsgBlockPages...), chatmsgActionPages...) + +// chatmsgReplies are the lines the demo assistant sends. +var chatmsgReplies = []string{ + "The ball begins at nine, in the Hall of Mirrors.", + "Guests arrive by the **east door**; carriages wait in the *lower court*.", + "Seating follows rank: dukes nearest the throne, then the envoys.", + "Shall I send the invitations tonight?", +} + +// chatmsgDemo is a chat whose messages the demo adds. +type chatmsgDemo struct { + ids []string + texts []string + list mujica.MessageListState + next int + sidebar bool + dated bool +} + +func newChatmsgDemo(n int) *chatmsgDemo { + d := &chatmsgDemo{sidebar: true, dated: true} + for range n { + d.add() + } + return d +} + +func (d *chatmsgDemo) add() { + d.ids = append(d.ids, fmt.Sprintf("msg-%d", d.next)) + d.texts = append(d.texts, chatmsgReplies[d.next%len(chatmsgReplies)]) + d.next++ +} + +// day puts three messages on each day, ending today. +func (d *chatmsgDemo) day(i int) time.Time { + last := (len(d.ids) - 1) / 3 + return time.Now().AddDate(0, 0, i/3-last) +} + +// row shows message i as a bubble, users and the assistant taking turns. +func (d *chatmsgDemo) row(c *ui.Context, i int) { + role := mujica.MessageAssistant + if i%2 == 0 { + role = mujica.MessageUser + } + mujica.MessageBubble(c, role, mujica.MessageBubbleOptions{}, func() { mujica.MarkdownView(c, d.texts[i]) }) +} + +// chatmsgControls adds messages to a demo chat and logs it. +func chatmsgControls(c *ui.Context, log func(string), d *chatmsgDemo) { + ui.Row(c).Gap(8).Children(func() { + if mujica.Button(c, "Add message", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + d.add() + log(fmt.Sprintf("message %s added", d.ids[len(d.ids)-1])) + } + if mujica.Button(c, "Add 10", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + for range 10 { + d.add() + } + log(fmt.Sprintf("10 messages added, %d in all", len(d.ids))) + } + if mujica.Button(c, "Jump to latest", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + d.list.ScrollToEnd() + log("scrolled to end") + } + }) +} + +var chatmsgListPages = []page{ + {ID: "201", Name: "ChatContainer", ZhName: "对话容器", Category: chatmsgCategory, + Keys: "Tab 在侧栏、消息与输入区之间移动;消息区滚轮与 PageUp/PageDown 滚动;上翻后出现回到最新按钮。", + Code: `mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{ + List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }}, + Input: func() { ui.Text(c, "Composer goes here") }, +}, func(i int) { ui.Text(c, msgs[i]) })`, + Demo: func(c *ui.Context, log func(string)) { + d := *ui.Local(c.Root(), "201", func() *chatmsgDemo { return newChatmsgDemo(6) }) + draft := ui.Local(c.Root(), "201.draft", func() string { return "" }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Sidebar": &d.sidebar}, "Sidebar") + chatmsgControls(c, log, d) + }) + inputSection(c, "Live", func() { + ui.Box(c).Height(420).Border(1, mujica.Tokens(c).Border).Clip().Children(func() { + opts := mujica.ChatContainerOptions{ + List: mujica.MessageListOptions{ID: func(i int) string { return d.ids[i] }}, + Input: func() { + r := mujica.TextInput(c, draft, mujica.TextInputOptions{Label: "Message", Placeholder: "Write to the herald"}) + if r.Submitted() && strings.TrimSpace(*draft) != "" { + d.add() + d.texts[len(d.texts)-1] = *draft + log("sent a message") + *draft = "" + } + }, + } + if d.sidebar { + opts.Sidebar = func() { + ui.Column(c).Padding(12).Gap(8).Children(func() { + for _, s := range []string{"The ball", "Seating", "Envoys"} { + ui.Text(c, s) + } + }) + } + } + r := mujica.ChatContainer(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) }) + if r.Unread() > 0 { + ui.Text(c, fmt.Sprintf("%d unread", r.Unread())).Absolute().Top(8).Right(12) + } + }) + }) + }}, + {ID: "202", Name: "MessageList", ZhName: "消息列表", Category: chatmsgCategory, + Keys: "滚轮与方向键滚动;上翻后停止跟随;Tab 到回到最新按钮,Enter 回到底部。", + Code: `r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{ + ID: func(i int) string { return ids[i] }, +}, func(i int) { ui.Text(c, "Message "+ids[i]) }) +fmt.Println("unread", r.Unread())`, + Demo: func(c *ui.Context, log func(string)) { + d := *ui.Local(c.Root(), "202", func() *chatmsgDemo { return newChatmsgDemo(30) }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Date separators": &d.dated}, "Date separators") + chatmsgControls(c, log, d) + ui.Row(c).Children(func() { + if mujica.Button(c, "Load 100,000", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + for len(d.ids) < 100000 { + d.add() + } + log("100,000 messages loaded") + } + }) + }) + inputSection(c, "Live", func() { + opts := mujica.MessageListOptions{ID: func(i int) string { return d.ids[i] }, Label: func(i int) string { return d.texts[i] }} + if d.dated { + opts.Date = d.day + } + ui.Column(c).Height(360).Border(1, mujica.Tokens(c).Border).Children(func() { + r := mujica.MessageList(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) }) + ui.Text(c, fmt.Sprintf("%d messages, %d unread, at end %v", len(d.ids), r.Unread(), d.list.AtEnd())).Padding(8) + }) + }) + }}, + {ID: "203", Name: "MessageBubble", ZhName: "消息气泡", Category: chatmsgCategory, + Keys: "气泡不获取焦点;内容中的链接与按钮按 Tab 顺序访问。", + Code: `mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() { + ui.Text(c, "The ball begins at nine.") +})`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + role mujica.MessageRole + avatar, named bool + width float64 + } + s := ui.Local(c.Root(), "203", func() state { return state{role: mujica.MessageAssistant, avatar: true, named: true, width: 360} }) + roles := []mujica.RadioItem[mujica.MessageRole]{{Value: mujica.MessageUser, Label: "User"}, {Value: mujica.MessageAssistant, Label: "Assistant"}, + {Value: mujica.MessageSystem, Label: "System"}, {Value: mujica.MessageError, Label: "Error"}} + inputSection(c, "Parameters", func() { + if mujica.RadioGroup(c, &s.role, roles, mujica.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() { + log(fmt.Sprintf("role %d", s.role)) + } + inputFlags(c, log, map[string]*bool{"Avatar": &s.avatar, "Name": &s.named}, "Avatar", "Name") + if mujica.NumberInput(c, &s.width, mujica.NumberInputOptions{Min: 160, Max: 640, Step: 40, Label: "MaxWidth"}).Changed() { + log(fmt.Sprintf("MaxWidth %v", s.width)) + } + }) + inputSection(c, "Live", func() { + opts := mujica.MessageBubbleOptions{MaxWidth: float32(s.width)} + if s.named { + opts.Name = "Herald" + } + if s.avatar { + opts.Avatar = func() { mujica.Avatar(c, "Herald", mujica.AvatarOptions{}) } + } + mujica.MessageBubble(c, s.role, opts, func() { ui.Text(c, chatmsgReplies[0]+" "+chatmsgReplies[2]) }) + }) + inputSection(c, "States", func() { + for _, r := range roles { + mujica.MessageBubble(c, r.Value, mujica.MessageBubbleOptions{Name: r.Label}, func() { ui.Text(c, r.Label+" message") }) + } + }) + }}, + {ID: "204", Name: "MessageHeader", ZhName: "消息头", Category: chatmsgCategory, Keys: "不获取焦点。", + Code: `mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent})`, + Demo: func(c *ui.Context, log func(string)) { + status := ui.Local(c.Root(), "204", func() mujica.MessageStatus { return mujica.MessageSent }) + items := []mujica.RadioItem[mujica.MessageStatus]{{Value: mujica.MessageNoStatus, Label: "None"}, {Value: mujica.MessageSending, Label: "Sending"}, + {Value: mujica.MessageStreaming, Label: "Streaming"}, {Value: mujica.MessageSent, Label: "Sent"}, {Value: mujica.MessageFailed, Label: "Failed"}} + inputSection(c, "Parameters", func() { + if mujica.RadioGroup(c, status, items, mujica.RadioGroupOptions{Horizontal: true, Label: "Status"}).Changed() { + log(fmt.Sprintf("status %d", *status)) + } + }) + inputSection(c, "Live", func() { + mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: *status}) + }) + inputSection(c, "States", func() { + for _, it := range items[1:] { + mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: it.Value}) + } + }) + }}, + {ID: "205", Name: "StreamingText", ZhName: "流式文本", Category: chatmsgCategory, Keys: "不获取焦点;减少动画时直接显示全部文本。", + Code: `mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true})`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + text string + smooth, streaming, auto bool + rate float64 + at time.Time + } + s := ui.Local(c.Root(), "205", func() state { return state{smooth: true, streaming: true, rate: 80} }) + const full = "By order of the Queen, the ball begins at nine. Guests arrive by the east door, and carriages wait in the lower court until midnight." + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Smooth": &s.smooth, "Streaming cursor": &s.streaming, "Stream": &s.auto}, "Smooth", "Streaming cursor", "Stream") + if mujica.NumberInput(c, &s.rate, mujica.NumberInputOptions{Min: 10, Max: 400, Step: 10, Label: "Rate"}).Changed() { + log(fmt.Sprintf("Rate %v", s.rate)) + } + ui.Row(c).Children(func() { + if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.text = "" + log("stream restarted") + } + }) + }) + // The demo's own source appends a word every 150 ms while streaming. + if s.auto && len(s.text) < len(full) && c.Now().Sub(s.at) > 150*time.Millisecond { + rest := full[len(s.text):] + n := strings.IndexByte(rest[1:], ' ') + 2 + if n <= 1 { + n = len(rest) + } + s.text += rest[:n] + s.at = c.Now() + if len(s.text) == len(full) { + log("stream finished") + } + } + if s.auto && len(s.text) < len(full) { + c.After(150 * time.Millisecond) + } + inputSection(c, "Live", func() { + mujica.StreamingText(c, s.text, mujica.StreamingTextOptions{Streaming: s.streaming && len(s.text) < len(full), Smooth: s.smooth, Rate: s.rate}) + }) + }}, +} diff --git a/examples/gallery/pages_chatmsg_actions.go b/examples/gallery/pages_chatmsg_actions.go new file mode 100644 index 0000000..4d84d48 --- /dev/null +++ b/examples/gallery/pages_chatmsg_actions.go @@ -0,0 +1,292 @@ +package main + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +var chatmsgActionPages = []page{ + {ID: "211", Name: "AudioMessage", ZhName: "语音消息", Category: chatmsgCategory, Keys: "Tab 到播放按钮与波形;波形上 Left/Right 跳转,Home、End 到首尾。", + Code: `r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos}) +if d, ok := r.Seek(); ok { + pos = d +}`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + pos time.Duration + playing bool + at time.Time + } + s := ui.Local(c.Root(), "211", func() state { return state{pos: 12 * time.Second} }) + const length = 47 * time.Second + // The demo plays: it moves the position while playing. + if s.playing { + s.pos += c.Now().Sub(s.at) + if s.pos >= length { + s.pos, s.playing = length, false + log("playback ended") + } + c.After(100 * time.Millisecond) + } + s.at = c.Now() + inputSection(c, "Live", func() { + r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: length, Position: s.pos, Playing: s.playing, Waveform: chatmsgWave}) + if r.Toggled() { + s.playing = !s.playing + if s.pos >= length { + s.pos = 0 + } + log(fmt.Sprintf("playing %v", s.playing)) + } + if d, ok := r.Seek(); ok { + s.pos = d + log(fmt.Sprintf("seek to %v", d.Round(time.Second))) + } + }) + inputSection(c, "States", func() { + ui.Column(c).Disabled(true).Children(func() { + mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: length, Waveform: chatmsgWave}) + }) + }) + }}, + {ID: "212", Name: "LinkPreviewCard", ZhName: "链接预览卡片", Category: chatmsgCategory, Keys: "Tab 聚焦卡片,Enter 或 Space 打开。", + Code: `p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} +if mujica.LinkPreviewCard(c, p).Clicked() { + fmt.Println("open", p.URL) +}`, + Demo: func(c *ui.Context, log func(string)) { + thumb := ui.Local(c.Root(), "212", func() bool { return true }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Thumbnail": thumb}, "Thumbnail") + }) + p := mujica.LinkPreview{URL: "https://example.com/versailles", Title: "The Palace of Versailles", Site: "example.com", + Description: "A royal château in the Île-de-France region, seat of the French court from 1682 until the Revolution."} + if *thumb { + p.Thumbnail = chatmsgPicture("#AD9164") + } + inputSection(c, "Live", func() { + if mujica.LinkPreviewCard(c, p).Clicked() { + log("open " + p.URL) + } + if mujica.LinkPreviewCard(c, mujica.LinkPreview{URL: "https://example.com/hall", Title: "Hall of Mirrors"}).Clicked() { + log("open https://example.com/hall") + } + }) + }}, + {ID: "213", Name: "QuoteReply", ZhName: "引用回复", Category: chatmsgCategory, Keys: "Tab 聚焦,Enter 或 Space 跳转。", + Code: `if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { + fmt.Println("jump to the quoted message") +}`, + Demo: func(c *ui.Context, log func(string)) { + inputSection(c, "Live", func() { + for _, q := range [][2]string{{"Marie", "Let them eat cake."}, {"Herald", chatmsgMarkdown}} { + if mujica.QuoteReply(c, q[0], q[1]).Clicked() { + log("jump to " + q[0] + ": " + chatmsgSnippet(q[1], 24)) + } + } + }) + }}, + {ID: "214", Name: "MessageActions", ZhName: "消息操作", Category: chatmsgCategory, Keys: "Tab 进入工具栏,Left/Right 移动,Enter 或 Space 触发。", + Code: `if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok { + fmt.Println("action", a) +}`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + role mujica.MessageRole + rating mujica.MessageFeedback + rated bool + } + s := ui.Local(c.Root(), "214", func() state { return state{role: mujica.MessageAssistant, rated: true} }) + names := map[mujica.MessageAction]string{mujica.ActionCopy: "copy", mujica.ActionRegenerate: "regenerate", mujica.ActionEdit: "edit", + mujica.ActionLike: "like", mujica.ActionDislike: "dislike", mujica.ActionSpeak: "speak", mujica.ActionShare: "share"} + inputSection(c, "Parameters", func() { + roles := []mujica.RadioItem[mujica.MessageRole]{{Value: mujica.MessageUser, Label: "User"}, {Value: mujica.MessageAssistant, Label: "Assistant"}, + {Value: mujica.MessageSystem, Label: "System"}, {Value: mujica.MessageError, Label: "Error"}} + if mujica.RadioGroup(c, &s.role, roles, mujica.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() { + log(fmt.Sprintf("role %d", s.role)) + } + inputFlags(c, log, map[string]*bool{"Rating": &s.rated}, "Rating") + }) + inputSection(c, "Live", func() { + opts := mujica.MessageActionsOptions{} + if s.rated { + opts.Feedback = &s.rating + } + if a, ok := mujica.MessageActions(c, s.role, opts).Action(); ok { + log(fmt.Sprintf("action %s, rating %d", names[a], s.rating)) + } + }) + }}, + {ID: "215", Name: "MessageEditor", ZhName: "消息编辑器", Category: chatmsgCategory, Keys: "Cmd/Ctrl+Enter 保存,Escape 取消。", + Code: `if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() { + fmt.Println("new branch from", draft) +}`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + original, draft string + editing bool + branches []string + index int + } + s := ui.Local(c.Root(), "215", func() state { + return state{original: "Seat the ambassador.", branches: []string{"Seat the ambassador."}} + }) + inputSection(c, "Live", func() { + if !s.editing { + mujica.MessageBubble(c, mujica.MessageUser, mujica.MessageBubbleOptions{}, func() { ui.Text(c, s.branches[s.index]) }) + ui.Row(c).Gap(8).Justify(ui.End).Children(func() { + if mujica.BranchNavigator(c, &s.index, len(s.branches)).Changed() { + log(fmt.Sprintf("branch %d", s.index+1)) + } + if mujica.Button(c, "Edit", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + s.editing, s.draft = true, s.branches[s.index] + log("editing") + } + }) + return + } + r := mujica.MessageEditor(c, &s.draft, mujica.MessageEditorOptions{Original: s.branches[s.index]}) + if r.Saved() { + s.branches = append(s.branches, s.draft) + s.index, s.editing = len(s.branches)-1, false + log(fmt.Sprintf("saved as branch %d", len(s.branches))) + } + if r.Canceled() { + s.editing = false + log("edit canceled") + } + }) + }}, + {ID: "216", Name: "BranchNavigator", ZhName: "分支切换", Category: chatmsgCategory, Keys: "Tab 聚焦,Left/Right 切换,Home、End 到首末。", + Code: `if mujica.BranchNavigator(c, &branch, 3).Changed() { + fmt.Println("show branch", branch) +}`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + index int + count float64 + disabled bool + } + s := ui.Local(c.Root(), "216", func() state { return state{index: 1, count: 3} }) + inputSection(c, "Parameters", func() { + if mujica.NumberInput(c, &s.count, mujica.NumberInputOptions{Min: 1, Max: 9, StepperOnly: true, Label: "Branches"}).Changed() { + s.index = min(s.index, int(s.count)-1) + log(fmt.Sprintf("%v branches", s.count)) + } + inputFlags(c, log, map[string]*bool{"Disabled": &s.disabled}, "Disabled") + }) + inputSection(c, "Live", func() { + ui.Row(c).Disabled(s.disabled).Children(func() { + if mujica.BranchNavigator(c, &s.index, int(s.count)).Changed() { + log(fmt.Sprintf("branch %d of %v", s.index+1, s.count)) + } + }) + }) + }}, + {ID: "217", Name: "ErrorMessage", ZhName: "错误消息", Category: chatmsgCategory, Keys: "Tab 到错误文本(可选择复制)与重试按钮。", + Code: `opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} +if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { + fmt.Println("send again") +}`, + Demo: func(c *ui.Context, log func(string)) { + until := ui.Local(c.Root(), "217", func() time.Time { return time.Time{} }) + inputSection(c, "Parameters", func() { + ui.Row(c).Children(func() { + if mujica.Button(c, "Rate limit for 10 s", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + *until = c.Now().Add(10 * time.Second) + log("rate limited for 10 s") + } + }) + }) + inputSection(c, "Live", func() { + opts := mujica.ErrorMessageOptions{RetryAt: *until} + if mujica.ErrorMessage(c, `429 Too Many Requests: {"type":"rate_limit_error","message":"Number of requests exceeded"}`, opts).Retried() { + log("retry") + } + if mujica.ErrorMessage(c, "dial tcp 10.0.0.7:443: connection refused", mujica.ErrorMessageOptions{Title: "Could not reach the server"}).Retried() { + log("retry connection") + } + }) + }}, + {ID: "218", Name: "ThinkingBlock", ZhName: "思考过程", Category: chatmsgCategory, Keys: "Tab 聚焦标题,Enter 或 Space 展开与折叠。", + Code: `mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { + ui.Text(c, "Weigh the guest list.") +})`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + open, thinking bool + started time.Time + elapsed time.Duration + } + s := ui.Local(c.Root(), "218", func() state { return state{elapsed: 14 * time.Second} }) + inputSection(c, "Parameters", func() { + label := "Start thinking" + if s.thinking { + label = "Finish thinking" + } + ui.Row(c).Children(func() { + if mujica.Button(c, label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if s.thinking { + s.elapsed = c.Now().Sub(s.started) + } else { + s.started = c.Now() + } + s.thinking = !s.thinking + log(fmt.Sprintf("thinking %v", s.thinking)) + } + }) + }) + inputSection(c, "Live", func() { + opts := mujica.ThinkingBlockOptions{Thinking: s.thinking, Started: s.started, Elapsed: s.elapsed} + if mujica.ThinkingBlock(c, &s.open, opts, func() { + ui.Text(c, "Forty guests, thirty-six seats. The envoys outrank the counts; four counts dine in the antechamber.") + }).Changed() { + log(fmt.Sprintf("open %v", s.open)) + } + }) + }}, + {ID: "219", Name: "ScrollToBottomButton", ZhName: "回到底部按钮", Category: chatmsgCategory, Keys: "Tab 聚焦,Enter 或 Space 回到底部。", + Code: `ui.Box(c).Fill().Children(func() { + if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { + list.ScrollToEnd() + } +})`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + away bool + unread float64 + } + s := ui.Local(c.Root(), "219", func() state { return state{away: true, unread: 3} }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Away from bottom": &s.away}, "Away from bottom") + if mujica.NumberInput(c, &s.unread, mujica.NumberInputOptions{Min: 0, Max: 99, Label: "Unread"}).Changed() { + log(fmt.Sprintf("unread %v", s.unread)) + } + }) + inputSection(c, "Live", func() { + ui.Box(c).Height(120).Background(mujica.Tokens(c).Surface).Border(1, mujica.Tokens(c).Border).Children(func() { + if mujica.ScrollToBottomButton(c, s.away, int(s.unread)).Clicked() { + s.away, s.unread = false, 0 + log("back to the bottom") + } + }) + }) + }}, + {ID: "220", Name: "TypingIndicator", ZhName: "正在输入", Category: chatmsgCategory, Keys: "不获取焦点;减少动画时显示静态文字。", + Code: `mujica.TypingIndicator(c, "Herald")`, + Demo: func(c *ui.Context, log func(string)) { + typing := ui.Local(c.Root(), "220", func() bool { return true }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Typing": typing}, "Typing") + }) + inputSection(c, "Live", func() { + if *typing { + mujica.TypingIndicator(c, "Herald") + } + }) + }}, +} diff --git a/examples/gallery/pages_chatmsg_blocks.go b/examples/gallery/pages_chatmsg_blocks.go new file mode 100644 index 0000000..3356e64 --- /dev/null +++ b/examples/gallery/pages_chatmsg_blocks.go @@ -0,0 +1,198 @@ +package main + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +const chatmsgMarkdown = "## The Decree\n\nBy order of **the Queen**, *all* guests use `the east door`. See [the protocol](https://example.com/protocol).\n\n" + + "1. Arrive at nine\n2. Bow to the throne\n - dukes first\n\n> Silence in the Hall of Mirrors.\n\n" + + "```python\ndef bow(guest):\n return f\"{guest} bows\" # deeply\n```\n\n| Guest | Seat |\n|---|--:|\n| Marie | 1 |\n| Axel | 14 |\n\n---\n\nVive la reine." + +// chatmsgPicture is a flat colored picture for image demos. +func chatmsgPicture(color string) ui.ImageSource { + return ui.MustParseSVG([]byte(fmt.Sprintf(``, color))) +} + +var chatmsgColors = []string{"#7D2034", "#AD9164", "#355E80", "#326247", "#805714", "#6A5B55", "#9C3C53", "#D2C3AF", "#211C1D"} + +var chatmsgBlockPages = []page{ + {ID: "206", Name: "MarkdownView", ZhName: "Markdown 视图", Category: chatmsgCategory, Keys: "Tab 到链接,Enter 激活;代码块与表格按钮按 Tab 顺序访问。", + Code: `if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { + fmt.Println("open", url) +}`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + src string + stream bool + at time.Time + } + s := ui.Local(c.Root(), "206", func() state { return state{src: chatmsgMarkdown} }) + inputSection(c, "Parameters", func() { + ui.Row(c).Children(func() { + if mujica.Button(c, "Stream it again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + s.src, s.stream = "", true + log("streaming started") + } + }) + mujica.TextArea(c, &s.src, mujica.TextAreaOptions{Label: "Markdown source", MaxHeight: 160}) + }) + if s.stream && len(s.src) < len(chatmsgMarkdown) && c.Now().Sub(s.at) > 40*time.Millisecond { + s.src = chatmsgMarkdown[:min(len(chatmsgMarkdown), len(s.src)+6)] + s.at = c.Now() + if len(s.src) == len(chatmsgMarkdown) { + s.stream = false + log("streaming finished; finished blocks were never parsed again") + } + } + if s.stream { + c.After(40 * time.Millisecond) + } + inputSection(c, "Live", func() { + if url, ok := mujica.MarkdownView(c, s.src).Link(); ok { + log("link " + url) + } + }) + }}, + {ID: "207", Name: "CodeBlock", ZhName: "代码块", Category: chatmsgCategory, Keys: "Tab 到换行、复制、折叠按钮;Space 或 Enter 触发。", + Code: `mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true})`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + lang string + numbers, wrap bool + } + s := ui.Local(c.Root(), "207", func() state { return state{lang: "go", numbers: true} }) + samples := map[string]string{ + "go": "// greet the court\nfunc greet(name string) int {\n\treturn len(\"Your Majesty, \" + name + \", the ball awaits you in the Hall of Mirrors\") + 42\n}", + "python": "def bow(guest):\n # deeply, always\n return f\"{guest} bows\" * 3", + "sql": "SELECT name, seat FROM guests WHERE rank > 2 ORDER BY seat LIMIT 10; -- the envoys", + } + inputSection(c, "Parameters", func() { + langs := []mujica.RadioItem[string]{{Value: "go", Label: "Go"}, {Value: "python", Label: "Python"}, {Value: "sql", Label: "SQL"}} + if mujica.RadioGroup(c, &s.lang, langs, mujica.RadioGroupOptions{Horizontal: true, Label: "Language"}).Changed() { + log("language " + s.lang) + } + inputFlags(c, log, map[string]*bool{"LineNumbers": &s.numbers, "Wrap at first": &s.wrap}, "LineNumbers", "Wrap at first") + }) + inputSection(c, "Live", func() { + ui.Column(c).Key(s.lang + fmt.Sprint(s.wrap)).Children(func() { + if mujica.CodeBlock(c, samples[s.lang], mujica.CodeBlockOptions{Language: s.lang, LineNumbers: s.numbers, Wrap: s.wrap}).Copied() { + log("code copied") + } + }) + }) + inputSection(c, "States", func() { + mujica.CodeBlock(c, samples["go"], mujica.CodeBlockOptions{Language: "go", Collapsed: true}) + }) + }}, + {ID: "208", Name: "TableBlock", ZhName: "表格块", Category: chatmsgCategory, Keys: "Tab 到复制按钮,Enter 或 Space 复制为 TSV。", + Code: `if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() { + fmt.Println("copied as TSV") +}`, + Demo: func(c *ui.Context, log func(string)) { + wide := ui.Local(c.Root(), "208", func() bool { return false }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Wide table": wide}, "Wide table") + }) + header := []string{"Guest", "Title", "Seat"} + rows := [][]string{{"Marie", "Queen of France", "1"}, {"Axel", "Count of Fersen", "14"}} + if *wide { + header = append(header, "Arrival", "Carriage", "Retinue", "Notes") + for i := range rows { + rows[i] = append(rows[i], "21:00", "Gilded coach", "Twelve", "Seated near the orchestra, away from the draughts") + } + } + inputSection(c, "Live", func() { + ui.Column(c).MaxWidth(520).Children(func() { + if mujica.TableBlock(c, header, rows, mujica.TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}).Copied() { + log("table copied as TSV") + } + }) + }) + }}, + {ID: "209", Name: "ImageGrid", ZhName: "图片网格", Category: chatmsgCategory, Keys: "Tab 聚焦图片,Enter 打开查看器;查看器内 Left/Right 切换,Escape 关闭。", + Code: `if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { + fmt.Println("reload", i) +}`, + Demo: func(c *ui.Context, log func(string)) { + type state struct { + n float64 + loading, fail bool + } + s := ui.Local(c.Root(), "209", func() state { return state{n: 5} }) + inputSection(c, "Parameters", func() { + if mujica.NumberInput(c, &s.n, mujica.NumberInputOptions{Min: 1, Max: 9, Label: "Images", StepperOnly: true}).Changed() { + log(fmt.Sprintf("%v images", s.n)) + } + inputFlags(c, log, map[string]*bool{"Last loading": &s.loading, "Second failed": &s.fail}, "Last loading", "Second failed") + }) + imgs := make([]mujica.GridImage, int(s.n)) + for i := range imgs { + imgs[i] = mujica.GridImage{Source: chatmsgPicture(chatmsgColors[i]), Alt: fmt.Sprintf("Portrait %d", i+1)} + } + if s.loading { + imgs[len(imgs)-1] = mujica.GridImage{Alt: "Loading portrait", Loading: true} + } + if s.fail && len(imgs) > 1 { + imgs[1] = mujica.GridImage{Alt: "Lost portrait", Err: errors.New("404")} + } + inputSection(c, "Live", func() { + if i, ok := mujica.ImageGrid(c, imgs).Retried(); ok { + s.fail = false + log(fmt.Sprintf("retry image %d", i)) + } + }) + }}, + {ID: "210", Name: "FileMessage", ZhName: "文件消息", Category: chatmsgCategory, Keys: "Tab 到文件与下载按钮,Enter 或 Space 触发。", + Code: `r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) +if r.Downloaded() { + fmt.Println("save treaty.pdf") +}`, + Demo: func(c *ui.Context, log func(string)) { + disabled := ui.Local(c.Root(), "210", func() bool { return false }) + inputSection(c, "Parameters", func() { + inputFlags(c, log, map[string]*bool{"Disabled": disabled}, "Disabled") + }) + inputSection(c, "Live", func() { + ui.Column(c).Gap(8).Disabled(*disabled).Children(func() { + for _, f := range []mujica.FileMessageOptions{{Name: "treaty.pdf", Size: 2621440}, {Name: "portrait.png", Size: 812000}, + {Name: "ledger.zip", Size: 73400320}, {Name: "decree.go", Size: 1840}, {Name: "notes", Size: 12}} { + r := mujica.FileMessage(c, f) + if r.Opened() { + log("open " + f.Name) + } + if r.Downloaded() { + log("download " + f.Name) + } + } + }) + }) + }}, +} + +// chatmsgWave is a fixed waveform for the voice demo. +var chatmsgWave = func() []float32 { + w := make([]float32, 48) + for i := range w { + w[i] = float32(0.25 + 0.7*float64((i*37)%11)/10) + if w[i] > 1 { + w[i] = 1 + } + } + return w +}() + +// chatmsgSnippet trims s to n runes for logs. +func chatmsgSnippet(s string, n int) string { + r := []rune(strings.TrimSpace(s)) + if len(r) > n { + return string(r[:n]) + "…" + } + return string(r) +} diff --git a/examples/gallery/pages_chatmsg_test.go b/examples/gallery/pages_chatmsg_test.go new file mode 100644 index 0000000..7a2bf62 --- /dev/null +++ b/examples/gallery/pages_chatmsg_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "image/png" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +// chatmsgDemoTester runs the demo of page id and returns its tester and event log. +func chatmsgDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) { + t.Helper() + i := slices.IndexFunc(chatmsgPages, func(p page) bool { return p.ID == id }) + if i < 0 { + t.Fatalf("no page %s", id) + } + events := &[]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() { + chatmsgPages[i].Demo(c, func(s string) { *events = append(*events, s) }) + }) + }, 900, 800) + tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) + tt.Frame() + return tt, events +} + +func TestChatmsgDemosRender(t *testing.T) { + if len(chatmsgPages) != 20 { + t.Fatalf("%d chat message pages, want 20", len(chatmsgPages)) + } + dir := os.Getenv("CHATMSG_DEMO_DIR") + for _, p := range chatmsgPages { + for _, m := range []mujica.Mode{mujica.Light, mujica.Dark} { + tt, _ := chatmsgDemoTester(t, p.ID, m) + if len(tt.Texts()) == 0 { + t.Errorf("%s: demo shows nothing", p.ID) + } + if dir == "" { + continue + } + f, err := os.Create(filepath.Join(dir, p.ID+"-"+m.String()+".png")) + if err != nil { + t.Fatal(err) + } + if err := png.Encode(f, tt.Image()); err != nil { + t.Fatal(err) + } + f.Close() + } + } +} + +func TestChatmsgDemosLogEvents(t *testing.T) { + for id, click := range map[string]string{ + "202": "Add message", "207": "Copy code", "214": "Regenerate", "216": "Next branch", "217": "Retry", + } { + tt, events := chatmsgDemoTester(t, id, mujica.Light) + if err := tt.Click(click); err != nil { + t.Fatalf("%s: %v", id, err) + } + if len(*events) == 0 { + t.Errorf("%s: clicking %q logged nothing", id, click) + } + } +} diff --git a/file_message.go b/file_message.go new file mode 100644 index 0000000..451d76c --- /dev/null +++ b/file_message.go @@ -0,0 +1,123 @@ +package mujica + +import ( + "fmt" + "log/slog" + "path" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// FileMessageOptions describe a file sent in a chat. +type FileMessageOptions struct { + // Name is the file name; required. + Name string + // Size is the length in bytes. + Size int64 + // Type is the MIME type; empty guesses from the name's extension. + Type string +} + +// FileMessageResult is a FileMessage's element and this frame's actions. +type FileMessageResult struct { + Element *ui.Element + opened, downloaded bool +} + +// Opened reports a click or Enter on the file. +func (r FileMessageResult) Opened() bool { return r.opened } + +// Downloaded reports a click on the download button. +func (r FileMessageResult) Downloaded() bool { return r.downloaded } + +// FileMessage shows a file's type icon, name and size, with open and download actions. +func FileMessage(c *ui.Context, opts FileMessageOptions) FileMessageResult { + if opts.Name == "" { + panic("mujica: FileMessage needs a Name") + } + if opts.Size < 0 { + panic("mujica: FileMessage Size must not be negative") + } + k := Tokens(c) + u := Density(c).Unit() + size := fileMessageSize(c, opts.Size) + var r FileMessageResult + r.Element = ui.Row(c).Gap(u).AlignItems(ui.Center).Padding(u, u, u, u*2).MaxWidth(360). + Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border) + disabled := r.Element.IsDisabled() + r.Element.Children(func() { + open := ui.ButtonBase(c).Grow(1).MinWidth(0).Gap(u * 3).Padding(u).Justify(ui.Start).Radius(theme.ControlRadius). + Label(fmt.Sprintf(msg(c, "file_message.open"), opts.Name, size)) + internal.Ring(open, k.Focus, theme.ControlRadius) + if open.Hovered() { + open.Background(k.SurfaceHover) + } + open.Children(func() { + ui.Icon(c, icons.Must(fileMessageIcon(opts.Name, opts.Type))).Size(24, 24).Shrink(0).TextColor(k.AccentText) + ui.Column(c).MinWidth(0).Grow(1).Children(func() { + ui.Text(c, opts.Name).SingleLine().FontWeight(600) + ui.Text(c, size).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + }) + }) + if open.Clicked() && !disabled { + r.opened = true + slog.Debug("mujica: file_message open") + } + if iconAction(c, icons.Must("download"), fmt.Sprintf(msg(c, "file_message.download"), opts.Name)).Clicked() && !disabled { + r.downloaded = true + slog.Debug("mujica: file_message download") + } + }) + return r +} + +// fileMessageSize formats n bytes in binary units. +func fileMessageSize(c *ui.Context, n int64) string { + units := []string{"B", "KB", "MB", "GB", "TB"} + v, i := float64(n), 0 + for v >= 1024 && i < len(units)-1 { + v /= 1024 + i++ + } + if i == 0 { + return fmt.Sprintf(msg(c, "file_message.bytes"), n) + } + return fmt.Sprintf("%.1f %s", v, units[i]) +} + +// fileMessageKinds maps extensions to type icons. +var fileMessageKinds = func() map[string]string { + m := map[string]string{} + for icon, exts := range map[string]string{ + "image": ".png .jpg .jpeg .gif .webp .svg", + "file-archive": ".zip .tar .gz .tgz .7z .rar", + "file-code": ".go .js .ts .py .json .sh .sql .html .css .rs .java .c .h", + "file-text": ".txt .md .pdf .doc .docx .rtf", + } { + for _, e := range strings.Fields(exts) { + m[e] = icon + } + } + return m +}() + +// fileMessageIcon picks a type icon from the MIME type, else the extension. +func fileMessageIcon(name, mime string) string { + switch { + case strings.HasPrefix(mime, "image/"): + return "image" + case strings.Contains(mime, "zip"): + return "file-archive" + case strings.HasPrefix(mime, "text/"): + return "file-text" + } + if icon, ok := fileMessageKinds[strings.ToLower(path.Ext(name))]; ok { + return icon + } + return "file" +} diff --git a/file_message_test.go b/file_message_test.go new file mode 100644 index 0000000..3ec84cf --- /dev/null +++ b/file_message_test.go @@ -0,0 +1,82 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestFileMessageOpenDownloadAndSize(t *testing.T) { + opened, downloaded, off := 0, 0, false + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Disabled(off).Children(func() { + r := FileMessage(c, FileMessageOptions{Name: "treaty.pdf", Size: 2_621_440}) + if r.Opened() { + opened++ + } + if r.Downloaded() { + downloaded++ + } + }) + }, 400, 200) + if !tt.HasText("2.5 MB") || !tt.HasText("treaty.pdf") { + t.Fatalf("texts %q", tt.Texts()) + } + _ = tt.Click("Open treaty.pdf, 2.5 MB") + _ = tt.Click("Download treaty.pdf") + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if opened != 2 || downloaded != 1 { + t.Fatalf("opened %d downloaded %d", opened, downloaded) + } + off = true + _ = tt.Click("Open treaty.pdf, 2.5 MB") + _ = tt.Click("Download treaty.pdf") + if opened != 2 || downloaded != 1 { + t.Fatalf("disabled file acted: opened %d downloaded %d", opened, downloaded) + } +} + +func TestFileMessageIconsAndSizes(t *testing.T) { + for name, want := range map[string]string{"a.png": "image", "b.zip": "file-archive", "c.go": "file-code", "d.md": "file-text", "e.bin": "file", "f.c": "file-code", "g.s": "file"} { + if got := fileMessageIcon(name, ""); got != want { + t.Errorf("%s: icon %s, want %s", name, got, want) + } + } + if fileMessageIcon("x", "image/heic") != "image" { + t.Error("MIME type ignored") + } + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + for _, n := range []int64{0, 1023, 1024, 5 << 30} { + ui.Text(c, fileMessageSize(c, n)) + } + }, 200, 200) + for _, s := range []string{"0 B", "1023 B", "1.0 KB", "5.0 GB"} { + if !tt.HasText(s) { + t.Errorf("missing size %q in %q", s, tt.Texts()) + } + } +} + +func TestFileMessageNeedsName(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic without a name") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + FileMessage(c, FileMessageOptions{}) + }, 100, 100) +} + +func TestFileMessageSnapshot(t *testing.T) { + snapshot(t, "file_message", 400, 160, func(c *ui.Context) { + ui.Column(c).Padding(16).Gap(8).Children(func() { + FileMessage(c, FileMessageOptions{Name: "seating-plan.xlsx", Size: 48_213}) + FileMessage(c, FileMessageOptions{Name: "portrait.png", Size: 3_400_000}) + }) + }) +} diff --git a/icons/svg/brain.svg b/icons/svg/brain.svg new file mode 100644 index 0000000..5a030d0 --- /dev/null +++ b/icons/svg/brain.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/icons/svg/download.svg b/icons/svg/download.svg new file mode 100644 index 0000000..4da2f2a --- /dev/null +++ b/icons/svg/download.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/icons/svg/file-archive.svg b/icons/svg/file-archive.svg new file mode 100644 index 0000000..c9ea9ef --- /dev/null +++ b/icons/svg/file-archive.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/icons/svg/file-code.svg b/icons/svg/file-code.svg new file mode 100644 index 0000000..34c065e --- /dev/null +++ b/icons/svg/file-code.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/icons/svg/pause.svg b/icons/svg/pause.svg new file mode 100644 index 0000000..12b6f06 --- /dev/null +++ b/icons/svg/pause.svg @@ -0,0 +1,15 @@ + + + + diff --git a/icons/svg/pencil.svg b/icons/svg/pencil.svg new file mode 100644 index 0000000..f0e3f38 --- /dev/null +++ b/icons/svg/pencil.svg @@ -0,0 +1,15 @@ + + + + diff --git a/icons/svg/play.svg b/icons/svg/play.svg new file mode 100644 index 0000000..6eb03bb --- /dev/null +++ b/icons/svg/play.svg @@ -0,0 +1,14 @@ + + + diff --git a/icons/svg/share-2.svg b/icons/svg/share-2.svg new file mode 100644 index 0000000..7825a02 --- /dev/null +++ b/icons/svg/share-2.svg @@ -0,0 +1,18 @@ + + + + + + + diff --git a/icons/svg/thumbs-down.svg b/icons/svg/thumbs-down.svg new file mode 100644 index 0000000..6e74b23 --- /dev/null +++ b/icons/svg/thumbs-down.svg @@ -0,0 +1,15 @@ + + + + diff --git a/icons/svg/thumbs-up.svg b/icons/svg/thumbs-up.svg new file mode 100644 index 0000000..30dca62 --- /dev/null +++ b/icons/svg/thumbs-up.svg @@ -0,0 +1,15 @@ + + + + diff --git a/icons/svg/volume-2.svg b/icons/svg/volume-2.svg new file mode 100644 index 0000000..d403d95 --- /dev/null +++ b/icons/svg/volume-2.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/icons/svg/wrap-text.svg b/icons/svg/wrap-text.svg new file mode 100644 index 0000000..d5ce615 --- /dev/null +++ b/icons/svg/wrap-text.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/image_grid.go b/image_grid.go new file mode 100644 index 0000000..8161285 --- /dev/null +++ b/image_grid.go @@ -0,0 +1,103 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// GridImage is one picture of an ImageGrid; the caller loads it. +type GridImage struct { + Source ui.ImageSource + Alt string + // Loading and Err show the caller's load in progress or failure. + Loading bool + Err error +} + +// ImageGridResult is an ImageGrid's element and a retry the user asked for. +type ImageGridResult struct { + Element *ui.Element + retry int +} + +// Retried returns the index of the failed picture whose retry was pressed this frame. +func (r ImageGridResult) Retried() (int, bool) { return r.retry, r.retry >= 0 } + +// imageGridState is the viewer opened from the grid. +type imageGridState struct { + open bool + index int +} + +// ImageGrid lays out 1 to 9 pictures; a click on a loaded one opens the ImageViewer. +func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult { + n := len(images) + if n < 1 || n > 9 { + panic("mujica: ImageGrid shows 1 to 9 images") + } + k := Tokens(c) + u := Density(c).Unit() + cols := 3 + switch n { + case 1: + cols = 1 + case 2, 4: + cols = 2 + } + cell := float32(120) + if n == 1 { + cell = 280 + } + r := ImageGridResult{retry: -1} + r.Element = ui.Grid(c).Columns(cols).Gap(u).AlignSelf(ui.Start) + st := ui.Local(r.Element, "viewer", func() imageGridState { return imageGridState{} }) + var viewable []ViewerImage + var at []int + for i, im := range images { + if im.Source != nil && !im.Loading && im.Err == nil { + at = append(at, i) + viewable = append(viewable, ViewerImage{Source: im.Source, Alt: im.Alt}) + } + } + disabled := r.Element.IsDisabled() + r.Element.Children(func() { + for i, im := range images { + loaded := im.Source != nil && !im.Loading && im.Err == nil + // Only a loaded picture is a button; a failed one keeps its retry. + var b *ui.Element + if loaded { + b = ui.ButtonBase(c).Label(im.Alt) + internal.Ring(b, k.Focus, theme.ControlRadius) + } else { + b = ui.Box(c) + } + b.Size(cell, cell).Radius(theme.ControlRadius).Clip().Children(func() { + v := Image(c, im.Source, ImageOptions{Width: cell, Height: cell, Crop: true, Alt: im.Alt, Loading: im.Loading, Err: im.Err}) + if v.Retry && !disabled { + r.retry = i + slog.Debug("mujica: image_grid retry", "index", i) + } + }) + if loaded && b.Clicked() && !disabled { + for vi, ii := range at { + if ii == i { + st.open, st.index = true, vi + } + } + slog.Debug("mujica: image_grid open", "index", i) + } + } + }) + if disabled { + st.open = false + } + if st.open && len(viewable) > 0 { + st.index = min(st.index, len(viewable)-1) + ImageViewer(c, &st.open, &st.index, viewable) + } + return r +} diff --git a/image_grid_test.go b/image_grid_test.go new file mode 100644 index 0000000..e5e65fd --- /dev/null +++ b/image_grid_test.go @@ -0,0 +1,95 @@ +package mujica + +import ( + "errors" + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +// imageGridPicture is a plain colored picture standing in for a photo. +func imageGridPicture(color string) ui.ImageSource { + return ui.MustParseSVG([]byte(fmt.Sprintf(``, color))) +} + +func imageGridSet(n int) []GridImage { + colors := []string{"#7D2034", "#AD9164", "#355E80", "#326247", "#805714", "#6A5B55", "#9C3C53", "#D2C3AF", "#211C1D"} + out := make([]GridImage, n) + for i := range out { + out[i] = GridImage{Source: imageGridPicture(colors[i]), Alt: fmt.Sprintf("Photo %d", i+1)} + } + return out +} + +func TestImageGridLayoutsByCount(t *testing.T) { + for n, cols := range map[int]int{1: 1, 2: 2, 3: 3, 4: 2, 5: 3, 9: 3} { + var xs map[float32]bool + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ImageGrid(c, imageGridSet(n)) + }, 900, 900) + tt.Frame() + xs = map[float32]bool{} + for i := range n { + r, ok := tt.Find(fmt.Sprintf("Photo %d", i+1)) + if !ok { + t.Fatalf("%d images: Photo %d missing", n, i+1) + } + xs[r.X] = true + } + if len(xs) != cols { + t.Errorf("%d images in %d columns, want %d", n, len(xs), cols) + } + } +} + +func TestImageGridOpensViewerAndRetries(t *testing.T) { + imgs := imageGridSet(3) + imgs[1] = GridImage{Alt: "Lost", Err: errors.New("404")} + retried := -1 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if i, ok := ImageGrid(c, imgs).Retried(); ok { + retried = i + } + }, 800, 600) + if err := tt.Click("Photo 3"); err != nil { + t.Fatal(err) + } + if !tt.HasText("Image 2 of 2") { + t.Fatalf("viewer not at the third picture (second viewable): %q", tt.Texts()) + } + tt.Key(0, ui.KeyEscape) + if err := tt.Click("Retry"); err != nil { + t.Fatal(err) + } + if retried != 1 { + t.Fatalf("retried %d, want 1", retried) + } +} + +func TestImageGridCountLimits(t *testing.T) { + for _, n := range []int{0, 10} { + func() { + defer func() { + if recover() == nil { + t.Errorf("%d images: no panic", n) + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ImageGrid(c, make([]GridImage, n)) + }, 100, 100) + }() + } +} + +func TestImageGridSnapshot(t *testing.T) { + imgs := imageGridSet(5) + imgs[3] = GridImage{Alt: "Loading", Loading: true} + imgs[4] = GridImage{Alt: "Lost", Err: errors.New("404")} + snapshot(t, "image_grid", 420, 300, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { ImageGrid(c, imgs) }) + }) +} diff --git a/internal/markdown.go b/internal/markdown.go new file mode 100644 index 0000000..8608cac --- /dev/null +++ b/internal/markdown.go @@ -0,0 +1,305 @@ +package internal + +import ( + "strconv" + "strings" +) + +// BlockKind is the kind of a Markdown block. +type BlockKind uint8 + +const ( + BlockParagraph BlockKind = iota + BlockHeading + BlockList + BlockQuote + BlockCode + BlockTable + BlockRule +) + +// MdSpan is a run of inline text with one style. +type MdSpan struct { + Text string + // URL is set for a link. + URL string + Strong, Em, Code, Strike bool +} + +// MdItem is one list item. +type MdItem struct { + // Number is the ordinal of an ordered item, 0 for a bullet. + Number int + Depth int + Spans []MdSpan +} + +// TableAlign is a table column's alignment. +type TableAlign uint8 + +const ( + AlignLeft TableAlign = iota + AlignCenter + AlignRight +) + +// MdBlock is one parsed block. +type MdBlock struct { + Kind BlockKind + // Level is a heading's level, 1 to 6. + Level int + // Spans hold a paragraph's, heading's or quote's text. + Spans []MdSpan + Items []MdItem + // Lang and Code hold a fenced code block; Open is true until its fence closes. + Lang, Code string + Open bool + Header []string + Align []TableAlign + Rows [][]string +} + +// Markdown is a document parsed incrementally as its source grows. +type Markdown struct { + src string + Blocks []MdBlock + starts []int +} + +// Update parses src and returns the index of the first block parsed anew. +// When src extends the last source, blocks before the last stay as they are. +func (m *Markdown) Update(src string) int { + if src == m.src && m.starts != nil { + return len(m.Blocks) + } + from := 0 + if len(m.Blocks) > 0 && strings.HasPrefix(src, m.src) { + from = len(m.Blocks) - 1 + } + off := 0 + if from > 0 { + off = m.starts[from] + } + m.Blocks, m.starts = m.Blocks[:from], m.starts[:from] + m.src = src + parseBlocks(m, src, off) + return from +} + +// mdLine is one source line and its byte offset. +type mdLine struct { + text string + off int +} + +func parseBlocks(m *Markdown, src string, off int) { + var lines []mdLine + for off < len(src) { + end := strings.IndexByte(src[off:], '\n') + if end < 0 { + lines = append(lines, mdLine{src[off:], off}) + break + } + lines = append(lines, mdLine{strings.TrimRight(src[off:off+end], "\r"), off}) + off += end + 1 + } + for i := 0; i < len(lines); { + l := lines[i].text + if strings.TrimSpace(l) == "" { + i++ + continue + } + start := lines[i].off + var b MdBlock + switch { + case mdFence(l) != "": + b, i = parseFenced(lines, i) + case mdHeadingLevel(l) > 0: + lv := mdHeadingLevel(l) + b = MdBlock{Kind: BlockHeading, Level: lv, Spans: ParseInline(strings.TrimSpace(strings.TrimRight(l[lv:], "# ")))} + i++ + case mdIsRule(l): + b = MdBlock{Kind: BlockRule} + i++ + case strings.HasPrefix(strings.TrimLeft(l, " "), ">"): + b, i = parseQuote(lines, i) + case mdListMarker(l) != nil: + b, i = parseList(lines, i) + case i+1 < len(lines) && strings.Contains(l, "|") && mdDelimiter(lines[i+1].text) != nil: + b, i = parseTable(lines, i) + default: + b, i = parseParagraph(lines, i) + } + m.Blocks = append(m.Blocks, b) + m.starts = append(m.starts, start) + } +} + +// mdStarts reports whether line l begins a block other than a paragraph. +func mdStarts(lines []mdLine, i int) bool { + l := lines[i].text + return strings.TrimSpace(l) == "" || mdFence(l) != "" || mdHeadingLevel(l) > 0 || mdIsRule(l) || + strings.HasPrefix(strings.TrimLeft(l, " "), ">") || mdListMarker(l) != nil || + i+1 < len(lines) && strings.Contains(l, "|") && mdDelimiter(lines[i+1].text) != nil +} + +func parseParagraph(lines []mdLine, i int) (MdBlock, int) { + var parts []string + for ; i < len(lines) && (len(parts) == 0 || !mdStarts(lines, i)); i++ { + parts = append(parts, strings.TrimSpace(lines[i].text)) + } + return MdBlock{Kind: BlockParagraph, Spans: ParseInline(strings.Join(parts, " "))}, i +} + +// mdFence returns the fence a line opens, "```" or "~~~", or "". +func mdFence(l string) string { + t := strings.TrimLeft(l, " ") + for _, f := range []string{"```", "~~~"} { + if strings.HasPrefix(t, f) { + return f + } + } + return "" +} + +func parseFenced(lines []mdLine, i int) (MdBlock, int) { + fence := mdFence(lines[i].text) + b := MdBlock{Kind: BlockCode, Open: true, Lang: strings.ToLower(strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(lines[i].text), fence[:1])))} + var code []string + for i++; i < len(lines); i++ { + if strings.HasPrefix(strings.TrimSpace(lines[i].text), fence) && strings.Trim(strings.TrimSpace(lines[i].text), fence[:1]) == "" { + b.Open = false + i++ + break + } + code = append(code, lines[i].text) + } + b.Code = strings.Join(code, "\n") + return b, i +} + +func mdHeadingLevel(l string) int { + n := 0 + for n < len(l) && l[n] == '#' { + n++ + } + if n == 0 || n > 6 || n < len(l) && l[n] != ' ' { + return 0 + } + return n +} + +func mdIsRule(l string) bool { + t := strings.ReplaceAll(strings.TrimSpace(l), " ", "") + if len(t) < 3 { + return false + } + return strings.Count(t, t[:1]) == len(t) && strings.ContainsAny(t[:1], "-*_") +} + +func parseQuote(lines []mdLine, i int) (MdBlock, int) { + var parts []string + for ; i < len(lines); i++ { + t := strings.TrimLeft(lines[i].text, " ") + if !strings.HasPrefix(t, ">") { + break + } + parts = append(parts, strings.TrimSpace(t[1:])) + } + return MdBlock{Kind: BlockQuote, Spans: ParseInline(strings.Join(parts, " "))}, i +} + +// mdMarker is a list item's marker. +type mdMarker struct { + number, depth, width int +} + +func mdListMarker(l string) *mdMarker { + indent := len(l) - len(strings.TrimLeft(l, " ")) + t := l[indent:] + if len(t) >= 2 && strings.ContainsRune("-*+", rune(t[0])) && t[1] == ' ' { + return &mdMarker{depth: indent / 2, width: indent + 2} + } + d := 0 + for d < len(t) && t[d] >= '0' && t[d] <= '9' { + d++ + } + if d == 0 || d > 9 || d+1 >= len(t) || !strings.ContainsRune(".)", rune(t[d])) || t[d+1] != ' ' { + return nil + } + n, _ := strconv.Atoi(t[:d]) + return &mdMarker{number: max(n, 1), depth: indent / 2, width: indent + d + 2} +} + +func parseList(lines []mdLine, i int) (MdBlock, int) { + b := MdBlock{Kind: BlockList} + var texts []string + for ; i < len(lines); i++ { + l := lines[i].text + if mk := mdListMarker(l); mk != nil { + b.Items = append(b.Items, MdItem{Number: mk.number, Depth: mk.depth}) + texts = append(texts, strings.TrimSpace(l[mk.width:])) + continue + } + if mdStarts(lines, i) { + break + } + // A lazy line continues the item above. + texts[len(texts)-1] += " " + strings.TrimSpace(l) + } + for j := range b.Items { + b.Items[j].Spans = ParseInline(texts[j]) + } + return b, i +} + +// mdCells splits a table row into trimmed cells. +func mdCells(l string) []string { + t := strings.TrimSpace(l) + t = strings.TrimSuffix(strings.TrimPrefix(t, "|"), "|") + cells := strings.Split(t, "|") + for i := range cells { + cells[i] = strings.TrimSpace(cells[i]) + } + return cells +} + +// mdDelimiter parses a table's delimiter row, or returns nil. +func mdDelimiter(l string) []TableAlign { + if !strings.Contains(l, "-") { + return nil + } + var out []TableAlign + for _, c := range mdCells(l) { + left, right := strings.HasPrefix(c, ":"), strings.HasSuffix(c, ":") + if strings.Trim(c, ":") == "" || strings.Trim(c, ":-") != "" { + return nil + } + switch { + case left && right: + out = append(out, AlignCenter) + case right: + out = append(out, AlignRight) + default: + out = append(out, AlignLeft) + } + } + return out +} + +func parseTable(lines []mdLine, i int) (MdBlock, int) { + b := MdBlock{Kind: BlockTable, Header: mdCells(lines[i].text), Align: mdDelimiter(lines[i+1].text)} + n := len(b.Header) + for len(b.Align) < n { + b.Align = append(b.Align, AlignLeft) + } + b.Align = b.Align[:n] + for i += 2; i < len(lines) && strings.Contains(lines[i].text, "|") && strings.TrimSpace(lines[i].text) != ""; i++ { + row := mdCells(lines[i].text) + for len(row) < n { + row = append(row, "") + } + b.Rows = append(b.Rows, row[:n]) + } + return b, i +} diff --git a/internal/markdown_inline.go b/internal/markdown_inline.go new file mode 100644 index 0000000..db9e160 --- /dev/null +++ b/internal/markdown_inline.go @@ -0,0 +1,128 @@ +package internal + +import "strings" + +// mdStyle is the inline style in effect. +type mdStyle struct{ strong, em, strike bool } + +// ParseInline splits text into styled spans: emphasis, code, strikethrough and links. +// An opener without its closer stays literal, as while a message streams in. +func ParseInline(s string) []MdSpan { + var out []MdSpan + parseInline(s, mdStyle{}, &out) + return out +} + +func mdEmit(out *[]MdSpan, sp MdSpan) { + if sp.Text == "" { + return + } + if n := len(*out); n > 0 { + last := &(*out)[n-1] + if sp.URL == "" && last.URL == "" && last.Strong == sp.Strong && last.Em == sp.Em && last.Code == sp.Code && last.Strike == sp.Strike { + last.Text += sp.Text + return + } + } + *out = append(*out, sp) +} + +func parseInline(s string, st mdStyle, out *[]MdSpan) { + plain := func(t string) { + mdEmit(out, MdSpan{Text: t, Strong: st.strong, Em: st.em, Strike: st.strike}) + } + for i := 0; i < len(s); { + switch c := s[i]; { + case c == '\\' && i+1 < len(s) && strings.IndexByte("\\`*_~[]()#!>-|", s[i+1]) >= 0: + plain(s[i+1 : i+2]) + i += 2 + continue + case c == '`': + if end := strings.IndexByte(s[i+1:], '`'); end >= 0 { + mdEmit(out, MdSpan{Text: s[i+1 : i+1+end], Code: true, Strong: st.strong, Em: st.em, Strike: st.strike}) + i += end + 2 + continue + } + case c == '[': + if text, url, n := mdLink(s[i:]); n > 0 { + mdEmit(out, MdSpan{Text: text, URL: url, Strong: st.strong, Em: st.em, Strike: st.strike}) + i += n + continue + } + case c == '*' || c == '_' || c == '~': + if n := mdDelimited(s, i, st, out); n > 0 { + i += n + continue + } + } + j := i + 1 + for j < len(s) && strings.IndexByte("\\`[*_~", s[j]) < 0 { + j++ + } + plain(s[i:j]) + i = j + } +} + +// mdDelimited parses emphasis or strikethrough opening at i and returns its length, or 0. +func mdDelimited(s string, i int, st mdStyle, out *[]MdSpan) int { + c := s[i] + n := 1 + if i+1 < len(s) && s[i+1] == c { + n = 2 + } + if c == '~' && n != 2 { + return 0 + } + // Intraword underscores stay literal, as in snake_case. + if c == '_' && i > 0 && mdWord(s[i-1]) { + return 0 + } + marker := s[i : i+n] + body := i + n + if body >= len(s) || s[body] == ' ' { + return 0 + } + end := -1 + for j := body + 1; j+n <= len(s); j++ { + if s[j:j+n] == marker && s[j-1] != ' ' && (n == 2 || j+1 >= len(s) || s[j+1] != c) { + end = j + break + } + } + if end < 0 { + return 0 + } + inner := st + switch { + case c == '~': + inner.strike = true + case n == 2: + inner.strong = true + default: + inner.em = true + } + parseInline(s[body:end], inner, out) + return end + n - i +} + +func mdWord(b byte) bool { + return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' +} + +// mdLink parses [text](url) at the start of s and returns its length, or 0. +func mdLink(s string) (text, url string, n int) { + close := strings.Index(s, "](") + if close < 1 || strings.IndexByte(s[1:close], '[') >= 0 { + return "", "", 0 + } + end := strings.IndexByte(s[close+2:], ')') + if end < 0 { + return "", "", 0 + } + url = strings.TrimSpace(s[close+2 : close+2+end]) + if url == "" || strings.ContainsAny(url, " \n") { + return "", "", 0 + } + return s[1:close], url, close + 3 + end +} diff --git a/internal/markdown_test.go b/internal/markdown_test.go new file mode 100644 index 0000000..373baeb --- /dev/null +++ b/internal/markdown_test.go @@ -0,0 +1,108 @@ +package internal + +import ( + "reflect" + "strings" + "testing" +) + +const mdSample = "# Title\n\nSome **bold** and *em* text with `code`\nand a [link](https://x.io).\n\n" + + "- one\n- two\n - nested\n3. three\n\n> quoted *line*\n\n```go\nfunc f() {}\n```\n\n" + + "| a | b |\n|:--|--:|\n| 1 | 2 |\n| 3 |\n\n---\n\nend ~~old~~ new" + +func TestMarkdownBlocks(t *testing.T) { + var m Markdown + m.Update(mdSample) + var kinds []BlockKind + for _, b := range m.Blocks { + kinds = append(kinds, b.Kind) + } + want := []BlockKind{BlockHeading, BlockParagraph, BlockList, BlockQuote, BlockCode, BlockTable, BlockRule, BlockParagraph} + if !reflect.DeepEqual(kinds, want) { + t.Fatalf("kinds %v, want %v", kinds, want) + } + b := m.Blocks + if b[0].Level != 1 || b[0].Spans[0].Text != "Title" { + t.Errorf("heading %+v", b[0]) + } + p := b[1].Spans + if p[1] != (MdSpan{Text: "bold", Strong: true}) || p[3] != (MdSpan{Text: "em", Em: true}) || p[5] != (MdSpan{Text: "code", Code: true}) { + t.Errorf("paragraph spans %+v", p) + } + if p[7] != (MdSpan{Text: "link", URL: "https://x.io"}) { + t.Errorf("link %+v", p[7]) + } + it := b[2].Items + if len(it) != 4 || it[2].Depth != 1 || it[2].Spans[0].Text != "nested" || it[3].Number != 3 { + t.Errorf("list items %+v", it) + } + if b[3].Spans[1] != (MdSpan{Text: "line", Em: true}) { + t.Errorf("quote %+v", b[3].Spans) + } + if b[4].Lang != "go" || b[4].Code != "func f() {}" || b[4].Open { + t.Errorf("code %+v", b[4]) + } + tb := b[5] + if !reflect.DeepEqual(tb.Header, []string{"a", "b"}) || !reflect.DeepEqual(tb.Align, []TableAlign{AlignLeft, AlignRight}) || + !reflect.DeepEqual(tb.Rows, [][]string{{"1", "2"}, {"3", ""}}) { + t.Errorf("table %+v", tb) + } + if b[7].Spans[1] != (MdSpan{Text: "old", Strike: true}) { + t.Errorf("strike %+v", b[7].Spans) + } +} + +func TestMarkdownInlineLiterals(t *testing.T) { + for in, want := range map[string]string{ + "2 * 3 * 4": "2 * 3 * 4", + "snake_case_name": "snake_case_name", + "**unclosed": "**unclosed", + `\*not em\*`: "*not em*", + "[no link]": "[no link]", + } { + sp := ParseInline(in) + if len(sp) != 1 || sp[0].Text != want || sp[0].Em || sp[0].Strong { + t.Errorf("%q gave %+v", in, sp) + } + } +} + +// TestMarkdownIncremental streams the sample byte by byte and checks finished blocks are kept. +func TestMarkdownIncremental(t *testing.T) { + var m Markdown + for i := 1; i <= len(mdSample); i++ { + before := append([]MdBlock(nil), m.Blocks...) + from := m.Update(mdSample[:i]) + if from < len(before)-1 { + t.Fatalf("at %d reparsed from %d of %d blocks", i, from, len(before)) + } + for j := 0; j < from; j++ { + if !reflect.DeepEqual(m.Blocks[j], before[j]) { + t.Fatalf("at %d finished block %d changed", i, j) + } + } + } + var whole Markdown + whole.Update(mdSample) + if !reflect.DeepEqual(m.Blocks, whole.Blocks) { + t.Fatal("streamed parse differs from a whole parse") + } + if m.Update(mdSample) != len(m.Blocks) { + t.Fatal("an unchanged source parsed again") + } + if m.Update("# Other") != 0 || len(m.Blocks) != 1 { + t.Fatal("a replaced source kept old blocks") + } +} + +func TestMarkdownOpenFence(t *testing.T) { + var m Markdown + m.Update("```py\nprint(1)\n") + if len(m.Blocks) != 1 || !m.Blocks[0].Open || m.Blocks[0].Code != "print(1)" { + t.Fatalf("open fence %+v", m.Blocks) + } + m.Update("```py\nprint(1)\n```\nafter") + if len(m.Blocks) != 2 || m.Blocks[0].Open || !strings.Contains(m.Blocks[1].Spans[0].Text, "after") { + t.Fatalf("closed fence %+v", m.Blocks) + } +} diff --git a/link_preview_card.go b/link_preview_card.go new file mode 100644 index 0000000..697ace7 --- /dev/null +++ b/link_preview_card.go @@ -0,0 +1,49 @@ +package mujica + +import ( + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// LinkPreview is a link's metadata, fetched by the caller. +type LinkPreview struct { + // URL is the link; required. + URL string + // Title is required; Description, Site and Thumbnail may be empty. + Title, Description, Site string + Thumbnail ui.ImageSource +} + +// LinkPreviewCard shows a link's site, title, description and thumbnail; Clicked opens it. +func LinkPreviewCard(c *ui.Context, p LinkPreview) *ui.Element { + if p.URL == "" || p.Title == "" { + panic("mujica: LinkPreviewCard needs URL and Title") + } + k := Tokens(c) + u := Density(c).Unit() + b := ui.ButtonBase(c).MaxWidth(420).Gap(u*3).Padding(u*2).AlignItems(ui.Start).Justify(ui.Start). + Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Label(p.Title).Tooltip(p.URL).Transition(colorFade) + internal.Ring(b, k.Focus, theme.CardRadius) + if b.Hovered() { + b.Background(k.SurfaceHover) + } + b.Children(func() { + ui.Column(c).Grow(1).MinWidth(0).Gap(u).Children(func() { + site := p.Site + if site == "" { + site = p.URL + } + ui.Text(c, site).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, p.Title).FontWeight(600).TextColor(k.AccentText).MaxLines(2) + if p.Description != "" { + ui.Text(c, p.Description).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).MaxLines(2) + } + }) + if p.Thumbnail != nil { + ui.Image(c, p.Thumbnail).Fit(ui.Cover).Size(64, 64).Radius(theme.ControlRadius).Shrink(0) + } + }) + return b +} diff --git a/link_preview_card_test.go b/link_preview_card_test.go new file mode 100644 index 0000000..527f4e9 --- /dev/null +++ b/link_preview_card_test.go @@ -0,0 +1,62 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +var linkPreviewSample = LinkPreview{ + URL: "https://example.com/versailles", Title: "The Palace of Versailles", + Description: "A royal château in the Île-de-France region, seat of the court from 1682.", Site: "example.com", +} + +func TestLinkPreviewCardShowsMetadataAndOpens(t *testing.T) { + opened := 0 + p := linkPreviewSample + p.Thumbnail = imageGridPicture("#7D2034") + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if LinkPreviewCard(c, p).Clicked() { + opened++ + } + }, 500, 200) + for _, s := range []string{"example.com", "The Palace of Versailles", "A royal château"} { + if !tt.HasText(s) { + t.Errorf("missing %q in %q", s, tt.Texts()) + } + } + _ = tt.Click("The Palace of Versailles") + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if opened != 2 { + t.Fatalf("opened %d", opened) + } +} + +func TestLinkPreviewCardSiteFallsBackToURL(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + LinkPreviewCard(c, LinkPreview{URL: "https://a.io/x", Title: "X"}) + }, 400, 200) + if !tt.HasText("https://a.io/x") { + t.Fatalf("texts %q", tt.Texts()) + } + defer func() { + if recover() == nil { + t.Fatal("no panic without a title") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + LinkPreviewCard(c, LinkPreview{URL: "https://a.io"}) + }, 100, 100) +} + +func TestLinkPreviewCardSnapshot(t *testing.T) { + p := linkPreviewSample + p.Thumbnail = imageGridPicture("#AD9164") + snapshot(t, "link_preview_card", 460, 130, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { LinkPreviewCard(c, p) }) + }) +} diff --git a/markdown_view.go b/markdown_view.go new file mode 100644 index 0000000..d06a6e7 --- /dev/null +++ b/markdown_view.go @@ -0,0 +1,126 @@ +package mujica + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// MarkdownResult is a MarkdownView's element and the link the user opened this frame. +type MarkdownResult struct { + Element *ui.Element + link string +} + +// Link returns the URL of the link clicked or activated this frame. +func (r MarkdownResult) Link() (string, bool) { return r.link, r.link != "" } + +// MarkdownView renders a Markdown subset; a growing src parses only its last block again. +func MarkdownView(c *ui.Context, src string) MarkdownResult { + u := Density(c).Unit() + var r MarkdownResult + r.Element = ui.Column(c).Gap(u * 3).MinWidth(0) + doc := ui.Local(r.Element, "doc", func() *internal.Markdown { return &internal.Markdown{} }) + if from := (*doc).Update(src); from < len((*doc).Blocks) { + slog.Debug("mujica: markdown_view parse", "from", from, "blocks", len((*doc).Blocks)) + } + r.Element.Children(func() { + for i := range (*doc).Blocks { + b := &(*doc).Blocks[i] + ui.Column(c).Key(i).MinWidth(0).Children(func() { + if url := markdownBlock(c, b); url != "" { + r.link = url + } + }) + } + }) + if r.link != "" { + slog.Debug("mujica: markdown_view link") + } + return r +} + +// markdownBlock builds one block and returns a link opened in it. +func markdownBlock(c *ui.Context, b *internal.MdBlock) string { + k := Tokens(c) + u := Density(c).Unit() + switch b.Kind { + case internal.BlockHeading: + size := []float32{theme.H1Size, theme.H2Size, theme.H3Size}[min(b.Level, 3)-1] + t := ui.RichText(c).Font(theme.SerifFont).FontSize(FontSize(c, size)).FontWeight(600).Role(ui.RoleText) + return markdownSpans(c, t, b.Spans) + case internal.BlockParagraph: + return markdownSpans(c, ui.RichText(c), b.Spans) + case internal.BlockQuote: + var link string + ui.Row(c).Padding(0, 0, 0, u*3).BorderWidth(0, 0, 0, 3).BorderColor(k.Border).Children(func() { + link = markdownSpans(c, ui.RichText(c).TextColor(k.TextMuted).Grow(1), b.Spans) + }) + return link + case internal.BlockList: + var link string + ui.Column(c).Gap(u).Role(ui.RoleList).Children(func() { + for _, it := range b.Items { + ui.Row(c).Gap(u*2).AlignItems(ui.Start).Padding(0, 0, 0, float32(it.Depth)*u*5).Role(ui.RoleListItem).Children(func() { + mark := "•" + if it.Number > 0 { + mark = fmt.Sprintf("%d.", it.Number) + } + ui.Text(c, mark).TextColor(k.TextMuted).FontFeatures("tnum").MinWidth(u * 4).Shrink(0) + if l := markdownSpans(c, ui.RichText(c).Grow(1).MinWidth(0), it.Spans); l != "" { + link = l + } + }) + } + }) + return link + case internal.BlockCode: + CodeBlock(c, b.Code, CodeBlockOptions{Language: b.Lang}) + case internal.BlockTable: + align := make([]ui.Align, len(b.Align)) + for i, a := range b.Align { + align[i] = []ui.Align{ui.Start, ui.Center, ui.End}[a] + } + TableBlock(c, b.Header, b.Rows, TableBlockOptions{Align: align}) + case internal.BlockRule: + ui.Box(c).Height(u * 4).Draw(func(p *ui.Painter, r ui.Rect) { + internal.RuleDiamond(p, r, u*2, 5, k.Ornament) + }) + } + return "" +} + +// markdownSpans fills paragraph t with styled spans; links are focusable and return their URL when used. +func markdownSpans(c *ui.Context, t *ui.Element, spans []internal.MdSpan) string { + k := Tokens(c) + var link string + t.MinWidth(0).Children(func() { + for _, sp := range spans { + e := ui.Text(c, sp.Text) + if sp.Strong { + e.FontWeight(700) + } + if sp.Em { + e.Italic() + } + if sp.Strike { + e.Strikethrough() + } + if sp.Code { + e.Font(theme.MonoFont).TextBackground(k.SurfaceHover) + } + if sp.URL == "" { + continue + } + e.TextColor(k.AccentText).Underline().Focusable().Role(ui.RoleLink).Cursor(ui.CursorPointer).Tooltip(sp.URL) + if e.Clicked() && !e.IsDisabled() { + link = sp.URL + } + } + }) + return link +} diff --git a/markdown_view_test.go b/markdown_view_test.go new file mode 100644 index 0000000..9f39b2f --- /dev/null +++ b/markdown_view_test.go @@ -0,0 +1,64 @@ +package mujica + +import ( + "strings" + "testing" + + "github.com/egoist/mygo/ui" +) + +const markdownViewSample = "# Decree\n\nBy order of **the Queen**, *all* guests use `the east door`.\n\n" + + "- Arrive at nine\n- Wear [court dress](https://example.com/dress)\n\n> Silence in the hall.\n\n" + + "```go\nfunc bow() {}\n```\n\n| Room | Floor |\n|---|--:|\n| Hall | 1 |\n\n---\n\nEnd." + +func TestMarkdownViewRendersSubset(t *testing.T) { + src := markdownViewSample + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Scroll(c).Fill().Children(func() { MarkdownView(c, src) }) + }, 600, 900) + for _, s := range []string{"Decree", "the Queen", "all", "the east door", "Arrive at nine", "court dress", + "Silence in the hall.", "func", "bow", "Room", "Hall", "End."} { + if !tt.HasText(s) { + t.Errorf("missing %q in %q", s, tt.Texts()) + } + } +} + +func TestMarkdownViewLinkByClickAndKeyboard(t *testing.T) { + var got []string + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if url, ok := MarkdownView(c, "See [the guide](https://x.io/guide).").Link(); ok { + got = append(got, url) + } + }, 400, 200) + if err := tt.Click("the guide"); err != nil { + t.Fatal(err) + } + tt.Key(0, ui.KeyEnter) + if len(got) != 2 || got[0] != "https://x.io/guide" || got[1] != got[0] { + t.Fatalf("links %q", got) + } +} + +func TestMarkdownViewStreamsWithoutReparsingFinishedBlocks(t *testing.T) { + src := "" + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Scroll(c).Fill().Children(func() { MarkdownView(c, src) }) + }, 600, 900) + for _, chunk := range strings.SplitAfter(markdownViewSample, " ") { + src += chunk + tt.Frame() + } + if !tt.HasText("End.") || !tt.HasText("Decree") { + t.Fatalf("streamed render incomplete: %q", tt.Texts()) + } +} + +func TestMarkdownViewSnapshot(t *testing.T) { + snapshot(t, "markdown_view", 560, 720, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { MarkdownView(c, markdownViewSample) }) + }) +} diff --git a/message_actions.go b/message_actions.go new file mode 100644 index 0000000..ba28375 --- /dev/null +++ b/message_actions.go @@ -0,0 +1,111 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +// MessageAction is one action on a chat message. +type MessageAction int + +const ( + ActionCopy MessageAction = iota + 1 + ActionRegenerate + ActionEdit + ActionLike + ActionDislike + ActionSpeak + ActionShare +) + +// MessageFeedback is the user's rating of an assistant message. +type MessageFeedback int + +const ( + FeedbackNone MessageFeedback = iota + FeedbackLiked + FeedbackDisliked +) + +// MessageActionsOptions configure MessageActions. +type MessageActionsOptions struct { + // Feedback holds the rating; nil hides like and dislike. + Feedback *MessageFeedback +} + +// MessageActionsResult is the toolbar's element and the action chosen this frame. +type MessageActionsResult struct { + Element *ui.Element + action MessageAction +} + +// Action returns the action the user chose this frame. +func (r MessageActionsResult) Action() (MessageAction, bool) { return r.action, r.action != 0 } + +// messageActionIcons names each action's icon and copy key. +var messageActionIcons = map[MessageAction][2]string{ + ActionCopy: {"copy", "message_actions.copy"}, + ActionRegenerate: {"refresh-cw", "message_actions.regenerate"}, + ActionEdit: {"pencil", "message_actions.edit"}, + ActionLike: {"thumbs-up", "message_actions.like"}, + ActionDislike: {"thumbs-down", "message_actions.dislike"}, + ActionSpeak: {"volume-2", "message_actions.speak"}, + ActionShare: {"share-2", "message_actions.share"}, +} + +// messageActionsFor lists the actions a role's messages offer. +func messageActionsFor(role MessageRole, rated bool) []MessageAction { + switch role { + case MessageUser: + return []MessageAction{ActionCopy, ActionEdit} + case MessageAssistant: + if rated { + return []MessageAction{ActionCopy, ActionRegenerate, ActionLike, ActionDislike, ActionSpeak, ActionShare} + } + return []MessageAction{ActionCopy, ActionRegenerate, ActionSpeak, ActionShare} + case MessageSystem: + return []MessageAction{ActionCopy} + case MessageError: + return []MessageAction{ActionCopy, ActionRegenerate} + } + panic("mujica: unknown MessageRole") +} + +// MessageActions is a toolbar of the actions a role's message offers; arrows move between them. +func MessageActions(c *ui.Context, role MessageRole, opts MessageActionsOptions) MessageActionsResult { + var r MessageActionsResult + r.Element = ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).FocusGroup(ui.Horizontal). + Role(ui.RoleToolbar).Label(msg(c, "message_actions.label")) + disabled := r.Element.IsDisabled() + r.Element.Children(func() { + for _, a := range messageActionsFor(role, opts.Feedback != nil) { + ic := messageActionIcons[a] + hit := false + if a == ActionLike || a == ActionDislike { + want := FeedbackLiked + if a == ActionDislike { + want = FeedbackDisliked + } + on := *opts.Feedback == want + if codeBlockToggle(c, ic[0], msg(c, ic[1]), &on) && !disabled { + hit = true + if on { + *opts.Feedback = want + } else { + *opts.Feedback = FeedbackNone + } + } + } else { + hit = iconAction(c, icons.Must(ic[0]), msg(c, ic[1])).Tooltip(msg(c, ic[1])).Clicked() && !disabled + } + if hit { + r.action = a + slog.Debug("mujica: message_actions action", "action", a) + } + } + }) + return r +} diff --git a/message_actions_test.go b/message_actions_test.go new file mode 100644 index 0000000..6c41739 --- /dev/null +++ b/message_actions_test.go @@ -0,0 +1,93 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestMessageActionsByRole(t *testing.T) { + for role, want := range map[MessageRole][]string{ + MessageUser: {"Copy", "Edit"}, + MessageAssistant: {"Copy", "Regenerate", "Good response", "Bad response", "Read aloud", "Share"}, + MessageSystem: {"Copy"}, + MessageError: {"Copy", "Regenerate"}, + } { + fb := FeedbackNone + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + MessageActions(c, role, MessageActionsOptions{Feedback: &fb}) + }, 400, 100) + for _, name := range want { + if _, ok := tt.Find(name); !ok { + t.Errorf("role %v: missing %q", role, name) + } + } + if _, ok := tt.Find("Edit"); ok != (role == MessageUser) { + t.Errorf("role %v: edit shown %v", role, ok) + } + } +} + +func TestMessageActionsChooseAndRate(t *testing.T) { + fb := FeedbackNone + var got []MessageAction + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if a, ok := MessageActions(c, MessageAssistant, MessageActionsOptions{Feedback: &fb}).Action(); ok { + got = append(got, a) + } + }, 400, 100) + _ = tt.Click("Regenerate") + _ = tt.Click("Good response") + if fb != FeedbackLiked { + t.Fatalf("feedback %v after like", fb) + } + _ = tt.Click("Bad response") + if fb != FeedbackDisliked { + t.Fatalf("feedback %v after dislike", fb) + } + _ = tt.Click("Bad response") + if fb != FeedbackNone { + t.Fatalf("feedback %v after undoing dislike", fb) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + tt.Key(0, ui.KeyEnter) + // Tab returns to the last focused action, Dislike; Right moves to Read aloud. + want := []MessageAction{ActionRegenerate, ActionLike, ActionDislike, ActionDislike, ActionSpeak} + if len(got) != len(want) { + t.Fatalf("actions %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("actions %v, want %v", got, want) + } + } +} + +func TestMessageActionsHideRatingAndDisable(t *testing.T) { + n := 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Disabled(true).Children(func() { + if _, ok := MessageActions(c, MessageAssistant, MessageActionsOptions{}).Action(); ok { + n++ + } + }) + }, 400, 100) + if _, ok := tt.Find("Good response"); ok { + t.Fatal("rating shown without Feedback") + } + _ = tt.Click("Copy") + if n != 0 { + t.Fatal("disabled actions fired") + } +} + +func TestMessageActionsSnapshot(t *testing.T) { + fb := FeedbackLiked + snapshot(t, "message_actions", 300, 70, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { MessageActions(c, MessageAssistant, MessageActionsOptions{Feedback: &fb}) }) + }) +} diff --git a/message_bubble.go b/message_bubble.go new file mode 100644 index 0000000..a4377ae --- /dev/null +++ b/message_bubble.go @@ -0,0 +1,115 @@ +package mujica + +import ( + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// MessageRole is who a chat message comes from. +type MessageRole int + +const ( + MessageUser MessageRole = iota + MessageAssistant + MessageSystem + MessageError +) + +func (r MessageRole) key() string { + switch r { + case MessageUser: + return "message_bubble.user" + case MessageAssistant: + return "message_bubble.assistant" + case MessageSystem: + return "message_bubble.system" + case MessageError: + return "message_bubble.error" + } + panic("mujica: unknown MessageRole") +} + +// MessageBubbleOptions configure a MessageBubble. +type MessageBubbleOptions struct { + // Name shows above the bubble; empty shows none. + Name string + // Avatar builds the picture beside the bubble. + Avatar func() + // MaxWidth caps the bubble in DIPs; zero means 560. + MaxWidth float32 +} + +// MessageBubble frames one chat message by role: users right, others left, system centered. +func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, content func()) *ui.Element { + if opts.MaxWidth < 0 { + panic("mujica: MessageBubble MaxWidth must not be negative") + } + if content == nil { + panic("mujica: MessageBubble needs content") + } + maxW := opts.MaxWidth + if maxW == 0 { + maxW = 560 + } + k := Tokens(c) + u := Density(c).Unit() + label := msg(c, role.key()) + if opts.Name != "" { + label += ", " + opts.Name + } + row := ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Role(ui.RoleGroup).Label(label) + switch role { + case MessageUser: + row.Justify(ui.End) + case MessageSystem: + row.Justify(ui.Center) + } + row.Children(func() { + if role == MessageSystem { + ui.Row(c).MaxWidth(maxW).Padding(u, u*3).Gap(u * 2).Radius(theme.ControlRadius). + Background(k.SurfaceHover).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)). + Children(content) + return + } + if opts.Avatar != nil && role != MessageUser { + opts.Avatar() + } + ui.Column(c).Gap(u).MaxWidth(maxW).Shrink(1).MinWidth(0).AlignItems(messageBubbleAlign(role)).Children(func() { + if opts.Name != "" { + ui.Text(c, opts.Name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + } + b := ui.Column(c).Padding(u*2, u*3).Gap(u * 2).Radius(theme.CardRadius).MinWidth(0) + switch role { + case MessageUser: + b.Background(k.Selection) + case MessageError: + b.Background(k.Surface).Border(theme.BorderWidth, k.Danger) + default: + b.Background(k.Surface).Border(theme.BorderWidth, k.Border) + } + b.Children(func() { + if role != MessageError { + content() + return + } + ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Children(func() { + ui.Icon(c, icons.Must("circle-alert")).Size(16, 16).Shrink(0).TextColor(k.Danger) + ui.Column(c).Gap(u * 2).Grow(1).MinWidth(0).Children(content) + }) + }) + }) + if opts.Avatar != nil && role == MessageUser { + opts.Avatar() + } + }) + return row +} + +func messageBubbleAlign(r MessageRole) ui.Align { + if r == MessageUser { + return ui.End + } + return ui.Start +} diff --git a/message_bubble_test.go b/message_bubble_test.go new file mode 100644 index 0000000..df5676c --- /dev/null +++ b/message_bubble_test.go @@ -0,0 +1,60 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestMessageBubbleRolesAlignAndLimit(t *testing.T) { + var user, bot, sys ui.Rect + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(600).Children(func() { + MessageBubble(c, MessageUser, MessageBubbleOptions{Name: "Ada", MaxWidth: 200}, func() { + user = ui.Text(c, "A long user message that must wrap inside the bubble's width limit.").Bounds() + }) + MessageBubble(c, MessageAssistant, MessageBubbleOptions{Avatar: func() { Avatar(c, "Bot", AvatarOptions{}) }}, func() { + bot = ui.Text(c, "Hello").Bounds() + }) + MessageBubble(c, MessageSystem, MessageBubbleOptions{}, func() { sys = ui.Text(c, "Joined").Bounds() }) + MessageBubble(c, MessageError, MessageBubbleOptions{}, func() { ui.Text(c, "Timeout") }) + }) + }, 600, 400) + tt.Frame() + if user.W > 200 || user.X < 300 { + t.Errorf("user bubble %+v not right-aligned within 200", user) + } + if bot.X > 100 || sys.X < 200 || sys.X > 300 { + t.Errorf("assistant at %v, system at %v", bot.X, sys.X) + } + for _, s := range []string{"Ada", "Bot", "Error"} { + if !tt.HasText(s) { + t.Errorf("missing %q", s) + } + } +} + +func TestMessageBubbleBadOptions(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic for a negative MaxWidth") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + MessageBubble(c, MessageUser, MessageBubbleOptions{MaxWidth: -1}, func() {}) + }, 100, 100) +} + +func TestMessageBubbleSnapshot(t *testing.T) { + snapshot(t, "message_bubble", 520, 300, func(c *ui.Context) { + ui.Column(c).Padding(16).Gap(12).Children(func() { + MessageBubble(c, MessageUser, MessageBubbleOptions{Name: "Marie"}, func() { ui.Text(c, "Is the ball tonight?") }) + MessageBubble(c, MessageAssistant, MessageBubbleOptions{Name: "Herald", Avatar: func() { Avatar(c, "Herald", AvatarOptions{}) }}, + func() { ui.Text(c, "Yes, at nine, in the Hall of Mirrors.") }) + MessageBubble(c, MessageSystem, MessageBubbleOptions{}, func() { ui.Text(c, "Herald joined the chat") }) + MessageBubble(c, MessageError, MessageBubbleOptions{}, func() { ui.Text(c, "The messenger did not return.") }) + }) + }) +} diff --git a/message_editor.go b/message_editor.go new file mode 100644 index 0000000..06439b5 --- /dev/null +++ b/message_editor.go @@ -0,0 +1,57 @@ +package mujica + +import ( + "log/slog" + "strings" + + "github.com/egoist/mygo/ui" +) + +// MessageEditorOptions configure a MessageEditor. +type MessageEditorOptions struct { + // Original is the sent text; saving it unchanged is not allowed. + Original string +} + +// MessageEditorResult is the editor's element and its outcome this frame. +type MessageEditorResult struct { + Element *ui.Element + saved, canceled bool +} + +// Saved reports a save; the caller makes a new branch from the draft. +func (r MessageEditorResult) Saved() bool { return r.saved } + +// Canceled reports Cancel or Escape. +func (r MessageEditorResult) Canceled() bool { return r.canceled } + +// MessageEditor edits a sent message in *draft; Cmd+Enter saves, Escape cancels. +func MessageEditor(c *ui.Context, draft *string, opts MessageEditorOptions) MessageEditorResult { + if draft == nil { + panic("mujica: MessageEditor needs a draft") + } + u := Density(c).Unit() + var r MessageEditorResult + r.Element = ui.Column(c).Gap(u * 2).MinWidth(0) + disabled := r.Element.IsDisabled() + r.Element.Children(func() { + area := TextArea(c, draft, TextAreaOptions{Label: msg(c, "message_editor.label"), MaxHeight: 240}) + ok := strings.TrimSpace(*draft) != "" && *draft != opts.Original + escape := area.Input.Shortcut(0, ui.KeyEscape) + ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { + if Button(c, msg(c, "message_editor.cancel"), ButtonOptions{Variant: Secondary}).Clicked() || escape { + r.canceled = !disabled + } + if Button(c, msg(c, "message_editor.save"), ButtonOptions{Disabled: !ok}).Clicked() || area.Submitted() && ok { + r.saved = !disabled + } + }) + }) + if r.saved { + slog.Debug("mujica: message_editor save", "chars", len(*draft)) + } + if r.canceled { + slog.Debug("mujica: message_editor cancel") + } + return r +} diff --git a/message_editor_test.go b/message_editor_test.go new file mode 100644 index 0000000..1a75412 --- /dev/null +++ b/message_editor_test.go @@ -0,0 +1,62 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func messageEditorTester(draft *string, saves, cancels *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := MessageEditor(c, draft, MessageEditorOptions{Original: "Hello"}) + if r.Saved() { + *saves++ + } + if r.Canceled() { + *cancels++ + } + }, 400, 300) +} + +func TestMessageEditorSaveRules(t *testing.T) { + draft, saves, cancels := "Hello", 0, 0 + tt := messageEditorTester(&draft, &saves, &cancels) + _ = tt.Click("Save") + _ = tt.Click("Edit message") + tt.Key(ui.Cmd, ui.KeyEnter) + if saves != 0 { + t.Fatal("saved unchanged text") + } + tt.Type(" there") + tt.Key(ui.Cmd, ui.KeyEnter) + _ = tt.Click("Save") + if saves != 2 || draft != "Hello there" { + t.Fatalf("saves %d draft %q", saves, draft) + } + _ = tt.Click("Edit message") + tt.Command("selectAll") + tt.Type(" ") + _ = tt.Click("Save") + if saves != 2 { + t.Fatal("saved blank text") + } +} + +func TestMessageEditorCancel(t *testing.T) { + draft, saves, cancels := "Hello", 0, 0 + tt := messageEditorTester(&draft, &saves, &cancels) + _ = tt.Click("Cancel") + _ = tt.Click("Edit message") + tt.Key(0, ui.KeyEscape) + if cancels != 2 || saves != 0 { + t.Fatalf("cancels %d saves %d", cancels, saves) + } +} + +func TestMessageEditorSnapshot(t *testing.T) { + draft := "Could you seat the ambassador nearer the throne?" + snapshot(t, "message_editor", 420, 200, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { MessageEditor(c, &draft, MessageEditorOptions{Original: "Seat the ambassador."}) }) + }) +} diff --git a/message_header.go b/message_header.go new file mode 100644 index 0000000..f45adde --- /dev/null +++ b/message_header.go @@ -0,0 +1,70 @@ +package mujica + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// MessageStatus is where a chat message is in its delivery. +type MessageStatus int + +const ( + // MessageNoStatus shows no status. + MessageNoStatus MessageStatus = iota + MessageSending + MessageStreaming + MessageSent + MessageFailed +) + +// MessageHeaderOptions configure a MessageHeader; zero fields are hidden. +type MessageHeaderOptions struct { + Model string + Time time.Time + Status MessageStatus +} + +// MessageHeader shows a message's model, time and status as icon and text. +func MessageHeader(c *ui.Context, opts MessageHeaderOptions) *ui.Element { + k := Tokens(c) + u := Density(c).Unit() + small := FontSize(c, theme.CaptionSize) + row := ui.Row(c).Gap(u * 2).AlignItems(ui.Center).FontSize(small).TextColor(k.TextMuted) + row.Children(func() { + if opts.Model != "" { + ui.Text(c, opts.Model).FontWeight(600).TextColor(k.Text).SingleLine() + } + if !opts.Time.IsZero() { + ui.Text(c, opts.Time.Format(msg(c, "message_header.time"))).FontFeatures("tnum").SingleLine() + } + if opts.Status == MessageNoStatus { + return + } + key, icon, color := "", "", k.TextMuted + switch opts.Status { + case MessageSending: + key, icon = "message_header.sending", "clock" + case MessageStreaming: + key = "message_header.streaming" + case MessageSent: + key, icon = "message_header.sent", "check" + case MessageFailed: + key, icon, color = "message_header.failed", "circle-alert", k.Danger + default: + panic("mujica: unknown MessageStatus") + } + ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(color).Children(func() { + if icon == "" { + Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, key)}) + } else { + ui.Icon(c, icons.Must(icon)).Size(12, 12) + } + ui.Text(c, msg(c, key)).SingleLine() + }) + }) + return row +} diff --git a/message_header_test.go b/message_header_test.go new file mode 100644 index 0000000..61f95b6 --- /dev/null +++ b/message_header_test.go @@ -0,0 +1,55 @@ +package mujica + +import ( + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +func TestMessageHeaderFieldsAndStatus(t *testing.T) { + at := time.Date(2026, 3, 1, 14, 5, 0, 0, time.UTC) + for status, want := range map[MessageStatus]string{ + MessageSending: "Sending", MessageStreaming: "Generating", MessageSent: "Sent", MessageFailed: "Failed", + } { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Locale: EnUS}) + MessageHeader(c, MessageHeaderOptions{Model: "claude", Time: at, Status: status}) + }, 400, 100) + for _, s := range []string{"claude", "14:05", want} { + if !tt.HasText(s) { + t.Errorf("status %v: missing %q in %q", status, s, tt.Texts()) + } + } + } + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + MessageHeader(c, MessageHeaderOptions{}) + }, 400, 100) + if len(tt.Texts()) != 0 { + t.Fatalf("zero header shows %q", tt.Texts()) + } +} + +func TestMessageHeaderUnknownStatus(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic for an unknown status") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + MessageHeader(c, MessageHeaderOptions{Status: 99}) + }, 100, 100) +} + +func TestMessageHeaderSnapshot(t *testing.T) { + at := time.Date(2026, 3, 1, 21, 30, 0, 0, time.UTC) + snapshot(t, "message_header", 360, 150, func(c *ui.Context) { + ui.Column(c).Padding(16).Gap(12).Children(func() { + for _, s := range []MessageStatus{MessageSending, MessageStreaming, MessageSent, MessageFailed} { + MessageHeader(c, MessageHeaderOptions{Model: "herald-2", Time: at, Status: s}) + } + }) + }) +} diff --git a/message_list.go b/message_list.go new file mode 100644 index 0000000..fc5bf1c --- /dev/null +++ b/message_list.go @@ -0,0 +1,108 @@ +package mujica + +import ( + "log/slog" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// MessageListState keeps a MessageList's place and what the user has seen. +type MessageListState struct { + list ui.ListState + seen int + started bool +} + +// ScrollToEnd shows the newest message and follows new ones again. +func (s *MessageListState) ScrollToEnd() { s.list.ScrollToEnd() } + +// AtEnd reports whether the newest message is in view. +func (s *MessageListState) AtEnd() bool { return s.list.AtEnd() } + +// MessageListOptions configure a MessageList. +type MessageListOptions struct { + // ID returns message i's stable identity; required. + ID func(i int) string + // Date returns message i's time; set, a separator heads each new day. + Date func(i int) time.Time + // Label returns message i's text for assistive technology and typing to find. + Label func(i int) string +} + +// MessageListResult is a MessageList's element and its unread count. +type MessageListResult struct { + Element *ui.Element + unread int +} + +// Unread returns how many messages arrived since the user scrolled away from the end. +func (r MessageListResult) Unread() int { return r.unread } + +// MessageList shows n messages, building only those in view, and follows the end until the user scrolls up. +func MessageList(c *ui.Context, s *MessageListState, n int, opts MessageListOptions, row func(i int)) MessageListResult { + if opts.ID == nil || row == nil { + panic("mujica: MessageList needs ID and row") + } + if n < 0 { + panic("mujica: MessageList with negative message count") + } + u := Density(c).Unit() + s.list.Key = func(i int) any { return opts.ID(i) } + s.list.Label = opts.Label + s.list.FollowEnd = true + at := !s.started || s.list.AtEnd() + if at || n < s.seen { + s.seen = n + } + s.started = true + r := MessageListResult{unread: n - s.seen} + r.Element = ui.Box(c).Grow(1).MinHeight(0) + r.Element.Children(func() { + ui.List(c, &s.list, n, func(i int) { + ui.Column(c).Padding(u*2, u*4).Gap(u * 2).Children(func() { + if opts.Date != nil && (i == 0 || !messageListSameDay(opts.Date(i-1), opts.Date(i))) { + messageListDay(c, opts.Date(i)) + } + row(i) + }) + }).Fill().Justify(ui.End).Label(msg(c, "message_list.label")) + jump := ScrollToBottomButton(c, !at, r.unread) + if jump.Clicked() && !r.Element.IsDisabled() { + s.list.ScrollToEnd() + s.seen = n + slog.Debug("mujica: message_list jump", "unread", r.unread) + } + }) + return r +} + +func messageListSameDay(a, b time.Time) bool { + ay, am, ad := a.Date() + by, bm, bd := b.Date() + return ay == by && am == bm && ad == bd +} + +// messageListDay is a separator naming the day: today, yesterday or the date. +func messageListDay(c *ui.Context, t time.Time) { + k := Tokens(c) + u := Density(c).Unit() + label := t.Format(msg(c, "message_list.date")) + now := c.Now().In(t.Location()) + switch { + case messageListSameDay(t, now): + label = msg(c, "message_list.today") + case messageListSameDay(t, now.AddDate(0, 0, -1)): + label = msg(c, "message_list.yesterday") + } + ui.Row(c).Gap(u*3).AlignItems(ui.Center).Padding(u*2, 0).Role(ui.RoleText).Label(label).Children(func() { + rule := func() { + ui.Box(c).Grow(1).Height(theme.BorderWidth).Background(k.Border) + } + rule() + ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + rule() + }) +} diff --git a/message_list_test.go b/message_list_test.go new file mode 100644 index 0000000..5aaf0a4 --- /dev/null +++ b/message_list_test.go @@ -0,0 +1,127 @@ +package mujica + +import ( + "fmt" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +// messageListApp is a chat of n messages with stable IDs. +type messageListApp struct { + ids []string + state MessageListState + built int + res MessageListResult +} + +func newMessageListApp(n int) *messageListApp { + a := &messageListApp{} + for i := range n { + a.ids = append(a.ids, fmt.Sprintf("m%d", i)) + } + return a +} + +func (a *messageListApp) view(c *ui.Context) { + Use(c, Settings{}) + a.built = 0 + day := time.Date(2026, 3, 1, 9, 0, 0, 0, time.UTC) + a.res = MessageList(c, &a.state, len(a.ids), MessageListOptions{ + ID: func(i int) string { return a.ids[i] }, + Date: func(i int) time.Time { return day.Add(time.Duration(i/3) * 24 * time.Hour) }, + }, func(i int) { + a.built++ + ui.Text(c, "Message "+a.ids[i]).Padding(8) + }) +} + +func TestMessageListVirtualizedAndFollowsEnd(t *testing.T) { + a := newMessageListApp(100000) + tt := ui.NewTester(a.view, 400, 300) + tt.Frame() + if a.built == 0 || a.built > 60 { + t.Fatalf("built %d rows of 100000", a.built) + } + if !tt.HasText("Message m99999") || !a.state.AtEnd() { + t.Fatal("list did not start at the newest message") + } + a.ids = append(a.ids, "m100000") + tt.Frame() + tt.Frame() + if !tt.HasText("Message m100000") || a.res.Unread() != 0 { + t.Fatalf("new message not followed, unread %d", a.res.Unread()) + } +} + +func TestMessageListStopsFollowingAndCountsUnread(t *testing.T) { + a := newMessageListApp(50) + tt := ui.NewTester(a.view, 400, 300) + tt.Frame() + tt.Scroll(200, 150, 0, -2000) + tt.Frame() + if a.state.AtEnd() { + t.Fatal("still at end after scrolling up") + } + a.ids = append(a.ids, "n1", "n2") + tt.Frame() + tt.Frame() + if tt.HasText("Message n2") || a.res.Unread() != 2 { + t.Fatalf("followed while scrolled up, unread %d", a.res.Unread()) + } + if err := tt.Click("Scroll to latest, 2 new"); err != nil { + t.Fatal(err) + } + tt.Frame() + tt.Frame() + if !tt.HasText("Message n2") || a.res.Unread() != 0 { + t.Fatalf("jump did not reach the end, unread %d", a.res.Unread()) + } +} + +func TestMessageListKeepsPlaceByID(t *testing.T) { + a := newMessageListApp(50) + tt := ui.NewTester(a.view, 400, 300) + tt.Frame() + tt.Scroll(200, 150, 0, -600) + tt.Frame() + r, _ := tt.Find("Message m40") + // Older messages load above; the one in view stays put. + a.ids = append([]string{"old1", "old2", "old3"}, a.ids...) + tt.Frame() + r2, ok := tt.Find("Message m40") + if !ok || r2.Y != r.Y { + t.Fatalf("m40 moved from %v to %v (found %v)", r.Y, r2.Y, ok) + } +} + +func TestMessageListDaySeparators(t *testing.T) { + a := newMessageListApp(6) + tt := ui.NewTester(a.view, 400, 600) + tt.Frame() + for _, d := range []string{"Mar 1, 2026", "Mar 2, 2026"} { + if !tt.HasText(d) { + t.Errorf("missing separator %q in %q", d, tt.Texts()) + } + } +} + +func TestMessageListNeedsID(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic without ID") + } + }() + var s MessageListState + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + MessageList(c, &s, 1, MessageListOptions{}, func(int) {}) + }, 200, 100) +} + +func TestMessageListSnapshot(t *testing.T) { + snapshot(t, "message_list", 420, 320, func(c *ui.Context) { + (*ui.Local(c.Root(), "app", func() *messageListApp { return newMessageListApp(6) })).view(c) + }) +} diff --git a/messages_chatmsg.go b/messages_chatmsg.go new file mode 100644 index 0000000..2ebff69 --- /dev/null +++ b/messages_chatmsg.go @@ -0,0 +1,57 @@ +package mujica + +func init() { + addMessages(map[string][2]string{ + "message_bubble.user": {"You", "你"}, + "message_bubble.assistant": {"Assistant", "助手"}, + "message_bubble.system": {"System", "系统"}, + "message_bubble.error": {"Error", "错误"}, + "message_header.time": {"15:04", "15:04"}, + "message_header.sending": {"Sending", "发送中"}, + "message_header.streaming": {"Generating", "生成中"}, + "message_header.sent": {"Sent", "已发送"}, + "message_header.failed": {"Failed", "失败"}, + "message_list.label": {"Messages", "消息"}, + "message_list.date": {"Jan 2, 2006", "2006年1月2日"}, + "message_list.today": {"Today", "今天"}, + "message_list.yesterday": {"Yesterday", "昨天"}, + "scroll_to_bottom_button.label": {"Scroll to latest", "回到最新"}, + "scroll_to_bottom_button.unread": {"Scroll to latest, %d new", "回到最新,%d 条新消息"}, + "code_block.plain": {"text", "文本"}, + "code_block.wrap": {"Wrap lines", "自动换行"}, + "code_block.copy": {"Copy code", "复制代码"}, + "code_block.copied": {"Copied", "已复制"}, + "code_block.collapse": {"Collapse", "折叠"}, + "code_block.expand": {"Show %d lines", "展开 %d 行"}, + "table_block.title": {"Table", "表格"}, + "table_block.copy": {"Copy as TSV", "复制为 TSV"}, + "file_message.open": {"Open %s, %s", "打开 %s,%s"}, + "file_message.download": {"Download %s", "下载 %s"}, + "file_message.bytes": {"%d B", "%d 字节"}, + "audio_message.play": {"Play", "播放"}, + "audio_message.pause": {"Pause", "暂停"}, + "audio_message.seek": {"Playback position", "播放位置"}, + "quote_reply.label": {"Reply to %s: %s", "回复 %s:%s"}, + "message_actions.label": {"Message actions", "消息操作"}, + "message_actions.copy": {"Copy", "复制"}, + "message_actions.regenerate": {"Regenerate", "重新生成"}, + "message_actions.edit": {"Edit", "编辑"}, + "message_actions.like": {"Good response", "赞"}, + "message_actions.dislike": {"Bad response", "踩"}, + "message_actions.speak": {"Read aloud", "朗读"}, + "message_actions.share": {"Share", "分享"}, + "message_editor.label": {"Edit message", "编辑消息"}, + "message_editor.cancel": {"Cancel", "取消"}, + "message_editor.save": {"Save", "保存"}, + "branch_navigator.label": {"Branch %d of %d", "第 %d 个分支,共 %d 个"}, + "branch_navigator.previous": {"Previous branch", "上一个分支"}, + "branch_navigator.next": {"Next branch", "下一个分支"}, + "error_message.title": {"Something went wrong", "出错了"}, + "error_message.retry": {"Retry", "重试"}, + "error_message.wait": {"Retry in %d s", "%d 秒后重试"}, + "error_message.limited": {"Rate limited", "已限流"}, + "thinking_block.done": {"Thought for %d s", "思考了 %d 秒"}, + "thinking_block.thinking": {"Thinking… %d s", "思考中… %d 秒"}, + "typing_indicator.label": {"%s is typing…", "%s 正在输入…"}, + }) +} diff --git a/quote_reply.go b/quote_reply.go new file mode 100644 index 0000000..ad82de1 --- /dev/null +++ b/quote_reply.go @@ -0,0 +1,33 @@ +package mujica + +import ( + "fmt" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// QuoteReply shows the author and a one-line excerpt of a quoted message; Clicked jumps to it. +func QuoteReply(c *ui.Context, author, excerpt string) *ui.Element { + if author == "" { + panic("mujica: QuoteReply needs an author") + } + k := Tokens(c) + u := Density(c).Unit() + line := strings.Join(strings.Fields(excerpt), " ") + b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MaxWidth(420).Gap(u*2).Padding(u, u*3, u, u*2).Justify(ui.Start). + Radius(theme.ControlRadius).Background(k.SurfaceHover).BorderWidth(0, 0, 0, 3).BorderColor(k.AccentText). + Label(fmt.Sprintf(msg(c, "quote_reply.label"), author, line)).Transition(colorFade) + internal.Ring(b, k.Focus, theme.ControlRadius) + if b.Hovered() { + b.Background(k.Selection) + } + b.Children(func() { + ui.Text(c, author).FontWeight(600).FontSize(FontSize(c, theme.CaptionSize)).SingleLine().Shrink(0) + ui.Text(c, line).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().MinWidth(0).Shrink(1) + }) + return b +} diff --git a/quote_reply_test.go b/quote_reply_test.go new file mode 100644 index 0000000..b9aa5b5 --- /dev/null +++ b/quote_reply_test.go @@ -0,0 +1,53 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestQuoteReplyJumpsAndFlattensExcerpt(t *testing.T) { + jumps, off := 0, false + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Disabled(off).Children(func() { + if QuoteReply(c, "Marie", "Let them\n eat cake.").Clicked() { + jumps++ + } + }) + }, 400, 200) + if !tt.HasText("Let them eat cake.") { + t.Fatalf("excerpt %q", tt.Texts()) + } + _ = tt.Click("Reply to Marie: Let them eat cake.") + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if jumps != 2 { + t.Fatalf("jumps %d", jumps) + } + off = true + _ = tt.Click("Reply to Marie: Let them eat cake.") + if jumps != 2 { + t.Fatal("disabled quote jumped") + } +} + +func TestQuoteReplyNeedsAuthor(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic without an author") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + QuoteReply(c, "", "x") + }, 100, 100) +} + +func TestQuoteReplySnapshot(t *testing.T) { + snapshot(t, "quote_reply", 420, 70, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { + QuoteReply(c, "Herald", "The ball begins at nine in the Hall of Mirrors, and every guest must arrive by the east door.") + }) + }) +} diff --git a/scroll_to_bottom_button.go b/scroll_to_bottom_button.go new file mode 100644 index 0000000..19ac548 --- /dev/null +++ b/scroll_to_bottom_button.go @@ -0,0 +1,47 @@ +package mujica + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// ScrollToBottomButton shows while away is true, with the unread count; click it to return to the end. +// While hidden it returns an empty element whose Clicked is false. +func ScrollToBottomButton(c *ui.Context, away bool, unread int) *ui.Element { + if unread < 0 { + panic("mujica: ScrollToBottomButton unread must not be negative") + } + if !away { + return ui.Box(c).Absolute().Invisible() + } + k := Tokens(c) + u := Density(c).Unit() + label := msg(c, "scroll_to_bottom_button.label") + if unread > 0 { + label = fmt.Sprintf(msg(c, "scroll_to_bottom_button.unread"), unread) + } + wrap := ui.Row(c).Absolute().Left(0).Right(0).Bottom(u * 3).Justify(ui.Center).PassThrough() + var b *ui.Element + wrap.Children(func() { + b = ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, u*3).Gap(u*2). + Radius(theme.MinHitSize/2).Label(label).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder). + TextColor(k.Text).Shadow(0, 2, 8, 0, ui.RGBA(0, 0, 0, 0.14)).Transition(colorFade) + b.Opacity(overlayEnter(c, b)) + internal.Ring(b, k.Focus, theme.MinHitSize/2) + if b.Hovered() { + b.Background(k.SurfaceHover) + } + b.Children(func() { + ui.Icon(c, icons.Must("arrow-down")).Size(16, 16) + if unread > 0 { + ui.Text(c, fmt.Sprint(unread)).FontFeatures("tnum").FontWeight(600).TextColor(k.AccentText).SingleLine() + } + }) + }) + return b +} diff --git a/scroll_to_bottom_button_test.go b/scroll_to_bottom_button_test.go new file mode 100644 index 0000000..2ea4271 --- /dev/null +++ b/scroll_to_bottom_button_test.go @@ -0,0 +1,51 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestScrollToBottomButtonShowsWhenAway(t *testing.T) { + away, unread, clicks := false, 0, 0 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Box(c).Fill().Children(func() { + if ScrollToBottomButton(c, away, unread).Clicked() { + clicks++ + } + }) + }, 300, 200) + if _, ok := tt.Find("Scroll to latest"); ok { + t.Fatal("shown while at the bottom") + } + away, unread = true, 4 + tt.Frame() + if !tt.HasText("4") { + t.Fatalf("unread count missing: %q", tt.Texts()) + } + _ = tt.Click("Scroll to latest, 4 new") + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if clicks != 2 { + t.Fatalf("clicks %d", clicks) + } +} + +func TestScrollToBottomButtonNegativeUnread(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic for negative unread") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ScrollToBottomButton(c, true, -1) + }, 100, 100) +} + +func TestScrollToBottomButtonSnapshot(t *testing.T) { + snapshot(t, "scroll_to_bottom_button", 240, 80, func(c *ui.Context) { + ui.Box(c).Fill().Children(func() { ScrollToBottomButton(c, true, 12) }) + }) +} diff --git a/streaming_text.go b/streaming_text.go new file mode 100644 index 0000000..8c658f4 --- /dev/null +++ b/streaming_text.go @@ -0,0 +1,77 @@ +package mujica + +import ( + "strings" + "time" + "unicode/utf8" + + "github.com/egoist/mygo/ui" +) + +// StreamingTextOptions configure a StreamingText. +type StreamingTextOptions struct { + // Streaming shows the cursor after the text while more is coming. + Streaming bool + // Smooth reveals new text gradually instead of at once. + Smooth bool + // Rate is the smooth reveal speed in characters per second; zero means 80. + Rate float64 +} + +// streamingState is how much of the text shows, and since when. +type streamingState struct { + text string + shown float64 + at time.Time +} + +// StreamingText shows text that grows as it streams; reduced motion shows it at once. +func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) *ui.Element { + if opts.Rate < 0 { + panic("mujica: StreamingText Rate must not be negative") + } + rate := opts.Rate + if rate == 0 { + rate = 80 + } + k := Tokens(c) + e := ui.Box(c).Label(text) + st := ui.Local(e, "stream", func() streamingState { + return streamingState{text: text, shown: float64(utf8.RuneCountInString(text)), at: c.Now()} + }) + total := float64(utf8.RuneCountInString(text)) + if !strings.HasPrefix(text, st.text) { + // Replaced text starts over from what it shares. + st.shown = min(st.shown, float64(utf8.RuneCountInString(streamingCommon(st.text, text)))) + } + st.text = text + now := c.Now() + if !opts.Smooth || Motion(c, time.Second) == 0 { + st.shown = total + } else if st.shown < total { + st.shown = min(total, st.shown+now.Sub(st.at).Seconds()*rate) + c.AnimationFrame() + } + st.at = now + shown := text + if n := int(st.shown); n < int(total) { + shown = string([]rune(text)[:n]) + } + spans := []ui.Span{{Text: shown}} + if opts.Streaming { + spans = append(spans, ui.Span{Text: "▍", Color: k.AccentText}) + } + return e.Children(func() { ui.RichText(c, spans...) }) +} + +// streamingCommon returns the longest common prefix of a and b, on rune boundaries. +func streamingCommon(a, b string) string { + n := 0 + for n < len(a) && n < len(b) && a[n] == b[n] { + n++ + } + for n > 0 && n < len(a) && !utf8.RuneStart(a[n]) { + n-- + } + return a[:n] +} diff --git a/streaming_text_test.go b/streaming_text_test.go new file mode 100644 index 0000000..a8499d3 --- /dev/null +++ b/streaming_text_test.go @@ -0,0 +1,80 @@ +package mujica + +import ( + "strings" + "testing" + + "github.com/egoist/mygo/ui" +) + +func streamingTester(text *string, opts StreamingTextOptions, reduce bool) *ui.Tester { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + StreamingText(c, *text, opts) + }, 400, 200) + tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1}) + tt.Frame() + return tt +} + +// streamingShown returns the visible text of the stream. +func streamingShown(tt *ui.Tester) string { + texts := tt.Texts() + return texts[len(texts)-1] +} + +func TestStreamingTextSmoothReveal(t *testing.T) { + text := "Hello" + tt := streamingTester(&text, StreamingTextOptions{Smooth: true, Streaming: true, Rate: 1}, false) + if !strings.HasPrefix(streamingShown(tt), "Hello") || !strings.HasSuffix(streamingShown(tt), "▍") { + t.Fatalf("first text %q", streamingShown(tt)) + } + text += ", court" + tt.Frame() + if s := streamingShown(tt); strings.Contains(s, "court") { + t.Fatalf("1 char/s revealed %q at once", s) + } +} + +func TestStreamingTextInstantAndReducedMotion(t *testing.T) { + for _, tc := range []struct { + opts StreamingTextOptions + reduce bool + }{{StreamingTextOptions{}, false}, {StreamingTextOptions{Smooth: true, Rate: 1}, true}} { + text := "Hello" + tt := streamingTester(&text, tc.opts, tc.reduce) + text += ", court" + tt.Frame() + if s := streamingShown(tt); s != "Hello, court" { + t.Errorf("%+v reduce %v: shows %q", tc.opts, tc.reduce, s) + } + } +} + +func TestStreamingTextReplacedRestarts(t *testing.T) { + text := "abcdef" + tt := streamingTester(&text, StreamingTextOptions{Smooth: true, Rate: 1}, false) + text = "abXYZ" + tt.Frame() + if s := streamingShown(tt); s != "ab" { + t.Fatalf("replaced text shows %q, want the shared prefix", s) + } +} + +func TestStreamingTextNegativeRate(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic for a negative rate") + } + }() + text := "" + streamingTester(&text, StreamingTextOptions{Rate: -1}, false) +} + +func TestStreamingTextSnapshot(t *testing.T) { + snapshot(t, "streaming_text", 360, 90, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { + StreamingText(c, "The herald reads the decree, word by word", StreamingTextOptions{Streaming: true, Smooth: true}) + }) + }) +} diff --git a/table_block.go b/table_block.go new file mode 100644 index 0000000..55ccfc7 --- /dev/null +++ b/table_block.go @@ -0,0 +1,125 @@ +package mujica + +import ( + "log/slog" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// TableBlockOptions configure a TableBlock. +type TableBlockOptions struct { + // Align sets each column's alignment: ui.Start, ui.Center or ui.End; missing columns start. + Align []ui.Align +} + +// TableBlock shows a small table inside a message; it scrolls sideways and copies as TSV. +func TableBlock(c *ui.Context, header []string, rows [][]string, opts TableBlockOptions) CopyResult { + n := len(header) + if n == 0 { + panic("mujica: TableBlock needs a header") + } + if len(opts.Align) > n { + panic("mujica: TableBlock has more alignments than columns") + } + for _, row := range rows { + if len(row) != n { + panic("mujica: TableBlock row width differs from the header") + } + } + k := Tokens(c) + u := Density(c).Unit() + var r CopyResult + r.Element = ui.Column(c).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border).Background(k.Surface).Clip().MinWidth(0) + copiedAt := ui.Local(r.Element, "copied", func() time.Time { return time.Time{} }) + r.Element.Children(func() { + ui.Row(c).Padding(0, u, 0, u*3).AlignItems(ui.Center).Background(k.SurfaceHover).Children(func() { + ui.Text(c, msg(c, "table_block.title")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) + icon, name := "copy", msg(c, "table_block.copy") + if left := codeBlockCopiedFor - c.Now().Sub(*copiedAt); left > 0 { + icon, name = "check", msg(c, "code_block.copied") + c.After(left) + } + if iconAction(c, icons.Must(icon), name).Clicked() && !r.Element.IsDisabled() { + c.WriteClipboard(tableBlockTSV(header, rows)) + *copiedAt, r.copied = c.Now(), true + c.Announce(msg(c, "code_block.copied")) + slog.Debug("mujica: table_block copy", "rows", len(rows)) + } + }) + tracks := make([]ui.Track, n) + for i := range tracks { + tracks[i] = ui.FitContent() + } + sc := ui.ScrollHorizontal(c).AlignItems(ui.Start) + // Rows span the block: the grid is at least last frame's scroll width. + w := sc.Bounds().W + internal.Remeasure(c, sc, w, sc.Bounds().H) + sc.Children(func() { + ui.Grid(c).ColumnTracks(tracks...).MinWidth(w).Children(func() { + for i, h := range header { + tableBlockCell(c, h, tableBlockAlign(opts.Align, i), true, false) + } + for ri, row := range rows { + for i, cell := range row { + tableBlockCell(c, cell, tableBlockAlign(opts.Align, i), false, ri == len(rows)-1) + } + } + }) + }) + }) + return r +} + +func tableBlockAlign(a []ui.Align, i int) ui.Align { + if i < len(a) { + return a[i] + } + return ui.Start +} + +func tableBlockCell(c *ui.Context, s string, align ui.Align, head, last bool) { + k := Tokens(c) + u := Density(c).Unit() + cell := ui.Box(c).Padding(u*2, u*3).Role(ui.RoleCell) + if !last { + cell.BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border) + } + if head { + cell.Role(ui.RoleColumnHeader) + } + cell.Children(func() { + t := ui.Text(c, tableBlockClean.Replace(s)).NoWrap().TextAlign(align) + if head { + t.FontWeight(600) + } + }) +} + +// tableBlockClean turns a cell's tabs and newlines into spaces. +var tableBlockClean = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ") + +// tableBlockTSV joins cells with tabs and rows with newlines. +func tableBlockTSV(header []string, rows [][]string) string { + clean := tableBlockClean + var b strings.Builder + line := func(cells []string) { + for i, s := range cells { + if i > 0 { + b.WriteByte('\t') + } + b.WriteString(clean.Replace(s)) + } + b.WriteByte('\n') + } + line(header) + for _, row := range rows { + line(row) + } + return b.String() +} diff --git a/table_block_test.go b/table_block_test.go new file mode 100644 index 0000000..c5bc804 --- /dev/null +++ b/table_block_test.go @@ -0,0 +1,58 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +var ( + tableBlockHeader = []string{"Guest", "Title", "Seat"} + tableBlockRows = [][]string{{"Marie", "Queen", "1"}, {"Louis", "King", "2"}, {"Axel", "Count\tof Fersen", "14"}} +) + +func TestTableBlockCopiesTSVAndScrolls(t *testing.T) { + copies := 0 + var w float32 + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Width(160).Children(func() { + r := TableBlock(c, tableBlockHeader, tableBlockRows, TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}) + w = r.Element.Bounds().W + if r.Copied() { + copies++ + } + }) + }, 400, 300) + tt.Frame() + if w > 160 { + t.Fatalf("table widened its message to %v", w) + } + if err := tt.Click("Copy as TSV"); err != nil { + t.Fatal(err) + } + want := "Guest\tTitle\tSeat\nMarie\tQueen\t1\nLouis\tKing\t2\nAxel\tCount of Fersen\t14\n" + if tt.Clipboard() != want || copies != 1 { + t.Fatalf("copies %d clipboard %q", copies, tt.Clipboard()) + } +} + +func TestTableBlockBadRows(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic for a short row") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + TableBlock(c, []string{"a", "b"}, [][]string{{"1"}}, TableBlockOptions{}) + }, 100, 100) +} + +func TestTableBlockSnapshot(t *testing.T) { + snapshot(t, "table_block", 400, 220, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { + TableBlock(c, tableBlockHeader, tableBlockRows, TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}) + }) + }) +} diff --git a/testdata/snapshots/darwin/audio_message-dark.png b/testdata/snapshots/darwin/audio_message-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f9872223db295fba05b9341ce16d1bc030039c41 GIT binary patch literal 2809 zcmc&$`8U*!8vj~ujY=a#DQmWDg=C4D67SdsL)o{+p6t6aLo`Zut2ANS_ zZe@nDR3suhV=QA|X0G@B6Ye?p+;g7uJfB~l^XyBiwIz)InAkA@0QgN!jBEga>((K* z25}$qPaQ==QVVhR|A$E}68#6O_#gWWzD=#2B8nuzh2pkBz7t=u@}b!E_A zs|L9^NY}Xv*DU;DI#T+nvFb_5{G6ZAhd2I#gQ;RAeM+fc{EE3wmm#cqjmE&Z`(ai? z*WQGyC@;QOzki;wx*dCU~!h&(Kw;5Y9w$YV>Km#PZ8RUTPxZBUkh-` zzKr~Kf=%rsJj~4SW|xdQ*L4A27U-eW%@f85VPIaiCW`p2^gA(~exZ$4m#vJHxo!8> z)pJnSwU()!oRp-MZY+RxP{f;dcxEdBXOuYc5C#qpb6Se3`E*^c&<~Q2n*oz|)~9~W z%QnelzTUy%l?g#()4AB>{8v8ZNm3wmR7Z4oP8S{)vUw<+aHlklLlmAM8JCmRJ{Gcx3EJW2P|&X!asi+~Rm0Yci6J zh%A((73FU+?#auTNSzu7LoP#OvL&ZsJFxQox8Y<)_ozm5x>O0bK4Y2lCxZIW*x0qjiLQQ__V&+fLcEz2uRA}DwnVwEYLRD7yKn1%r!CB6QN z^urQCXizOJU4{Gpf$Ee(tx%nOGXN!~6Q3*~CnmmmRTZK0-shKnf(| zBBSoCVv(X?Ra)lPs8={PUT=>ztkpg-JiGJ@0UkRdthaSw4D)L~p~ztH zdt;G{Gf%P<;EcJswwSV&^_JuG3zU!EuYYcko)_R-!PeJ2LpsqbJ>2F&+c703*W*$*3i`rNz(+ z=Gm6y;In7;vX;*q-><4?O4dyE9c2RmBt@n??yq`tZa};_Fe~5uz+8&!Bz^UL8EOi zr|cXNp{Ad+6iBnstyiNH0-!kN=Mf5Lyd<~R?X{FN3QwEvFpqb;v=Q1upHaoh-RjE} zx`n{d9^eU??2fjUpR5e2Oe`D@hs3iD!)brfjK3?B0-h7J#DNaEJ3^ic*koRwccnvl zb?ubat#>fGB@<8cR?8(-Hg^t=xy2mF1BzZ6Gw&sAvc9U;h>?1FBi=PClA@g=NygPpkoY5W##O?4r-`IR~45yX$2M*$^v;mddj03 zl=Q0CgSv>zOyt$Xq)YTHns-HSe0?PAkMg?QSH{Ox?34|DzSwu0UM{Y@;rj{@p7xYD zCLFdE5m@gqKmP++A7{d>G3j^wHKZS~Yiqi|TW_)spU}cpC=iorX z_-xS(qS_7Pa*>r#pcf0Fn^Nn4J^oifU98LRBiEsk#T>Oyeyra zZfvEeuV;*Vz+fpFhSFJPT26r>AzymQ=t4RDtvc(kZ-jwvzv#b z&v3KNpw0M}N?zM2O0d`~u|T2I9%uZ$4W2^g+Jt>}FUpE$woRYnpY~*eiK7TRw|eC8 zZ;>bRs*W|5kDyt(U_g&TZCj((k1o2uAXNo8Ja5uD9Qf*m{Igb2-5aOlspEq8O9rQm zaQ>2j+47k4LH}zl-Cki9viRE6DhEyr(AcAYv-UBt(D`%G~{W*ov_` zq6A}h(vpukBBA^sRF?D3aI=R!zBGsFHFtN{&Pnp}mAnIf-dIQp#Evh-44 zgL?iC9BOx`^%+f5vw!2r zk%M-fNkb*G+uC)X1XHdkzUjt^_Ij!k>coKh2vJ6y;T0xe#_XVS}~qn96B zf2_GW0x0Y(`Ov<(vvd5pOz|8@)Y?j_?YcNhf)w9}$P6PiF4Mt4mmE0=WnZ0cJ-2=Y zN728#Ma5X~HgxSSY?AZ74dFR%w>JoF4|XvIHWBx9wM-BSd8&4CJ1;J|y1Gh`d7h++sqI`$(o0Ohbq7yVL4qa^*d~{Hopb`8U>yT~YGAo{)u1R((SOK3 zfm;7q3Z#^v37wbiGSqh*-}B-+JNtzOFe)f##{{13spqwW9z literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/audio_message-light.png b/testdata/snapshots/darwin/audio_message-light.png new file mode 100644 index 0000000000000000000000000000000000000000..8c92e8e54d205eece0f09e384f22c7de7904f282 GIT binary patch literal 2802 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 C*BDO# literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/chat_container-dark.png b/testdata/snapshots/darwin/chat_container-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..850e4c245b1f2f211c747904955d1ae88af4a189 GIT binary patch literal 11397 zcmeHtbx@p5wM3D_suY(^l7hoADb z0qDCF%$ADf)yMJPk?Nm#+X} z))<2r7!qYc|42I!PZo>S@mpU;xxy*J=!%E!_koyFG=o z5_^A+X3Qx9Umj_#j+QRdz|PnJJ!-#P-Msw<&T@WOG9o}a8g8F_lM(SR?`%EIUS;IgX zJt`{7+i!XI*DuMWL1P^c=1ofP``@wFOf$E(=T;Yh-=@4F*YhlBYtMHyuq{=n zt2$I_9<~jW{ccU z>#UPjJwa>O~vaUmruC|U1ah(%6Z-g53cy%P{ZeXPGcQcX%_!IMW`DS zgB4f~dNR{f;8p3YE%SzTcr~_VH||`ChIZ+jY6RS?kwWk9^{nkq+uQkF?{s9Wo~I1o zU5+H^G%ps78)H3xJ}Wv&E#w)USG;d1w7wjJw^<`(0O-1~%%e}?Fk@vAmqmv!42gwS zkpRVWJ3UtdX>xKS%m(5qUXv;vXkRk+%SmZz^pxRd1CN7kM#xwdtcHzeXQR7+WE2lY z3S`Hog%pK%z9VsL&0&`FGA2GI);Jw7Yy^A%{{7+773$9__4LBVjx>qDw0PY0b+aE^ zK0`ZK&8)RUmB}m}+OsnQtYt2-nV-LJK&xd;nE&Q_mGNx=*Z|l`&C5?gbwg*eSB?m6g#vO8mUUT zaN{B^U8m6WV~f!^vb$t^T9%~_?G}L9!W+(1OMq?bi zLnDSj6ml{*;Ve}gzfBBhBEIVs^)CUJxB%TU_Mok6o#x>r0xMnEOS{qi9O4tPhk50k zS2x|_z(bjz|0=-tjkt;ov6Xtp<0ff*p06V!%2h`9HF)0Mn;M}-c8h!5|MWYd@jrPC zg>ZiPs;6xfQJ#SRawlt-O5}!ZcucD_ebbWeZq1YA9f+uOTJ-$l19*4|%FpH0Bk z)itJAd;YVzb;ZgQ28#raKw7cn-UgdVbY^3v4z(Lh!XvlEGjNbE5;TPuC{Q`hb z@Zb4AM*_X!9{{oK!Y|(Ho2cq(#I~kVGN?{rAuR$8vB3;Y=;G-YhVb zNyKeI7B55}bcfZgKc6~o5We{knC)bIkw$EN8m#s{v3=@7 z#TNks^#6O#BL=E;(1z6sNAx;0E{rfFecy}5i_RM$J|7^!iR_lHLop(Mh^_B%oc9;l z#-v3gpheUaK^?5p-*S$7)(P9kcI6^!d2|Kg6U%52nam1ji1bq608kjjI2hcl`xB_- zJz=KU0zv}{UybctdG|GY5v$(?_cM= z@`x1}EsT5rOd%LWA$UerYKl+n1?C`GR9_uRXiMQR**_CeY!2WB+5yQ4aUzL5=?p!Q z{|vQ+1C+B+&T~{=DD3yl_@kjcvydKhF6v_hpd`}&MfSC(EhxAe8yfobP*BKFR?4Dh zNScA)Xfr(&MR;zr0qD1m9vvF`y(T6_@)VcHl)#u04z-)=sa9V1HyLXR{CL3lcaaxc zndAMV`YZ{TEepnJ_w&TnaM#r1c0i!!^qZS-E7rgs zaC&!OsUkt77#4ALlGAy*JhSI+b0j`E@`mEo;hxu;5##ee;(}))R9XyxYO-GWdR2Sq z_nYBE5$Np49c5A~FP82a#V?J)M9ks`02Cw$TPYdC!v$M+TyHdWAk-! zrTQ%-g3yK$T6m?ZxM!esu-atbpF6cxT@72V@v+g+ILmIQm;RK7n$-81Jbi_=q;&sh z3YyXlF%6M)xyjM@vP!Scb_S%WH8CTxY`SIf)%Kt5J+?8=QIO!I8lBhMtss!|$>YQl zEz>6C#Ayo}VW8gkmHRHR0jCPt3HjCs&*pQ;?B-*(i5&Fh)t$Q;HQ)PBt1g+s9s+RAJ44zryqdTq+!oG)L(~~x5d3SI>zI2JM+9=b4 zSb8K?yyM+Qo4%Hl;-FrHMZiHUs#OjJAnf-XpEzKRSwi>n&Avgz`Lv*piCBCFq0Q&WdiDrH-w=yC#VyBupXkV7^(MbisOI5Guj zi*oQbzZz&kMzYDU!f||YiV7JZJ3A}TM+}U(E*~N0_CzHd${IuVGI2x0)6KA{sme0W zr&kW!AhK!Wgm_b9L9^+Z(CqGZtt7olX&Z1X_8T*tbwFnE9?pcRV< zA_DHS&Aa?KG5nFkV=*)gKq%6~8nwiI&0gBjI`#n3{AlTJPb8Uyx$}kP{->3#S1+C=U zx6^^@EnbzIi#-Z_aaX^?qR9hsYQc#Vyu`FLXBsJmxdKPF;kXyC!jPFIVAPHHM_H}7 z7=TLFZB>w{SZ^S5!1UDk@2h7}l2r$lrwYI6LRCC;VZ+6H`@vdOzL#M{qAdQWzb5lO z-TB>6rSZ8(O9NozL0*yJW4{8S?z!Bw=D!MKUx|wc*lENqly;h=NZ`_KdTnO zg;K_Hvd)Q)Lw=oihEt}LQ!;x!+RMsXc64H-KCpV0J2~?_{PW}b@0-^qSq$RG?$Ec= z<+{tK79kH7NPlBC?=isdb#~4O$wqMpd&@Ir&M-lk=qgeC{X3PA<=cxhu8R16>tpja_r6{KH%m?& z^wuvzf*mIIvOZ-Crk!2dlfnG{!In5eW@@fZTs!-$>J4ecI9`Um$>u$tJA#IRs;yo} z$>#OP#~4cFs_Y+lu;|R*p5>E8SQs?uaZaXAO3BHov6CokX-$KEAcO?N@s?0DsF$dn zuLk#+9n5+99y=%slkza9j&srJNvFo3ws3C#VD~XlXt-vpzy0p`BbW52hK}6QmOdX* z%@VzLr>8i5UIq3C6f7*ISkJl}7EsD&l{99)3~mW!zKF=RZz(A$$twB>72AISl;}h( z)8)WNFW%@zVpJoyt6luQy~5<5r%jUbJC7TgBK20M%#BlAQ z`NZ3AxaoW~rq7EW!p%`(*bIb|-Z@-RBwWYL;t7~PMwS~&+?EkB(;I+r>UZg|A^r{h zc@P7iJ7Q2CcAkaSuka-@szu}a=cK22MdHO7(@)$_i*`%40+hb^1grEUmn!}`{?`F{z0pl4!3 zzO!?kB0Rj8!%-G}W5~_f$^i$a1=(sPD4Q*ud~0-dii@G-$+Vbj9hpd_r#>8g`dojFcO=@@f~CHBJAGcOPRB*b#`I_)b9J^7@zHviy<{@lbr~ z5&sK<;mw!?*D^l?6&7#@uO30$HEWm#G*SAWvF}^Pp9L%oPK0M+Io9nTSJjgKQSPAJ zpI5{K?)PK+SX9^@t9fA_`MFo%*BW0ywqOEI8Xq$V!@$3|HW_DKWVQ~td~FrmFg8*% zflhuU7~7Ph)AVgKu(&S(1j?^$%TK8-PoOW5P|{%5J6KQq$eSz(!Mxf46hiitf4xau2Rwu z-avp*QiK>aYPp3X_ZJ>1hN z5=&ra8puc|XJL8&J1l$Vc3jpS(o|v8`sKRgjq_^ToJTt0k%!bHA2g22>dMEH@-M3Q zUnGUe+x&WQW8SJ+7c$VPQR3E7+9)wzg8PD4uqS8l?2E>$a%DjJB)hLYx8$eQ^2V#e z*>}#5dMb?OrcCX2ew#Zw@n&8N)02^`dg8mmxnJ?*azw8G4#Dhnzf^200-}g`$lPu1^%3_?R25g zZwcWr{t?N5_}}!rpwAXEQ#{iW(f4nzyMIaG`>o zkd^wh`<0#D`9F8xA_U|nWgYS4Y&$LE1355IkC*dG^V$adDrVXUGK6~*(rsKW4UTt& zhPxdLn0|I!-`;&Ew;QGpleM%QqK290sxw9J20(TXoho_M58Q08OQwT>^hHy>ZC~?V z+FcF7YHN6Pw720Ov}1|nqzumV=5%sjpD})Z?&9wy?OrDq6CqZkqir1lGx0Rf(Z$IL z0>lsj)gI(X?p zLXhi$>>*?XBOvpL%dg`gSsoyNMgbVi@JOG-Bd?;O!r*+L&!lRAmk;{6fs9X94>DALP+H0Y zy}lpK+9D@(zqpI}i4nKek+Jg;Z>cJcCody;=I{Y#3`dsO9Q_jL+i7rfiDfIIK zMPrYBaeTym>Z{~xw?^;L2eRp+5fYk`*5z%Q`KOp(%Lw_N{R3>A)Ae!zYdn*Y*J%dLK2<7b}L%xeRF2$qqB5fVPWRTdKEBqY(jx2T~kxDw3J@T zGDMJS@96lRh-ks-8tx1gHNSa@{_;?BX8N$LfgQ{yL|;){90nX5C~g`VIlN}Ed3JBQY z0iI#mXR-@y^ySpusJ4W9u*s(xW5p~qA0S9GF$;N|xR{|qs|PoeeBIsiQ&4+JNc#Lc zendESz-YMzcs4IkZ?tHpP}{yr2E*1n=X7*+Q<9S_4R!*UZhM+1xO{w?dzxZ=eM9l3 z@e>4I=5NB4_ZnSie#Un!rv$DMc2Unvb}u-2(U)^$F1(>3CePywVh$CaJ-hFvFNll} zJN0h6?EwXOxG#d1~<#&belMf7+s6W$&AGL2+&tagL`3V89|SMb1v=;j$jx=DxjE z{su(4dw0~0m5I%?$y-HB>%z<;0WwDE@xJD{IDs@o5-yo?wowkV*tHgRJiv#8Ym-XU z+DL3FOB?%AFu3VMn%dAR+DusYWLHCcgm9Kx%hWUtr)f=GdRn!9c>}vFx=9Ctv#mpM zWs%<{BndsYKv@*53ob0QbmA2~)ZD(#g5pcZbJzF_l;e?3>>R_duy6)J$;rEEwWK9c zRDTqubc*ybMcEPcJKTTy&cB zfjrL0AG;*o9AE3`=uA!#uy+p(4FG`*^Ya3N!VB;PE-bu|{8ev7O{EB~Dn-Q`lOiFx z@En!qjARgq$bE}J`fXTcp~^ypS4tro^>Ru{ z1MIopRL*utg{fOxXD-OV6_sdD}Fo;nC0FasftE022v8JJ}soVZYPBwKdC6!Yo&aBwVrnQ-653~`} z$uGZ<5Qa^jSC*8Nk&^+X?5=uB59qx*uj~h^rX5b0)mDZ#pY7F>hW}Jujd?5*i=3Y3 zp`vu^|J;!IOYnHvm65|_myVS&bSn%+)D?!`h|=#ST-G$TkTI5Yc<*@4$HRvyM5dHz zjpif5MSVh*dWY1(#5-j9>V>{ZKq$DFRWIMHjR|Z<&rK3Ddrc*SJ~&W?db=ElrzR{1 z(-I^_y-#%|6@2meMpxL7+K_S9RE~|mY{wiA{hgxMhk7U%mY;Z$dQ%VR;3RT;ZK6JLU><%V5agHxyJ);v6s?R4V zP_+AYjc1ajoRVK$fkH9INn>Nd=8KeDw(-j4`&fp*-mA0`)xLwCBg(XU+O6~1w>a_} zk`w%ntUSav1x1VF@GU1DC}O70WcZ1k|HG&BbR`)p!mPcl@8SJTt-??jW9BGs5An*6 zA6NXIprs`htnBQLPEIS;2f^x0JzP$8qf8i?#qx7nlb3FXFAn@}YuIA=(d}-tF;Y}7kNUs3EX|{j>6GaWpigpsq7l}zp&Mv$4dI{<2rEj?w z+NiMdo!MSsmHVxd`aWs;KV&T}`3fWc?G%cmfLGcc9w4Zu&uwenuO?+-0w0_OonQVNLWB?i{_2c3YQQrlYgC)%}+ce0?VRHUhUM|KPzNu2M&QA>|436q(ieDPjgLj zA)L(2ZY17gEG!&ZLdzLrH>%nN{KhdOj6yS)1w1~?W1|^cs(3#KiUnuRn@OUh<8Cff ziHWM{0k6{F?#d8Gask+sDdNbgB%xdd|#)o}c{zXDm#drBY zuQDpmQ=~TPpQxn`ToTjQGJ=>(G_*aa@WeyT9Kg8G6Oufz_hA%b&*8&Ao8Rti7^zg`j&zdv16?3f(va;HOH!n9l=&w&S z1#YPU6>Y7p@0=Iq(WB}eliAL)bCQx^TP;Tw>5NfKnzL~8lw0b^>8eajlKenSl3Oqv z-y(?|M>x|p{+PFPyc@?p0ZOO=4!V!1Ea{bxI@0tJO)>0*vc z`(M|9QF9gR8X6jRw`bET88?_Hx9K`-JS#=OB5;NAk-XeugEMZAoB!lbp|zOKqJ}Aa z6aWzcP_aR@PM6)uK0(86Z!bQJAjulk> zkPk%>R86!N_!*Tm$JH+7tC=YEt@6$57MUcPqDj-D9zNdjf$`k-PF4d0gJ;N&4i1Hc z;vFxrB5GPmyTVBRX04U00e`j(}edw^Jntl5`+`25(*|E)Y?L&z(tzu1KVe)m$;!j=Z*^-EU z354F{sN)(lk`Jga^YKrS*b#AQKf5wRNk(j@txe!5l&Bhd9C{43UPb%M3Urv#?uhxh zG2QAd{Z-$yik3lcu7&L6aN94y7aS7e(f9AZ|J@S=&s?LGa%EeHc6&dGY{8@G;50l) z6*hAur>N0qLc=XyzybJFKo?dCWDJDT6g(P6qxu7vu><}!P_@>IAoQ2<-?xx-cL0`L#}w+;`d2Y=0D3W5QSPFIS{Y-@>5$I8I27WuS#)w4hqVNv5m=0VICm+3F3 zVk4j&!pisKEBP-a?3@QB?5})zB6X!@m*g{QF{74NRbUilB{2uZC?Z!Yyk8$93j3yp zxE(~{e*wa;%hVXaXvLiF{ue0s+&#hLc>WQ;crF{oWzheZi*O4r7-qK~XYdlG8zHWY zH*H~NVge0>RMM4KAHPaDV~#gFVW_UIEbB<#)$Z-}?i=MA)yjzn{!QdMG)OWW3?fRD z)>Ob$1TX{ty$>__pohq92_Ouh@+2cHA&y=7Pe4EgE;ysV_0++}?te;+6|5rvVUYt@ zsP_GDQZaa5@esC;8~A8{mCG(r+_Z$i)4a)HV8-qNv?$QKqdf?7{ZG_FEQY2IiQfFx z0o3mHm*5yIYU}pY9Cp5G&7|Ps`6sQ8x=ak#etFHCH<|fbgQ(sadVXDsiO?pGMD!}yT-TDI4Z#8+cuwAckf%ZRoiXO)N>>h(B7oUYuX6!BP#(*ao5WFit zj^^wKo#~yVw8xg8_3zk?W5yjfF7BSrFcNA1WAoG!tr!}@m}Ea9;{t&cJZuI*QBGC% JyR`Yoe*>m2$I<`* literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/chat_container-light.png b/testdata/snapshots/darwin/chat_container-light.png new file mode 100644 index 0000000000000000000000000000000000000000..503f7a37e5c1ac63c25f77da169b4806a67f15d3 GIT binary patch literal 10862 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 WmpTdpm+e0e+{j8QNmPlM1pf!KMwUMS literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/code_block-dark.png b/testdata/snapshots/darwin/code_block-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6c41a3580c96c9e2cd87fd4303913d9c9dfd642d GIT binary patch literal 11803 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 bZ2~aWY{Igd7@c{I20%_)S*ljTB=|o7NsSzf literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/error_message-dark.png b/testdata/snapshots/darwin/error_message-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ba389db18399f1225a960d4b52f09eb1c7263dce GIT binary patch literal 13200 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/error_message-light.png b/testdata/snapshots/darwin/error_message-light.png new file mode 100644 index 0000000000000000000000000000000000000000..04a134487a8d32625cc5e62ebdfee0c24ec64c2a GIT binary patch literal 12731 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/file_message-dark.png b/testdata/snapshots/darwin/file_message-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f3438b91e8045c965f48e2d53fbc2e9a9cc9df47 GIT binary patch literal 8517 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/image_grid-light.png b/testdata/snapshots/darwin/image_grid-light.png new file mode 100644 index 0000000000000000000000000000000000000000..a622891c7be75cd669f8f644c8a4b95e497502c2 GIT binary patch literal 3925 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 IGL{kl3mO=^NB{r; literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/link_preview_card-light.png b/testdata/snapshots/darwin/link_preview_card-light.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e45ba7eb9c4239d60348e895c9c3c7747bc206 GIT binary patch literal 10454 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 U;<+e5zgGlEiOGvriWmm|2T;$E2mk;8 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/markdown_view-dark.png b/testdata/snapshots/darwin/markdown_view-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a4d43d3231741947b12b635c6c477d034c93a2d3 GIT binary patch literal 29310 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_actions-dark.png b/testdata/snapshots/darwin/message_actions-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..878585a0fe6739a9f6f57e92bcdeca5d859e9573 GIT binary patch literal 2824 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 W&c@He{89>=Z~^o+459+=^yq&r=t3R< literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_actions-light.png b/testdata/snapshots/darwin/message_actions-light.png new file mode 100644 index 0000000000000000000000000000000000000000..f31f9f05d62f696a320be5044b89ee3cd307fb31 GIT binary patch literal 2783 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_bubble-dark.png b/testdata/snapshots/darwin/message_bubble-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..71d3f9ad370deb99ba520dde4404e753a4c0f8dc GIT binary patch literal 17251 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_bubble-light.png b/testdata/snapshots/darwin/message_bubble-light.png new file mode 100644 index 0000000000000000000000000000000000000000..fd189ccd2b7e84628deddf41a32e3f7c516e9c0a GIT binary patch literal 16414 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_editor-dark.png b/testdata/snapshots/darwin/message_editor-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b61c1ac98c724f3b5087b66999863e20dc5e7f0c GIT binary patch literal 8615 zcmeHtXHZjJ+b&H(>Aiy>9i$f_0t%rMdau$11f(Z)6cFi17X;}d9qBO?kzPaZMG|^K z@BPd3p7Z~l=bJNg{(LiQ)~q$N*WP!%?(1H&qjj`Yh#xaN#=^oPR#OG)VPRnxVb*;F zIGCBo9p@Yii|w5n@RfmIHaZ92KwpivmplKXpkY=`9(!xKiHDL7IW;!#V?6^ZOAFmm zYQi(CnrfGhyE)^U(NtSSV43(ML!~GxZ0F?9M>z+6QEbtDRpyJ=?VD)-{Smv3k(_=Z zv12cbR}M;?csL(kuE3%$9{eVGjbDR1i;cztV*%bX|9{)=!NYQ%gp3)-i%+xKRPb}M zDRbcPV5efKW35{gt3OPco1IKf($ES{O1y3(Mk~{ymbFKI&$FaBXXvWzhOa!LCmN7U#@?1O|evJ<^o^AcJM-8qHz1=%a z3@7M&u!iG>4ZVBgbu01k|Gh0%Xu!$I6qFUcL*Le&Y;B(i2*knKweWCkuBXXh{nO3J zli36Noe>du*QVW}kHN17X}G$rS73p+1An_qwD(|M+q0w$Z~az{qh7uQr&ON$ zd_p8>i;9ltH~}zkR&l9G?>Vbv>#WB|CTM$ok#j~y{uKhBEzGTq^L(0{H=(EBK|t<} zD$OakM7{ZTQp?JYfD4|rUZQ?B_l~j9%>2`fF~3!*jIVbD}&rl=Bj-*N}tv^c2 z<1?_(%^mL&Hix9YiCNR6<5kXXiF@C5D3%zcQisacD!zP)6}r~W$;qjNF#sQ*I>rW? zAfoSc+FkoE?``KmjgnGgfA-t@T`#A(xVeu92i_}q{apD&{k3T?BlWWhc?*Hi4K0dM zP$#&>qTFRCUC?}H^Z=X9XYY^it5+TGbAx(KO}S)MB}GKOppftEe@tBD^FG^En*D&> z-8FFJ4rCQFCar!yJuMkATKoHfa^$b$o3Q7S7O^w|pn7I*A)&8nY1}-0(Icy!AcYwL zK+ws_?S%>fo4^0u1b64VrD#I=VCYIbMKoT!Ka2i%O`Xpf*(!m@r86sUH`gPiJ`w2<*ApA0KwscgqmWOe8a-JUX^u zmD-1X@LO`igatpp278hR0O4w&`KMo>mR#ZOTfSB9GiOpp&3uDgS;#XPF6;$U0a6QL zucFMnyq5vWBX_sl%QI3Q93_wm1j05$kSX|EdT~j4Wq!UgSH_;a&vre7N&bB^|XLoA%*L%d94Jhot$f)$ZBbByuS;h3T-Lhvbs?;XXKYQ%?!)v z#(228qUeC(X%(AI^uW--)ENCycx2W!y8DzkzLj!>pNO7*yKWU9sHA3Or?Q;z9QJ03 zq>(n?`AqWbG(JC1A9b>Zl-naDrD2n2&NCpk?bf^lvO<7fv!_s{6aZnh5zYK6Rh62O zl0W9O@kV*IL?{>Lk)yqH^@Y__cYLP8DEIWz8`YAb2_3D)aXQk-jh6pEkzn1KXS&z9O>`6$?T6t9NJAzruMIm%a8w9 z;y@|gEmNrve~gvt>QaL2TS$52g=J+OeYpJ^A9sq_lg-n0j=gOS4qmL;HTkP2&lgYQ z$*-)V^TZE&??H$(uI3XJ^(IruG=)Bj);@@tI}?yN-d*6MiKS-DNX^xL10!R&NAloc zV}v#`VrlkAKE@;>ds2s=)z;b?Oa|MKul~;61W0SG8geT*EzSRC z?KSP`mW_%A)iOCZI!vfrjZBq?e6w}DMJTOh;W}f z4Ac?13(@N8QUC}d-az35R(skMoJ&w|Z6omSUlJeAz?5VPxJAVnt1VfSY;I65!|uuU z#didcj(pVMeQdO8Bu&C+U)E;8Muk^N$R;dk}nn!4q9$bppMRIe<<~L4Y9`J>D zza-%0=TE2=ml~acvflsr>p*@#c`73B&d1lZ``p09B#bNzg_6?@^mt<=$ih&84yZ)e zz_m@cT^7eUX<|Q;W!Xx-($ll`e5tx~ED$lemxFS3tH6ZHVaY}R+}JmT(#%cT$?@@k zyFc;e#tBGH%eU{|`JY_OJ?Gc(6d9XtOq7%~Iz5f&g^SW$`JcgbnRD0(@{!zvEq%Pr z-sg9HNpE&p7nmKNt81u@91{+NBnb%!_#7_UII3LNZC160+=2%ianF&g4fXZPD%@5t zLTCGQgl>k`F`1ge9iJE<`1#}s)UmK>e^E?AgiZ#Cnpqk7*&Z5&exRkMb{VGBE=(7P zKpg#Dzq<;Uw=F2rEff&6j$W+Wr&EZ+2rut`HkzL`ThtZjGSSUhO{P!^uS>^w2NDo7 z{dnz^hirE;1i4w-*z`|7D~}`^D{3iMCYYL$!iZW{?Vg&F1`RoFu1g8Vgx8GB#JKg= z7ExtI0OVqeo-gg{uWn3zs=Z@D^K&~}N;_n7O@jyW6Bh>1B_td~bakI9E@3bTgCZ&f z>uYTmuwO3GYa2T{N~PZbq5du|Un|h>QN~)$zqAq)KxhG=4ZfC-kdid|@2bAZ*f8Vj zG*V@M4&qwY`fNr7!Jy~u&9A@%0Knm)oGJa2;|7~q9#%F%H?tw=3x93W&E#Y+SDyIz z6EcMoE$h483;(l?jT3BHOE+w6Y%ASWiTsuz(V01l8heoQ9O?ruwe6mjxaeeS5GMs& z*K6UHpp3Ra<8U%F=Gs&Y0Da3H=^cw4&7&ttIVOw&?@Gsf!m2?%0KDOz)WeLAzs!-U@LX?o~0ibq@dvd8AVfp^m~Qss_t$q zXMcZFPtTtP2vHE|bo~QiYARK6@f-me4GrSQLF~Z5h@&taI=144D{kz1@1Mns*|8jS z!w?oPnClbq_9K8C$kB1r24rkJQ&z?^H92kb2uR@D{!T*!UR8~Wf!1nwRe0Y|20)l9 z$3dAGC+Cu9q@~44@#JPEDeLzxZn=$hO?aOtxOSz%ZudR*cv+b`0UNoE4O$6wKESiL zj7tQzwJl)SDI-r3nJrZa4z^Q%GcqfJR{hn3Fnzdesa0I;2!Vj-4a_6Rs{e7Kn5}l% zWW6g_N;DZF?La89!)DvhAal+?GV&KY`=6GAKfi`Fh>2O%2WQJa?d+I6E>_05FA1=2 z4T(+Ki!DV3hM1WzU~1f(=b9nuS@g{>>I=gYRIo9wfnog5l~P<;`4KS7#iy>Pr&m!` zMNLDqlLz_xZgOmF?D#47`+LTsR%-`>$B!`p$57e3a@M@pxK`pEAJ6K_WfGmN3S<}I z%L@KGwXaOBpshc?dHFzEGuK{vJV@=gg}ack_C6;&hmB0mhheb@otX!*>+P=-Fm5_O zB{ntnZ)n)RKIe*6FSd2Bbz-&@iH(YS60x(hJTuwWo5e36P+C!Ra-$&!nxH32IQ4Z$ zNtXa*ws3H8@NmXLJ6x-L->vvR2LM)jdfpCgtx~%6f^y}YJzo+SZ*J?`gg1!C#0q*E za+CdDTIw(`DeWV(^k=cp=8B_*p)oo-Ne3QifrrDM1e{+e&3@3F)Q_gNcw=OBkvWLg z*EOY=4c;v%5N*kJyZL%oFf>H;lTMX`T%Ypx=;-mo@Q!jV%rdTGaRIU)9UIF`upsUg zQCgo`)YwS#N#ELi_Vu%P^CqOE+oMjBL4l!oGKFqVbUv%Z026?eL3V)2{JpqDgAR|x zlAd{Dq21R`MMfU=^q{@%Dw@7++F&7 zBR;(RE-Wriys>y)$H1or_BA{@x?{{54wlhTK%|67?d-1a2nUZB^OI+0LOY*nk|`8B zRQD@6u+%XnGR=e6cb=)Kd0MkJHKpxaEYa%mOwB7cU}|1lwfn)>%?~plgt-Dnz;A@V zUjW@svl?c4?_pPv7Ni8kW@5jP6l@w6d1GWa>(aY368y@*Q_AN3AGQ6Qz_OlVSdqB#&qw1?6+sM;fx-dU9} zzu0oo@8wSRG)l++BPknl^uoiR=kB82ybHc}XVBWmYtj^A>s3*N(Mwi1+R$d5ZNW+j{{ z;7Vw!t4mEzUszT)d0ldVaoF6PKRBfa09ZIj3xMNqn+n6*I6QV^vvI1W#b&Ck_;@+V ze}zCWgrmgFPkh*3)I`7tQnKZv4C$%)U1R<*{7{_JQpwbFY5+@CcK2(N2!_lbmZd18 z2rS!Sm6dLwzHtzvQGI=5rCD8$dhsc^v9WP?4x*{9?gi@8!!X^%+@jir;t6N*`J=zY z_Fkn;87MgRakBonx%onk6{{?GWBzm@@}Ue{JD!ts8K&*pUHpiWMRvii)tHog9-0%d zRoP9>pC!4dGDRcOJmhSiM2uKB>)yAqz5ZF9FhZuBcw(C*mMOrM((kfyLe=ZK&D5a% zOq5Qu<*4c#f-+CL6q%b_<+$a;m7=!qd35Y96;@j??wBCN>l5uhyUloEnajZsc>TWK zz`@WLuJ~z>mW0_S0IvVgb?S*ayM#p3fsENskg^F7*bId}fOx3CdL@WZ_Ol9EEyz{y zSYdTD00JX8*cZJyr!3hNqU;#i+1N&GMFaOiHP)6sp;!S7XQA&pwR$h3fE45u6o?6k z73f@7Pmh9CzR^2-@}NxL!h-0h3De!O7JCv8C+FPa;`njOTyU}m2ifztze85IIeEtb z3%C0FA_&Uy8LpP5e=Z}uCcAw%PCOeAeufk!xNsB{aPXzYI7G29xS?p?5uQ zu1y%eceYGt+vEGixt@fC;#AVCV6*LA`TGkv%iJpDPy~@>QsAu<1Qjj z8XoOewtY^R8QMm=uG-zIGS8^`5ff+6d48L_jV~)JYxCKDvvXXrkL>;YglsDjbsb^p zqlrT+L`OFpIvcgp^5CEAC47Sa-W{3sBiMbNd2jP$99=qP@BVTt%?b{^Ep`DnOL=N) z*prC#=&9Xax}-11E0H$X)+BRD%>Og~odtHy!7jYyNkU?Em9*q8yq8xd=eq02&W=%p?!gpT!m;qYKZTe-Iyef(H z5;HEfLb|lj|HlEQhj9l&SSBZ-=h!z81p)vz^#ckpCv}WXiV1ZE%3up8#wgzib!}t? zFbj4r0V|@sN(^728k**ptC}j!76wj6ES^1fs(z9;I>`Pc@B#ZPf@m^|Xtj}?_q_>m zVD+Y&pmsRxn!gqcoP0CrDEl~@8wcp&TqD4!-2ORB8xw!3F;&nY|SN>yDQ?9%-1(%QQ!h z(L~y`yja|T6Tw))b(Tt1(~TBx_7T0p$O<<{XiQ`bv!I3E_c^^Dav2Yfpk6Wbq$81YHf3#?*)7u=&!^49^<4*!^6D-|xU!--q-4{}j zvxuI4;7#vNu%Vl~;ttOk+GN-Y@U=T@g!o#9K0L>m4I3s;3hPg~9Y1&FjP&p&-^xd&i|Jn zKYqV+R;UOMzx_U0xe2EwADh~k-k`ZO?p?NSLm$>2@fjPhV%m zC6;`|M)lV4{^9Fud~1*ECXYpqHlV@bl(T~(AvtyEm${k3R{z4@mLwa6^kmR5sGn4A;L6lV&4A z4}J?q+E_%4*#7J-TkAq3A7#x)ZYPZwQ6B==kk905LLE(J!=<7Am6bCT2wL}*Gj{Um zZWj0b6%i0as_#{Hn|8b0`nE)QhIe3{hYYHUVyptB4KfvF1Z`M}&tK>#5XSeN=W6XVj}N z;IU@ScDV4EPf5(mV7fWt!lazOiPrqXoxC1^^r__@#MIFnzCoFErF_0)IM`*Ic=ycb z?_YeCgf|pp(be+V*9Swb^q*{L;oe~IV)ORt4Pi!wF%^+mO!S9yn!(jJV4mfW=I46Ww*T5405F~)#MoM;9lRjxqGcL zao&ApWf=@!rQC0m6zOZ1gN){y-iwO240QnpN-mUqTo@ePL zzOM83x(bqpY;3YrKOjIHcbt6eEa&(XVT#0k!R!9~R^vphA5KFPY$Y;|CYa97&`OQuwew4p$)|#sP@dLGMpiLmYvLv?S zE^!Ovcx{zhczvP9razGQjXiyxup(M@!B=siR5TG?#h>PD95^onm1p_Kv{FPhr&^aES9rFZfnV=zG28CTwDvwkF93RGw|fhI)_SY&9#hv zbDy`zMP2dJU=U1iRDSX_hkAOZP`2@iGo@pTEe%gjba3&*k2}o|9FhTN8-_%klexT4 z*d1E^@k)5Ap8>p*qtF&POB`*M#LvPZ5C5qxbx**#!H2^w(7KeA^k!(h5RU-4|1bHp zmPsdZ7U`_Hxl(aDA~E~bXbogbD-bg`Yn^L>M=p!f9tA>5n5EODoeLHQs1Gw@G)&Wn z`-e5m$nQt2@4e82?m5{^(-l_Be8vKSUjHcJEGUrMa*(e=+G3HJS-3xSDbKWxK$-^f z!C$;|*=L~rOWn95l2Zl{=S@ewiWxj*_^3xkzo>g$_^bQIdX0zCY--qTwLG_`v1y7Y zwL^opIf^^Sq^Y&KBm;%_A63M`W|i-b3C*oTeUG!h(8tn@uoYl{mT^Ih;psM<+8cPRFtxvqpZgdm?7P7g|ozjRfA4XrKDb*4{dDbI+&ix7iToN!G+=O zMg}Er3)i8%!jMn<$`WwQd8 z6Jd^?HBDk!no{m(p2SVa*X}C7_FEiDUpK@EL~ivh#KG}z1#G8Xm~Q04TP z%P+w=*gu3b57su-OF<)Z`GXj-;aSx)PSD7F&PvHp9m0QL(8m587LVSeYPGUnIe2P( z&5LPTDp}^|ORqi87Ba63|I{uWp&$(&W9KBRUxWHxLv2c-67sf5( z{Nt8?$pg*51C#*80r z=z13H3VxY)N)RQ?k3#l|JKKH`1)DI&HvBYUyw;fMNjI1r%B2_dsoy+Q-YjVwv)6;T zsPnzuJ1@Ko* w{D-;yOWptH%)h3K7^e1rea2B2eFwmr%MLvuqmM*ke!##|Q_=!fDZCE*FK9)|I{*Lx literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_editor-light.png b/testdata/snapshots/darwin/message_editor-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e6090721e8dbaf92a66e5d421b1295d02c0ea008 GIT binary patch literal 8260 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_header-dark.png b/testdata/snapshots/darwin/message_header-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..547af066146855f64c3c1af80f860a3e9b7ec365 GIT binary patch literal 7421 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 tb|Ss{FqFY)lKy0pk1UH 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 RB|}3U$_i@owLtT*{{Wjn1+xGE literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_list-dark.png b/testdata/snapshots/darwin/message_list-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..085017048b4e0e184098b36a471d8c6eae589c0f GIT binary patch literal 12244 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 a$hut;<&wN^!FTK5ke5*bmAw7%@xKA{4=XYN literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/message_list-light.png b/testdata/snapshots/darwin/message_list-light.png new file mode 100644 index 0000000000000000000000000000000000000000..1853340664c715219d6e27063479003b214ad6a8 GIT binary patch literal 11756 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 CFGyzq literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/quote_reply-dark.png b/testdata/snapshots/darwin/quote_reply-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..dae6d61ef603aac45e7bb12434a47e28c55dd75e GIT binary patch literal 5002 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/quote_reply-light.png b/testdata/snapshots/darwin/quote_reply-light.png new file mode 100644 index 0000000000000000000000000000000000000000..9c4e5431a285d5c6ab269a5ab5661a0313aaa2f6 GIT binary patch literal 4881 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 W{#UeYatQ@>Q5oo%fU2|{V*UvohlgSS literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/scroll_to_bottom_button-dark.png b/testdata/snapshots/darwin/scroll_to_bottom_button-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2213efe07e4ed8f6ab1a21ad0d472d90c13fce2c GIT binary patch literal 2053 zcmd5-`#%#3A6`efom^fO*}{=&4rSdeBpkEi65qzF_5002sGM_adD zUELiQ`CoSViBY5x03bIExBbH-o-CZfp+SN@a;t*&{1s<6EGz4ZZ}+=G!=b|9Bak`Q z%^;mk-Di#Xt;U{Lg}57M>C6Xehl6xvf8_?wG>$6Uzk8gFdE3Kj7&|2!y#KVfW`si2 zmwtvsT|XL(C_KS!2!+R89-#;l-9jLW5l02moq<Xx9o7%AvI9!rQ>Sf9F zHz;)KBUDgBV}737Y?4!R8XVEdl*YY?B9Hf+*HVTbv@ou1A6yo}wvuL9G|9T8gyi1j z`@_O~i8=x-8!BV%2)fGY^F2?fR~zoUl$3;&dxvD7wcXpJX+Ep1@cKx#&J3Ac`Y?F! z6|J<@i=}#44xbpy>P_FK{M1|PtKLbn$Qz*>QeUfI{o5d`<9Q=Yg9*ueVGRbGmSiI| zi(nVIN1By6R5Rd8X%VWZsGWvE$}mGKFv!y-o*o;vmsD$LH8^FY94_~)^M=E*8jUNh z2e9^)kj@VJlP}hnPBQ(yQLJfl^BsAC%`++go?Ve-P+96-Z<6yVmbk|nI z>C19uI7JTc`=4X?E2xX^F?I7NLlZI1u9M~HDoizigD=bJxoaRlcKz)H&)=^0mWE5A zOyiNj7RmuTR|~`OVApqxqM1B6p5Qa&pD3VBPh8lHX{o#l&Ktgg zZ%#5l6F_&*(&nHpG#BHU&|;sU7D!(G{{3Ah2|c5J1((^0w15=}xcmB#ba6eK{gc#X zl`s#&@{{3VUYu=9uck=sZn85AIOHI$iIe~8vrC(cq4XQ{tMNxKA78USXUZ!o{1U#F zFjFKu?-N(GbwAjKpCh#gcttOrm_6ntgC?aha-lrqsNk6FNo_SBWxx34=itn`pmPK; zDt=i~Dbz!C#)`I>OiBIP(ykqzBK}$Y&kne&M#n>2xL?giy=iFooauMG=+2i!Bl%Ib z<)^F@D0V-zzzNzF&(AS>O#KT%_}*WD*j;Np3>z31L}OTNXmI)@2EtQ6DGQ3+5x=ja z<;GGYX@H-r>9ee`)l%rg9tk--ni>v53O;#$`CG6D*MDsU!|jZ z@D$M2voHr~pxFW*$8Yt?QC%N}EtSU&IYrD|W`Bw?hlLZTpgH`T?xGt)fHUhmiI}q0 zC2}AUcg{pDvQi`kCRX5XNH-+47q5hWms=2WjLVNKW|HdAGw1Gpw(?6VF?p2!h9`^; zuoOF$^~EhuL74G7GY(99D(gLAIK)S(3@1ke%-tD`tsLKydbie6>e}_~A^OXRO>&Ywo^v<8Yg> zg-pSv68X5;;fb4}Z*E{$3i5P72kL0uhqaz<&>si0P>Xa85icv*b*I<)eMYCbsZ;oZ zLT3y^q-8WE{f?l>b$(!^pg^}C44IWC%-@tg4baYo)>uAI&Tp9(o4b*|9{h6tPR1ip z{;ASM=gkYP0yQOaev)pfAmXonZyDp&R>3}Lp=F(m;UUGoG)G6*N#U0ub1zke*Libm z4Qqq9K8I#Wr$@?YB(fU8$Nx~ti_FqF-P2kMuVwP)J_wiBY7gtf#MpR7jEes>+}-9t zZL`6r8h=93ARqXHVI#*@*|?|IvzXu1I?!)LQ^kglFtnpB;tf*Y^m!z7x(HR`?sc9w zs9pm3=HM4w4NPx!d=y-=V~&2%1Tjr4&D|YTG?d1m7?(ez+xfT^&mNy zA~JZ!N{k2=xCSDMI|r-3{Z{)WWxwg}OWTu>GOh$f)2pXPDBVUC|Cm-yV2}bE&}W|x zKB#47&?crjEY&z2R_i@5{?UHw5Hif;xX4Tcxt60EI14&s?Q2ak-2`GsiTOFORRB7b zaT}IrVM>jtEAwUGhX7qF=>CpG&ORM@+QX`NbxT48|0J+_?bmw&{VPzJkEmmTnVx`@hPm^;G}> literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/scroll_to_bottom_button-light.png b/testdata/snapshots/darwin/scroll_to_bottom_button-light.png new file mode 100644 index 0000000000000000000000000000000000000000..31fd1d73983b5b7dece7926966ab12f0c4c7a305 GIT binary patch literal 2677 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/streaming_text-light.png b/testdata/snapshots/darwin/streaming_text-light.png new file mode 100644 index 0000000000000000000000000000000000000000..cadc899c637756e4bb0705e84dda4d263e185e42 GIT binary patch literal 4207 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 R)Bl6zM_Hf@Sjsf$KLE-LrrZDk literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/thinking_block-light.png b/testdata/snapshots/darwin/thinking_block-light.png new file mode 100644 index 0000000000000000000000000000000000000000..4f62d55190ce52ef266934729d48319dc397d0b6 GIT binary patch literal 9494 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/typing_indicator-dark.png b/testdata/snapshots/darwin/typing_indicator-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..50032a90610deb4f3530444ce8da0661ce5766af GIT binary patch literal 2445 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