chat.go

  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}