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.NoContentMessage, &t.Chat.NoContentMessage)
 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.UserMessageFocused, &t.Chat.UserMessageBlurred)
 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.ErrorTag.Render("ERROR")
106			title := t.Chat.ErrorTitle.Render(finishedData.Message)
107			details := t.Chat.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.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
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.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred)
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// AppendMessage appends a new message item to the chat list.
238func (m *Chat) AppendMessage(msg message.Message) {
239	if msg.ID == "" {
240		m.AppendItem(NewChatNoContentItem(m.com.Styles))
241	} else {
242		m.AppendItem(NewChatMessageItem(m.com.Styles, msg))
243	}
244}
245
246// AppendItem appends a new item to the chat list.
247func (m *Chat) AppendItem(item list.Item) {
248	if m.Len() > 0 {
249		// Always add a spacer between messages
250		m.list.AppendItem(list.NewSpacerItem(1))
251	}
252	m.list.AppendItem(item)
253}
254
255// Focus sets the focus state of the chat component.
256func (m *Chat) Focus() {
257	m.list.Focus()
258}
259
260// Blur removes the focus state from the chat component.
261func (m *Chat) Blur() {
262	m.list.Blur()
263}
264
265// ScrollToTop scrolls the chat view to the top.
266func (m *Chat) ScrollToTop() {
267	m.list.ScrollToTop()
268}
269
270// ScrollToBottom scrolls the chat view to the bottom.
271func (m *Chat) ScrollToBottom() {
272	m.list.ScrollToBottom()
273}
274
275// ScrollBy scrolls the chat view by the given number of line deltas.
276func (m *Chat) ScrollBy(lines int) {
277	m.list.ScrollBy(lines)
278}
279
280// ScrollToSelected scrolls the chat view to the selected item.
281func (m *Chat) ScrollToSelected() {
282	m.list.ScrollToSelected()
283}
284
285// SelectedItemInView returns whether the selected item is currently in view.
286func (m *Chat) SelectedItemInView() bool {
287	return m.list.SelectedItemInView()
288}
289
290// SetSelected sets the selected message index in the chat list.
291func (m *Chat) SetSelected(index int) {
292	m.list.SetSelected(index)
293}
294
295// SelectPrev selects the previous message in the chat list.
296func (m *Chat) SelectPrev() {
297	m.list.SelectPrev()
298}
299
300// SelectNext selects the next message in the chat list.
301func (m *Chat) SelectNext() {
302	m.list.SelectNext()
303}
304
305// SelectFirst selects the first message in the chat list.
306func (m *Chat) SelectFirst() {
307	m.list.SelectFirst()
308}
309
310// SelectLast selects the last message in the chat list.
311func (m *Chat) SelectLast() {
312	m.list.SelectLast()
313}
314
315// SelectFirstInView selects the first message currently in view.
316func (m *Chat) SelectFirstInView() {
317	m.list.SelectFirstInView()
318}
319
320// SelectLastInView selects the last message currently in view.
321func (m *Chat) SelectLastInView() {
322	m.list.SelectLastInView()
323}
324
325// HandleMouseDown handles mouse down events for the chat component.
326func (m *Chat) HandleMouseDown(x, y int) {
327	m.list.HandleMouseDown(x, y)
328}
329
330// HandleMouseUp handles mouse up events for the chat component.
331func (m *Chat) HandleMouseUp(x, y int) {
332	m.list.HandleMouseUp(x, y)
333}
334
335// HandleMouseDrag handles mouse drag events for the chat component.
336func (m *Chat) HandleMouseDrag(x, y int) {
337	m.list.HandleMouseDrag(x, y)
338}