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