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 list.BaseFocusable
81 list.BaseHighlightable
82
83 item list.Item
84 msg message.Message
85}
86
87var (
88 _ list.Item = (*ChatMessageItem)(nil)
89 _ list.Focusable = (*ChatMessageItem)(nil)
90 _ list.Highlightable = (*ChatMessageItem)(nil)
91)
92
93// NewChatMessageItem creates a new instance of [ChatMessageItem].
94func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem {
95 c := new(ChatMessageItem)
96
97 switch msg.Role {
98 case message.User:
99 item := list.NewMarkdownItem(msg.ID, msg.Content().String()).
100 WithFocusStyles(&t.Chat.UserMessageFocused, &t.Chat.UserMessageBlurred)
101 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
102 // TODO: Add attachments
103 c.item = item
104 default:
105 var thinkingContent string
106 content := msg.Content().String()
107 thinking := msg.IsThinking()
108 finished := msg.IsFinished()
109 finishedData := msg.FinishPart()
110 reasoningContent := msg.ReasoningContent()
111 reasoningThinking := strings.TrimSpace(reasoningContent.Thinking)
112
113 if finished && content == "" && finishedData.Reason == message.FinishReasonError {
114 tag := t.Chat.ErrorTag.Render("ERROR")
115 title := t.Chat.ErrorTitle.Render(finishedData.Message)
116 details := t.Chat.ErrorDetails.Render(finishedData.Details)
117 errContent := fmt.Sprintf("%s %s\n\n%s", tag, title, details)
118
119 item := list.NewStringItem(msg.ID, errContent).
120 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
121
122 c.item = item
123
124 return c
125 }
126
127 if thinking || reasoningThinking != "" {
128 // TODO: animation item?
129 // TODO: thinking item
130 thinkingContent = reasoningThinking
131 } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled {
132 content = "*Canceled*"
133 }
134
135 var parts []string
136 if thinkingContent != "" {
137 parts = append(parts, thinkingContent)
138 }
139
140 if content != "" {
141 if len(parts) > 0 {
142 parts = append(parts, "")
143 }
144 parts = append(parts, content)
145 }
146
147 item := list.NewMarkdownItem(msg.ID, strings.Join(parts, "\n")).
148 WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
149 item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection))
150
151 c.item = item
152 }
153
154 return c
155}
156
157// Draw implements list.Item.
158func (c *ChatMessageItem) Draw(scr uv.Screen, area uv.Rectangle) {
159 c.item.Draw(scr, area)
160}
161
162// Height implements list.Item.
163func (c *ChatMessageItem) Height(width int) int {
164 return c.item.Height(width)
165}
166
167// ID implements list.Item.
168func (c *ChatMessageItem) ID() string {
169 return c.item.ID()
170}
171
172// Chat represents the chat UI model that handles chat interactions and
173// messages.
174type Chat struct {
175 com *common.Common
176 list *list.List
177}
178
179// NewChat creates a new instance of [Chat] that handles chat interactions and
180// messages.
181func NewChat(com *common.Common) *Chat {
182 l := list.New()
183 return &Chat{
184 com: com,
185 list: l,
186 }
187}
188
189// Height returns the height of the chat view port.
190func (m *Chat) Height() int {
191 return m.list.Height()
192}
193
194// Draw renders the chat UI component to the screen and the given area.
195func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) {
196 m.list.Draw(scr, area)
197}
198
199// SetSize sets the size of the chat view port.
200func (m *Chat) SetSize(width, height int) {
201 m.list.SetSize(width, height)
202}
203
204// Len returns the number of items in the chat list.
205func (m *Chat) Len() int {
206 return m.list.Len()
207}
208
209// PrependItem prepends a new item to the chat list.
210func (m *Chat) PrependItem(item list.Item) {
211 m.list.PrependItem(item)
212}
213
214// AppendMessage appends a new message item to the chat list.
215func (m *Chat) AppendMessage(msg message.Message) {
216 if msg.ID == "" {
217 m.AppendItem(NewChatNoContentItem(m.com.Styles, uuid.NewString()))
218 } else {
219 m.AppendItem(NewChatMessageItem(m.com.Styles, msg))
220 }
221}
222
223// AppendItem appends a new item to the chat list.
224func (m *Chat) AppendItem(item list.Item) {
225 if m.Len() > 0 {
226 // Always add a spacer between messages
227 m.list.AppendItem(list.NewSpacerItem(uuid.NewString(), 1))
228 }
229 m.list.AppendItem(item)
230}
231
232// Focus sets the focus state of the chat component.
233func (m *Chat) Focus() {
234 m.list.Focus()
235}
236
237// Blur removes the focus state from the chat component.
238func (m *Chat) Blur() {
239 m.list.Blur()
240}
241
242// ScrollToTop scrolls the chat view to the top.
243func (m *Chat) ScrollToTop() {
244 m.list.ScrollToTop()
245}
246
247// ScrollToBottom scrolls the chat view to the bottom.
248func (m *Chat) ScrollToBottom() {
249 m.list.ScrollToBottom()
250}
251
252// ScrollBy scrolls the chat view by the given number of line deltas.
253func (m *Chat) ScrollBy(lines int) {
254 m.list.ScrollBy(lines)
255}
256
257// ScrollToSelected scrolls the chat view to the selected item.
258func (m *Chat) ScrollToSelected() {
259 m.list.ScrollToSelected()
260}
261
262// SelectedItemInView returns whether the selected item is currently in view.
263func (m *Chat) SelectedItemInView() bool {
264 return m.list.SelectedItemInView()
265}
266
267// SetSelectedIndex sets the selected message index in the chat list.
268func (m *Chat) SetSelectedIndex(index int) {
269 m.list.SetSelectedIndex(index)
270}
271
272// SelectPrev selects the previous message in the chat list.
273func (m *Chat) SelectPrev() {
274 m.list.SelectPrev()
275}
276
277// SelectNext selects the next message in the chat list.
278func (m *Chat) SelectNext() {
279 m.list.SelectNext()
280}
281
282// HandleMouseDown handles mouse down events for the chat component.
283func (m *Chat) HandleMouseDown(x, y int) {
284 m.list.HandleMouseDown(x, y)
285}
286
287// HandleMouseUp handles mouse up events for the chat component.
288func (m *Chat) HandleMouseUp(x, y int) {
289 m.list.HandleMouseUp(x, y)
290}
291
292// HandleMouseDrag handles mouse drag events for the chat component.
293func (m *Chat) HandleMouseDrag(x, y int) {
294 m.list.HandleMouseDrag(x, y)
295}