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