1package chat
2
3import (
4 "fmt"
5 "path/filepath"
6 "strings"
7
8 "charm.land/lipgloss/v2"
9 "github.com/charmbracelet/crush/internal/message"
10 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/styles"
12 "github.com/charmbracelet/x/ansi"
13)
14
15// UserMessageItem represents a user message in the chat UI.
16type UserMessageItem struct {
17 *highlightableMessageItem
18 *cachedMessageItem
19 message *message.Message
20 sty *styles.Styles
21 focused bool
22}
23
24// NewUserMessageItem creates a new UserMessageItem.
25func NewUserMessageItem(sty *styles.Styles, message *message.Message) MessageItem {
26 return &UserMessageItem{
27 highlightableMessageItem: defaultHighlighter(sty),
28 cachedMessageItem: &cachedMessageItem{},
29 message: message,
30 sty: sty,
31 focused: false,
32 }
33}
34
35// Render implements MessageItem.
36func (m *UserMessageItem) Render(width int) string {
37 cappedWidth := cappedMessageWidth(width)
38
39 style := m.sty.Chat.Message.UserBlurred
40 if m.focused {
41 style = m.sty.Chat.Message.UserFocused
42 }
43
44 content, height, ok := m.getCachedRender(cappedWidth)
45 // cache hit
46 if ok {
47 return style.Render(m.renderHighlighted(content, cappedWidth, height))
48 }
49
50 renderer := common.MarkdownRenderer(m.sty, cappedWidth)
51
52 msgContent := strings.TrimSpace(m.message.Content().Text)
53 result, err := renderer.Render(msgContent)
54 if err != nil {
55 content = msgContent
56 } else {
57 content = strings.TrimSuffix(result, "\n")
58 }
59
60 if len(m.message.BinaryContent()) > 0 {
61 attachmentsStr := m.renderAttachments(cappedWidth)
62 content = strings.Join([]string{content, "", attachmentsStr}, "\n")
63 }
64
65 height = lipgloss.Height(content)
66 m.setCachedRender(content, cappedWidth, height)
67 return style.Render(m.renderHighlighted(content, cappedWidth, height))
68}
69
70// SetFocused implements MessageItem.
71func (m *UserMessageItem) SetFocused(focused bool) {
72 m.focused = focused
73}
74
75// ID implements MessageItem.
76func (m *UserMessageItem) ID() string {
77 return m.message.ID
78}
79
80// renderAttachments renders attachments with wrapping if they exceed the width.
81// TODO: change the styles here so they match the new design
82func (m *UserMessageItem) renderAttachments(width int) string {
83 const maxFilenameWidth = 10
84
85 attachments := make([]string, len(m.message.BinaryContent()))
86 for i, attachment := range m.message.BinaryContent() {
87 filename := filepath.Base(attachment.Path)
88 attachments[i] = m.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
89 " %s %s ",
90 styles.DocumentIcon,
91 ansi.Truncate(filename, maxFilenameWidth, "…"),
92 ))
93 }
94
95 // Wrap attachments into lines that fit within the width.
96 var lines []string
97 var currentLine []string
98 currentWidth := 0
99
100 for _, att := range attachments {
101 attWidth := lipgloss.Width(att)
102 sepWidth := 1
103 if len(currentLine) == 0 {
104 sepWidth = 0
105 }
106
107 if currentWidth+sepWidth+attWidth > width && len(currentLine) > 0 {
108 lines = append(lines, strings.Join(currentLine, " "))
109 currentLine = []string{att}
110 currentWidth = attWidth
111 } else {
112 currentLine = append(currentLine, att)
113 currentWidth += sepWidth + attWidth
114 }
115 }
116
117 if len(currentLine) > 0 {
118 lines = append(lines, strings.Join(currentLine, " "))
119 }
120
121 return strings.Join(lines, "\n")
122}