1package model
2
3import (
4 "fmt"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/message"
9 "github.com/charmbracelet/crush/internal/tui/components/anim"
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 uv "github.com/charmbracelet/ultraviolet"
14)
15
16// ChatAnimItem represents a chat animation item in the chat UI.
17type ChatAnimItem struct {
18 list.BaseFocusable
19 anim *anim.Anim
20}
21
22var (
23 _ list.Item = (*ChatAnimItem)(nil)
24 _ list.Focusable = (*ChatAnimItem)(nil)
25)
26
27// NewChatAnimItem creates a new instance of [ChatAnimItem].
28func NewChatAnimItem(a *anim.Anim) *ChatAnimItem {
29 m := new(ChatAnimItem)
30 return m
31}
32
33// Init initializes the chat animation item.
34func (c *ChatAnimItem) Init() tea.Cmd {
35 return c.anim.Init()
36}
37
38// Step advances the animation by one step.
39func (c *ChatAnimItem) Step() tea.Cmd {
40 return c.anim.Step()
41}
42
43// SetLabel sets the label for the animation item.
44func (c *ChatAnimItem) SetLabel(label string) {
45 c.anim.SetLabel(label)
46}
47
48// Draw implements list.Item.
49func (c *ChatAnimItem) Draw(scr uv.Screen, area uv.Rectangle) {
50 styled := uv.NewStyledString(c.anim.View())
51 styled.Draw(scr, area)
52}
53
54// Height implements list.Item.
55func (c *ChatAnimItem) Height(int) int {
56 return 1
57}
58
59// ChatNoContentItem represents a chat item with no content.
60type ChatNoContentItem struct {
61 *list.StringItem
62}
63
64// NewChatNoContentItem creates a new instance of [ChatNoContentItem].
65func NewChatNoContentItem(t *styles.Styles) *ChatNoContentItem {
66 c := new(ChatNoContentItem)
67 c.StringItem = list.NewStringItem("No message content").
68 WithFocusStyles(&t.Chat.NoContentMessage, &t.Chat.NoContentMessage)
69 return c
70}
71
72// ChatMessageItem represents a chat message item in the chat UI.
73type ChatMessageItem struct {
74 item list.Item
75 msg message.Message
76}
77
78var (
79 _ list.Item = (*ChatMessageItem)(nil)
80 _ list.Focusable = (*ChatMessageItem)(nil)
81 _ list.Highlightable = (*ChatMessageItem)(nil)
82)
83
84// NewChatMessageItem creates a new instance of [ChatMessageItem].
85func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem {
86 c := new(ChatMessageItem)
87
88 switch msg.Role {
89 case message.User:
90 item := list.NewMarkdownItem(msg.Content().String()).
91 WithFocusStyles(&t.Chat.UserMessageFocused, &t.Chat.UserMessageBlurred)
92 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
93 // TODO: Add attachments
94 c.item = item
95 default:
96 var thinkingContent string
97 content := msg.Content().String()
98 thinking := msg.IsThinking()
99 finished := msg.IsFinished()
100 finishedData := msg.FinishPart()
101 reasoningContent := msg.ReasoningContent()
102 reasoningThinking := strings.TrimSpace(reasoningContent.Thinking)
103
104 if finished && content == "" && finishedData.Reason == message.FinishReasonError {
105 tag := t.Chat.ErrorTag.Render("ERROR")
106 title := t.Chat.ErrorTitle.Render(finishedData.Message)
107 details := t.Chat.ErrorDetails.Render(finishedData.Details)
108 errContent := fmt.Sprintf("%s %s\n\n%s", tag, title, details)
109
110 item := list.NewStringItem(errContent).
111 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
112
113 c.item = item
114
115 return c
116 }
117
118 if thinking || reasoningThinking != "" {
119 // TODO: animation item?
120 // TODO: thinking item
121 thinkingContent = reasoningThinking
122 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
123 content = "*Canceled*"
124 }
125
126 var parts []string
127 if thinkingContent != "" {
128 parts = append(parts, thinkingContent)
129 }
130
131 if content != "" {
132 if len(parts) > 0 {
133 parts = append(parts, "")
134 }
135 parts = append(parts, content)
136 }
137
138 item := list.NewMarkdownItem(strings.Join(parts, "\n")).
139 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
140 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
141
142 c.item = item
143 }
144
145 return c
146}
147
148// Draw implements list.Item.
149func (c *ChatMessageItem) Draw(scr uv.Screen, area uv.Rectangle) {
150 c.item.Draw(scr, area)
151}
152
153// Height implements list.Item.
154func (c *ChatMessageItem) Height(width int) int {
155 return c.item.Height(width)
156}
157
158// Blur implements list.Focusable.
159func (c *ChatMessageItem) Blur() {
160 if blurable, ok := c.item.(list.Focusable); ok {
161 blurable.Blur()
162 }
163}
164
165// Focus implements list.Focusable.
166func (c *ChatMessageItem) Focus() {
167 if focusable, ok := c.item.(list.Focusable); ok {
168 focusable.Focus()
169 }
170}
171
172// IsFocused implements list.Focusable.
173func (c *ChatMessageItem) IsFocused() bool {
174 if focusable, ok := c.item.(list.Focusable); ok {
175 return focusable.IsFocused()
176 }
177 return false
178}
179
180// GetHighlight implements list.Highlightable.
181func (c *ChatMessageItem) GetHighlight() (startLine int, startCol int, endLine int, endCol int) {
182 if highlightable, ok := c.item.(list.Highlightable); ok {
183 return highlightable.GetHighlight()
184 }
185 return 0, 0, 0, 0
186}
187
188// SetHighlight implements list.Highlightable.
189func (c *ChatMessageItem) SetHighlight(startLine int, startCol int, endLine int, endCol int) {
190 if highlightable, ok := c.item.(list.Highlightable); ok {
191 highlightable.SetHighlight(startLine, startCol, endLine, endCol)
192 }
193}
194
195// Chat represents the chat UI model that handles chat interactions and
196// messages.
197type Chat struct {
198 com *common.Common
199 list *list.List
200}
201
202// NewChat creates a new instance of [Chat] that handles chat interactions and
203// messages.
204func NewChat(com *common.Common) *Chat {
205 l := list.New()
206 return &Chat{
207 com: com,
208 list: l,
209 }
210}
211
212// Height returns the height of the chat view port.
213func (m *Chat) Height() int {
214 return m.list.Height()
215}
216
217// Draw renders the chat UI component to the screen and the given area.
218func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) {
219 m.list.Draw(scr, area)
220}
221
222// SetSize sets the size of the chat view port.
223func (m *Chat) SetSize(width, height int) {
224 m.list.SetSize(width, height)
225}
226
227// Len returns the number of items in the chat list.
228func (m *Chat) Len() int {
229 return m.list.Len()
230}
231
232// PrependItem prepends a new item to the chat list.
233func (m *Chat) PrependItem(item list.Item) {
234 m.list.PrependItem(item)
235}
236
237// AppendMessage appends a new message item to the chat list.
238func (m *Chat) AppendMessage(msg message.Message) {
239 if msg.ID == "" {
240 m.AppendItem(NewChatNoContentItem(m.com.Styles))
241 } else {
242 m.AppendItem(NewChatMessageItem(m.com.Styles, msg))
243 }
244}
245
246// AppendItem appends a new item to the chat list.
247func (m *Chat) AppendItem(item list.Item) {
248 if m.Len() > 0 {
249 // Always add a spacer between messages
250 m.list.AppendItem(list.NewSpacerItem(1))
251 }
252 m.list.AppendItem(item)
253}
254
255// Focus sets the focus state of the chat component.
256func (m *Chat) Focus() {
257 m.list.Focus()
258}
259
260// Blur removes the focus state from the chat component.
261func (m *Chat) Blur() {
262 m.list.Blur()
263}
264
265// ScrollToTop scrolls the chat view to the top.
266func (m *Chat) ScrollToTop() {
267 m.list.ScrollToTop()
268}
269
270// ScrollToBottom scrolls the chat view to the bottom.
271func (m *Chat) ScrollToBottom() {
272 m.list.ScrollToBottom()
273}
274
275// ScrollBy scrolls the chat view by the given number of line deltas.
276func (m *Chat) ScrollBy(lines int) {
277 m.list.ScrollBy(lines)
278}
279
280// ScrollToSelected scrolls the chat view to the selected item.
281func (m *Chat) ScrollToSelected() {
282 m.list.ScrollToSelected()
283}
284
285// SelectedItemInView returns whether the selected item is currently in view.
286func (m *Chat) SelectedItemInView() bool {
287 return m.list.SelectedItemInView()
288}
289
290// SetSelected sets the selected message index in the chat list.
291func (m *Chat) SetSelected(index int) {
292 m.list.SetSelected(index)
293}
294
295// SelectPrev selects the previous message in the chat list.
296func (m *Chat) SelectPrev() {
297 m.list.SelectPrev()
298}
299
300// SelectNext selects the next message in the chat list.
301func (m *Chat) SelectNext() {
302 m.list.SelectNext()
303}
304
305// SelectFirst selects the first message in the chat list.
306func (m *Chat) SelectFirst() {
307 m.list.SelectFirst()
308}
309
310// SelectLast selects the last message in the chat list.
311func (m *Chat) SelectLast() {
312 m.list.SelectLast()
313}
314
315// SelectFirstInView selects the first message currently in view.
316func (m *Chat) SelectFirstInView() {
317 m.list.SelectFirstInView()
318}
319
320// SelectLastInView selects the last message currently in view.
321func (m *Chat) SelectLastInView() {
322 m.list.SelectLastInView()
323}
324
325// HandleMouseDown handles mouse down events for the chat component.
326func (m *Chat) HandleMouseDown(x, y int) {
327 m.list.HandleMouseDown(x, y)
328}
329
330// HandleMouseUp handles mouse up events for the chat component.
331func (m *Chat) HandleMouseUp(x, y int) {
332 m.list.HandleMouseUp(x, y)
333}
334
335// HandleMouseDrag handles mouse drag events for the chat component.
336func (m *Chat) HandleMouseDrag(x, y int) {
337 m.list.HandleMouseDrag(x, y)
338}