package core import ( "fmt" "strings" ) // Card represents a structured rich message that can be rendered as // platform-specific cards (Feishu Interactive Card, Telegram message, etc.) // or degraded to plain text for platforms without card support. type Card struct { Header *CardHeader Elements []CardElement } // CardHeader is the optional colored title bar of a card. type CardHeader struct { Title string Color string // blue, green, red, orange, purple, grey, turquoise, violet, indigo, wathet, yellow, carmine } // CardElement is the interface satisfied by all card content elements. type CardElement interface { cardElement() } // CardMarkdown renders markdown-formatted text. type CardMarkdown struct{ Content string } // CardDivider renders a horizontal rule. type CardDivider struct{} // CardActions renders a row of clickable buttons. type CardActions struct { Buttons []CardButton Layout CardActionLayout } // CardNote renders small footnote text at the bottom. // Tag is an optional machine-readable identifier (not displayed) used by // platform renderers to recognize and handle specific notes programmatically. type CardNote struct { Text string Tag string } // CardListItem renders a row with description text on the left and a button on the right. // On Feishu this maps to div+extra; on other platforms it degrades to a text line. type CardListItem struct { Text string // left-side description BtnText string // button label BtnType string // "primary", "default", "danger" BtnValue string // callback data Extra map[string]string // additional key-value pairs carried in the callback } // CardSelect renders a dropdown selector. // On Feishu this maps to select_static; on other platforms it degrades to text. type CardSelect struct { Placeholder string Options []CardSelectOption InitValue string // pre-selected option value (empty = none) } // CardSelectOption is one item in a CardSelect dropdown. type CardSelectOption struct { Text string Value string } func (CardMarkdown) cardElement() {} func (CardDivider) cardElement() {} func (CardActions) cardElement() {} func (CardNote) cardElement() {} func (CardListItem) cardElement() {} func (CardSelect) cardElement() {} // CardButton represents a clickable button inside a CardActions element. type CardButton struct { Text string // display label Type string // "primary", "default", "danger" Value string // callback data, e.g. "cmd:/new", "cmd:/switch 3" Extra map[string]string // additional key-value pairs carried in the callback (platform-specific) } // CardActionLayout controls how a CardActions row should be rendered by // platforms with richer layout capabilities. type CardActionLayout string const ( CardActionLayoutRow CardActionLayout = "row" CardActionLayoutEqualColumns CardActionLayout = "equal_columns" ) // Btn is a shorthand constructor for CardButton. func Btn(text, typ, value string) CardButton { return CardButton{Text: text, Type: typ, Value: value} } // PrimaryBtn creates a primary-styled button. func PrimaryBtn(text, value string) CardButton { return CardButton{Text: text, Type: "primary", Value: value} } // DefaultBtn creates a default-styled button. func DefaultBtn(text, value string) CardButton { return CardButton{Text: text, Type: "default", Value: value} } // DangerBtn creates a danger-styled button. func DangerBtn(text, value string) CardButton { return CardButton{Text: text, Type: "danger", Value: value} } // --- Builder API --- // CardBuilder provides a fluent API for constructing Card instances. type CardBuilder struct { card Card } // NewCard returns a new CardBuilder. func NewCard() *CardBuilder { return &CardBuilder{} } // Title sets the card header with a title and color. func (b *CardBuilder) Title(title, color string) *CardBuilder { b.card.Header = &CardHeader{Title: title, Color: color} return b } // Markdown appends a markdown text element. func (b *CardBuilder) Markdown(content string) *CardBuilder { if content != "" { b.card.Elements = append(b.card.Elements, CardMarkdown{Content: content}) } return b } // Markdownf appends a formatted markdown text element. func (b *CardBuilder) Markdownf(format string, args ...any) *CardBuilder { return b.Markdown(fmt.Sprintf(format, args...)) } // Divider appends a horizontal divider. func (b *CardBuilder) Divider() *CardBuilder { b.card.Elements = append(b.card.Elements, CardDivider{}) return b } // Buttons appends an action row with the given buttons. func (b *CardBuilder) Buttons(buttons ...CardButton) *CardBuilder { if len(buttons) > 0 { b.card.Elements = append(b.card.Elements, CardActions{Buttons: buttons, Layout: CardActionLayoutRow}) } return b } // ButtonsEqual appends an action row where each button should take equal width // on platforms that support richer layouts. func (b *CardBuilder) ButtonsEqual(buttons ...CardButton) *CardBuilder { if len(buttons) > 0 { b.card.Elements = append(b.card.Elements, CardActions{Buttons: buttons, Layout: CardActionLayoutEqualColumns}) } return b } // ListItem appends a list row: description on the left, button on the right. func (b *CardBuilder) ListItem(desc, btnText, btnValue string) *CardBuilder { b.card.Elements = append(b.card.Elements, CardListItem{ Text: desc, BtnText: btnText, BtnType: "default", BtnValue: btnValue, }) return b } // ListItemBtn is like ListItem but allows specifying the button type. func (b *CardBuilder) ListItemBtn(desc, btnText, btnType, btnValue string) *CardBuilder { b.card.Elements = append(b.card.Elements, CardListItem{ Text: desc, BtnText: btnText, BtnType: btnType, BtnValue: btnValue, }) return b } // ListItemBtnExtra is like ListItemBtn but with extra callback data. func (b *CardBuilder) ListItemBtnExtra(desc, btnText, btnType, btnValue string, extra map[string]string) *CardBuilder { b.card.Elements = append(b.card.Elements, CardListItem{ Text: desc, BtnText: btnText, BtnType: btnType, BtnValue: btnValue, Extra: extra, }) return b } // Select appends a dropdown selector element. func (b *CardBuilder) Select(placeholder string, options []CardSelectOption, initValue string) *CardBuilder { if len(options) > 0 { b.card.Elements = append(b.card.Elements, CardSelect{ Placeholder: placeholder, Options: options, InitValue: initValue, }) } return b } // Note appends a footnote element. func (b *CardBuilder) Note(text string) *CardBuilder { if text != "" { b.card.Elements = append(b.card.Elements, CardNote{Text: text}) } return b } func (b *CardBuilder) TaggedNote(tag, text string) *CardBuilder { if text != "" { b.card.Elements = append(b.card.Elements, CardNote{Text: text, Tag: tag}) } return b } // Build returns the constructed Card. func (b *CardBuilder) Build() *Card { c := b.card return &c } // --- Text Fallback --- // RenderText converts the card to a plain-text representation for platforms // that do not support rich cards. func (c *Card) RenderText() string { var sb strings.Builder if c.Header != nil && c.Header.Title != "" { sb.WriteString("**") sb.WriteString(c.Header.Title) sb.WriteString("**\n\n") } for _, elem := range c.Elements { switch e := elem.(type) { case CardMarkdown: sb.WriteString(e.Content) sb.WriteString("\n\n") case CardDivider: sb.WriteString("---\n\n") case CardActions: // Render buttons as a hint line for i, btn := range e.Buttons { if i > 0 { sb.WriteString(" ") } sb.WriteString("[") sb.WriteString(btn.Text) sb.WriteString("]") } sb.WriteString("\n\n") case CardListItem: sb.WriteString(e.Text) sb.WriteString(" [") sb.WriteString(e.BtnText) sb.WriteString("]\n") case CardSelect: sb.WriteString(e.Placeholder) sb.WriteString(": ") for i, opt := range e.Options { if i > 0 { sb.WriteString(" | ") } sb.WriteString(opt.Text) } sb.WriteString("\n\n") case CardNote: sb.WriteString(e.Text) sb.WriteString("\n") } } return strings.TrimRight(sb.String(), "\n") } // HasButtons returns true if the card contains any interactive elements. func (c *Card) HasButtons() bool { for _, elem := range c.Elements { switch elem.(type) { case CardActions, CardListItem, CardSelect: return true } } return false } // CollectButtons extracts all buttons from the card as a 2D slice // (one inner slice per CardActions element), suitable for InlineButtonSender. // CardListItem elements are collected as single-button rows. func (c *Card) CollectButtons() [][]ButtonOption { var rows [][]ButtonOption for _, elem := range c.Elements { switch e := elem.(type) { case CardActions: var row []ButtonOption for _, btn := range e.Buttons { row = append(row, ButtonOption{Text: btn.Text, Data: btn.Value}) } if len(row) > 0 { rows = append(rows, row) } case CardListItem: rows = append(rows, []ButtonOption{{Text: e.BtnText, Data: e.BtnValue}}) } } return rows }