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