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/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}