Chat Input

ChatInput draws a box to send a chat message. It returns the message on the run the send starts, and empties the box.

API

Interface

func ChatInput(c *tgframe.Container, placeholder string, conf ...*ChatInputConf) (string, bool)

Parameters

  • c: Parent container.
  • placeholder: The hint shown in the empty box. It also names the input, so two inputs with the same placeholder collide; give one a Conf.ID.
  • conf: Optional configuration, at most one.
// ChatInputConf is the configuration for the ChatInput component.
type ChatInputConf struct {
	tgframe.Base // ID

	// MaxLength is the most characters a message may have. 0 is no limit.
	MaxLength int

	// Disabled keeps the app user from sending, e.g. while a reply streams.
	Disabled bool
}

Return

The message and true on the run its send starts; "" and false on every other run. The message is not kept: store it, as a chat keeps its history.

Enter sends, Shift+Enter breaks the line. An Enter that ends an IME composition only picks the characters.

Where it is drawn

Written in the page's main container, p.Main, it is pinned to the bottom of the page wherever it is written. That lets a page read the message first and draw the messages it adds after, as the example does. In any other container it stays where it is written.

Example

type chatTurn struct {
	Role string
	Text string
}

	history := p.State.Default("chat_history", []chatTurn{})
	msg, sent := tgcomp.ChatInput(p.Main, "Say something")

	for _, turn := range *history {
		tgcomp.Markdown(tgcomp.ChatMessage(p.Main, turn.Role), turn.Text)
	}

	if sent {
		// Kept before streaming: a new send cancels this run mid-stream,
		// and the message is already gone from the input.
		*history = append(*history, chatTurn{Role: "user", Text: msg})
		tgcomp.Markdown(tgcomp.ChatMessage(p.Main, "user"), msg)

		reply, err := tgcomp.WriteStream(
			tgcomp.ChatMessage(p.Main, "assistant"), echo(p.Context, msg))
		if err != nil {
			return err
		}

		*history = append(*history, chatTurn{Role: "assistant", Text: reply})
	}

echo stands in for a model, streamed with WriteStream:

// echo stands in for a model: it answers with the message, a word at a time.
func echo(ctx context.Context, msg string) iter.Seq2[string, error] {
	return func(yield func(string, error) bool) {
		for _, word := range strings.SplitAfter("You said: "+msg, " ") {
			select {
			case <-ctx.Done():
				return
			case <-time.After(60 * time.Millisecond):
			}

			if !yield(word, nil) {
				return
			}
		}
	}
}