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