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