1package chat
2
3import (
4 "strings"
5
6 tea "charm.land/bubbletea/v2"
7 "charm.land/lipgloss/v2"
8 "github.com/charmbracelet/crush/internal/message"
9 "github.com/charmbracelet/crush/internal/ui/attachments"
10 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/styles"
12)
13
14// UserMessageItem represents a user message in the chat UI.
15type UserMessageItem struct {
16 *highlightableMessageItem
17 *cachedMessageItem
18 *focusableMessageItem
19
20 attachments *attachments.Renderer
21 message *message.Message
22 sty *styles.Styles
23}
24
25// NewUserMessageItem creates a new UserMessageItem.
26func NewUserMessageItem(sty *styles.Styles, message *message.Message, attachments *attachments.Renderer) MessageItem {
27 return &UserMessageItem{
28 highlightableMessageItem: defaultHighlighter(sty),
29 cachedMessageItem: &cachedMessageItem{},
30 focusableMessageItem: &focusableMessageItem{},
31 attachments: attachments,
32 message: message,
33 sty: sty,
34 }
35}
36
37// RawRender implements [MessageItem].
38func (m *UserMessageItem) RawRender(width int) string {
39 cappedWidth := cappedMessageWidth(width)
40
41 content, height, ok := m.getCachedRender(cappedWidth)
42 // cache hit
43 if ok {
44 return m.renderHighlighted(content, cappedWidth, height)
45 }
46
47 renderer := common.MarkdownRenderer(m.sty, cappedWidth)
48
49 msgContent := strings.TrimSpace(m.message.Content().Text)
50 result, err := renderer.Render(msgContent)
51 if err != nil {
52 content = msgContent
53 } else {
54 content = strings.TrimSuffix(result, "\n")
55 }
56
57 if len(m.message.BinaryContent()) > 0 {
58 attachmentsStr := m.renderAttachments(cappedWidth)
59 if content == "" {
60 content = attachmentsStr
61 } else {
62 content = strings.Join([]string{content, "", attachmentsStr}, "\n")
63 }
64 }
65
66 height = lipgloss.Height(content)
67 m.setCachedRender(content, cappedWidth, height)
68 return m.renderHighlighted(content, cappedWidth, height)
69}
70
71// Render implements MessageItem.
72func (m *UserMessageItem) Render(width int) string {
73 // Bypass the prefix cache while a highlight range is active so
74 // selection drags reflect immediately without invalidating the
75 // cache. Highlight changes are intentionally applied "above" the
76 // prefix cache.
77 useCache := !m.isHighlighted()
78 var key uint64
79 if m.focused {
80 key = 1
81 }
82 if useCache {
83 if cached, ok := m.getCachedPrefixedRender(width, key); ok {
84 return cached
85 }
86 }
87 var prefix string
88 if m.focused {
89 prefix = m.sty.Messages.UserFocused.Render()
90 } else {
91 prefix = m.sty.Messages.UserBlurred.Render()
92 }
93 lines := strings.Split(m.RawRender(width), "\n")
94 for i, line := range lines {
95 lines[i] = prefix + line
96 }
97 out := strings.Join(lines, "\n")
98 if useCache {
99 m.setCachedPrefixedRender(out, width, key)
100 }
101 return out
102}
103
104// ID implements MessageItem.
105func (m *UserMessageItem) ID() string {
106 return m.message.ID
107}
108
109// renderAttachments renders attachments.
110func (m *UserMessageItem) renderAttachments(width int) string {
111 var attachments []message.Attachment
112 for _, at := range m.message.BinaryContent() {
113 attachments = append(attachments, message.Attachment{
114 FileName: at.Path,
115 MimeType: at.MIMEType,
116 })
117 }
118 return m.attachments.Render(attachments, false, width)
119}
120
121// HandleKeyEvent implements KeyEventHandler.
122func (m *UserMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
123 if k := key.String(); k == "c" || k == "y" {
124 text := m.message.Content().Text
125 return true, common.CopyToClipboard(text, "Message copied to clipboard")
126 }
127 return false, nil
128}