package views
import (
"fmt"
"git.b0b.be/bdeb/workout-suggester/internal/coach"
"git.b0b.be/bdeb/workout-suggester/internal/session"
"git.b0b.be/bdeb/workout-suggester/internal/weather"
)
// PageData is everything the full page needs.
type PageData struct {
DefaultPlace string
Plan *coach.Plan // nil before the first plan
Messages []session.Message
}
templ Page(d PageData) {
}
// PlanView is the weather, the scores, the coach's advice, routes and the
// follow-up chat.
templ PlanView(p coach.Plan, msgs []session.Message) {
@WeatherCard(p)
@Scores(p)
for _, m := range msgs {
if m.Role == "assistant" && m.State != session.Done {
@StreamingReply(m)
} else {
@MessageView(m)
}
}
if p.Assessment.Pick != coach.Indoor {
if p.RoutesLoaded {
@RoutesCard(p)
} else {
🗺️ Routes nearby
Planning loops from your door and looking up signposted routes…
if p.RoutesErr != "" {
The route services are busy, so no routes this time. Try again in a minute.
} else {
No { p.Assessment.Pick.Noun() } routes found close by. An out-and-back from your door works too.
}
} else {
for i, s := range p.Routes {
if s.Route.Generated {
from your door
} else {
signposted
}
{ s.Route.Name }
if s.Route.Generated {
{ loopDetails(p, s.Route) }
} else {
{ s.Describe() } ≈ { fmt.Sprintf("%.1f km", s.TotalKm) } · { fmt.Sprintf("%.1f km", s.FromYouKm) } from you
}
}
// Exchange is a follow-up question plus its streaming reply.
templ Exchange(user, reply session.Message) {
@MessageView(user)
@StreamingReply(reply)
}
// StreamingReply opens an SSE connection. "token" events append text as it
// arrives; the final "done" event swaps in the rendered Markdown.
templ StreamingReply(m session.Message) {
}
templ Token(text string) {
{ text }
}
// HealthInfo describes the model server status shown in the header.
type HealthInfo struct {
Checking bool
OK bool
Model string
Err string
}
templ Health(h HealthInfo) {
if h.Checking {
connecting…
} else if h.OK {
{ h.Model }
} else {
model offline
}