Fixed styling and added a landing page

This commit is contained in:
Radu Macocian (admac)
2026-06-10 17:41:17 +02:00
parent bbf01ed041
commit de546d8df4
2 changed files with 91 additions and 30 deletions
+63 -30
View File
@@ -17,14 +17,17 @@ import (
var logger *slog.Logger var logger *slog.Logger
type model struct { type model struct {
viewport viewport.Model viewport viewport.Model
messages []llm.Message messages []llm.Message
textarea textarea.Model textarea textarea.Model
senderStyle lipgloss.Style senderStyle lipgloss.Style
receiverStyle lipgloss.Style agentStyle lipgloss.Style
client llm.LLMClient errorStyle lipgloss.Style
waiting bool client llm.LLMClient
err error width int
height int
waiting bool
err error
} }
type agentResponseMessage struct { type agentResponseMessage struct {
@@ -52,7 +55,7 @@ func InitialModel(config config.Config) model {
ta.ShowLineNumbers = false ta.ShowLineNumbers = false
vp := viewport.New(viewport.WithWidth(30), viewport.WithHeight(5)) vp := viewport.New(viewport.WithWidth(30), viewport.WithHeight(5))
vp.SetContent(`What are we doing on today?`) vp.SetContent(renderLanding(vp.Width(), vp.Height()))
vp.KeyMap.Left.SetEnabled(false) vp.KeyMap.Left.SetEnabled(false)
vp.KeyMap.Right.SetEnabled(false) vp.KeyMap.Right.SetEnabled(false)
@@ -63,6 +66,8 @@ func InitialModel(config config.Config) model {
messages: []llm.Message{}, messages: []llm.Message{},
viewport: vp, viewport: vp,
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
errorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
agentStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("86")),
client: llm.LLMClient{Cfg: config}, client: llm.LLMClient{Cfg: config},
err: nil, err: nil,
} }
@@ -76,14 +81,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
logger.Debug("Received WindowSizeMsg", "width", msg.Width, "height", msg.Height) logger.Debug("Received WindowSizeMsg", "width", msg.Width, "height", msg.Height)
m.viewport.SetWidth(msg.Width) m.viewport.SetWidth(msg.Width * 80 / 100)
m.textarea.SetWidth(msg.Width) m.textarea.SetWidth(msg.Width * 80 / 100)
m.viewport.SetHeight(msg.Height - m.textarea.Height()) m.viewport.SetHeight(msg.Height - m.textarea.Height())
m.width = msg.Width
m.height = msg.Height
var messages = getMessages(m.messages) var messages = m.renderMessages()
if len(m.messages) > 0 { if len(m.messages) > 0 {
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages)) m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
} else {
m.viewport.SetContent(renderLanding(m.viewport.Width(), m.viewport.Height()))
} }
m.viewport.GotoBottom() m.viewport.GotoBottom()
@@ -97,11 +106,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "enter": case "enter":
input := m.textarea.Value() input := m.textarea.Value()
m.messages = append(m.messages, llm.Message{ m.messages = append(m.messages, llm.Message{
Content: m.senderStyle.Render(input), Content: input,
Role: "You", Role: "user",
}) })
logger.Debug("Received enter. Creating new message", "message", input) logger.Debug("Received enter. Creating new message", "message", input)
var messages = getMessages(m.messages) var messages = m.renderMessages()
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages)) m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
m.textarea.Reset() m.textarea.Reset()
m.viewport.GotoBottom() m.viewport.GotoBottom()
@@ -135,7 +144,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
logger.Debug("Appending response message", "message", r) logger.Debug("Appending response message", "message", r)
m.messages = append(m.messages, r) m.messages = append(m.messages, r)
} }
var messages = getMessages(m.messages) var messages = m.renderMessages()
m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages)) m.viewport.SetContent(lipgloss.NewStyle().Width(m.viewport.Width()).Render(messages))
return m, nil return m, nil
} }
@@ -144,15 +153,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m model) View() tea.View { func (m model) View() tea.View {
viewportView := m.viewport.View() ui := lipgloss.JoinVertical(
v := tea.NewView(viewportView + "\n" + m.textarea.View()) lipgloss.Left,
m.viewport.View(),
m.textarea.View())
c := m.textarea.Cursor() c := m.textarea.Cursor()
if c != nil { if c != nil {
c.Y += lipgloss.Height(viewportView) c.Y += lipgloss.Height(m.viewport.View())
gap := m.width - lipgloss.Width(ui)
c.X += max(gap / 2, 0)
} }
v.Cursor = c view := tea.NewView(lipgloss.PlaceHorizontal(m.width, lipgloss.Center, ui))
v.AltScreen = true view.Cursor = c
return v view.AltScreen = true
return view
} }
func (m model) callAgent(input string, messages []llm.Message) tea.Cmd { func (m model) callAgent(input string, messages []llm.Message) tea.Cmd {
@@ -163,13 +177,32 @@ func (m model) callAgent(input string, messages []llm.Message) tea.Cmd {
} }
} }
func getMessages(m []llm.Message) string { func (m model) renderMessages() string {
var messages strings.Builder var result strings.Builder
for _, m := range m { var width = m.viewport.Width()
messages.WriteString(m.Role) for _, message := range m.messages {
messages.WriteString(": ") switch message.Role {
messages.WriteString(m.Content) case "user":
messages.WriteString("\n") var line strings.Builder
line.WriteString(m.senderStyle.Render("You: "))
line.WriteString(message.Content)
result.WriteString(lipgloss.PlaceHorizontal(width, lipgloss.Right, line.String()))
case "assistant":
result.WriteString(m.agentStyle.Render("Agent: "))
result.WriteString(message.Content)
case "error":
result.WriteString(m.errorStyle.Render("Error:"))
result.WriteString(message.Content)
}
result.WriteString("\n")
var sepStyle lipgloss.Style = lipgloss.NewStyle().
Foreground(lipgloss.Color("240")).
MarginTop(1).
MarginBottom(1)
sep := sepStyle.Render(strings.Repeat("┈", width))
result.WriteString(sep)
result.WriteString("\n")
} }
return messages.String() return result.String()
} }
+28
View File
@@ -0,0 +1,28 @@
package ui
import "charm.land/lipgloss/v2"
// Logo is the souvenir snow-globe banner (ANSI 256-color, half-block).
const Logo = "\x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m\x1b[38;5;111m▄\x1b[0m\x1b[38;5;111m▄\x1b[38;5;111;48;5;152m▀\x1b[38;5;111;48;5;152m▀\x1b[38;5;111;48;5;152m▀\x1b[38;5;111;48;5;152m▀\x1b[0m\x1b[38;5;111m▄\x1b[0m\x1b[38;5;111m▄\x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[0m \x1b[0m\x1b[38;5;111m▄\x1b[38;5;111;48;5;111m▀\x1b[38;5;111;48;5;152m▀\x1b[38;5;152;48;5;195m▀\x1b[38;5;152;48;5;231m▀\x1b[38;5;152;48;5;195m▀\x1b[38;5;195;48;5;195m▀\x1b[38;5;195;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;111;48;5;152m▀\x1b[38;5;111;48;5;111m▀\x1b[0m\x1b[38;5;111m▄\x1b[0m \x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[0m\x1b[38;5;111m▄\x1b[38;5;111;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;195;48;5;195m▀\x1b[38;5;195;48;5;231m▀\x1b[38;5;195;48;5;195m▀\x1b[38;5;195;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;231;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;111;48;5;152m▀\x1b[0m\x1b[38;5;111m▄\x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[38;5;111;48;5;111m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;195;48;5;195m▀\x1b[38;5;195;48;5;195m▀\x1b[38;5;195;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;28m▀\x1b[38;5;152;48;5;22m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;231;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;111;48;5;111m▀\x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;195;48;5;152m▀\x1b[38;5;152;48;5;231m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;22;48;5;22m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;231;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[38;5;111;48;5;111m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;28;48;5;28m▀\x1b[38;5;22;48;5;22m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;152;48;5;152m▀\x1b[38;5;111;48;5;111m▀\x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[0m\x1b[38;5;111m▀\x1b[38;5;231;48;5;111m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;94;48;5;231m▀\x1b[38;5;94;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;231m▀\x1b[38;5;231;48;5;111m▀\x1b[0m\x1b[38;5;111m▀\x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[0m \x1b[0m \x1b[0m\x1b[38;5;130m▄\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[38;5;214;48;5;130m▀\x1b[0m\x1b[38;5;130m▄\x1b[0m \x1b[0m \x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[0m \x1b[0m \x1b[0m\x1b[38;5;130m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[38;5;130;48;5;94m▀\x1b[0m\x1b[38;5;130m▀\x1b[0m \x1b[0m \x1b[0m \x1b[0m\n" +
"\x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m \x1b[0m\n"
const Wordmark = `
___ ___ _ ___ _____ _ __ (_)_ __
/ __|/ _ \ | | \ \ / / _ \ '_ \| | '__|
\__ \ (_) | |_| |\ V / __/ | | | | |
|___/\___/ \__,_| \_/ \___|_| |_|_|_|`
func renderLanding(width int, height int) string {
return lipgloss.PlaceVertical(height, 0.7,
lipgloss.PlaceHorizontal(width,
lipgloss.Center,
lipgloss.JoinHorizontal(lipgloss.Center, Logo, Wordmark)))
}