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