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.Message.NoContent, &t.Chat.Message.NoContent)
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.Message.UserFocused, &t.Chat.Message.UserBlurred)
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.Message.ErrorTag.Render("ERROR")
106 title := t.Chat.Message.ErrorTitle.Render(finishedData.Message)
107 details := t.Chat.Message.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.Message.AssistantFocused, &t.Chat.Message.AssistantBlurred)
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.Message.AssistantFocused, &t.Chat.Message.AssistantBlurred)
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// AppendMessages appends a new message item to the chat list.
238func (m *Chat) AppendMessages(msgs ...MessageItem) {
239 for _, msg := range msgs {
240 m.AppendItem(msg)
241 }
242}
243
244// AppendItem appends a new item to the chat list.
245func (m *Chat) AppendItem(item list.Item) {
246 if m.Len() > 0 {
247 // Always add a spacer between messages
248 m.list.AppendItem(list.NewSpacerItem(1))
249 }
250 m.list.AppendItem(item)
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}