// Package pages provides server-side HTML rendering via Go templates. // // Templates are embedded in the binary via //go:embed and organized as: // // templates/base.html — outer shell (banner + surface block + scripts) // templates/login.html — standalone login page // templates/components/*.html — reusable partials (model-select, team-select, chat-pane, etc.) // templates/surfaces/*.html — one per surface (chat, editor, notes, admin, etc.) package pages import ( "context" "crypto/rand" "embed" "encoding/hex" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "strings" "sync" "github.com/gin-gonic/gin" "switchboard-core/config" "switchboard-core/store" ) //go:embed templates/*.html templates/components/*.html templates/surfaces/*.html var templateFS embed.FS // Engine holds parsed templates and rendering state. type Engine struct { mu sync.RWMutex templates *template.Template cfg *config.Config stores store.Stores loaders map[string]DataLoaderFunc surfaces []SurfaceManifest // v0.25.0: registered surface definitions devMode bool } // SurfaceManifest describes a surface's registration. Core surfaces are // registered in Go at startup. Extension surfaces will be registered from // manifest files (future). type SurfaceManifest struct { ID string `json:"id"` // unique identifier: "chat", "editor", "my-dashboard" Route string `json:"route"` // primary URL pattern: "/", "/editor/:wsId" AltRoutes []string `json:"alt_routes"` // additional URL patterns: ["/chat/:chatID"] Title string `json:"title"` // human-readable: "Chat", "Editor" Template string `json:"template"` // Go template name: "surface-chat", "surface-editor" Components []string `json:"components"` // component IDs used: ["chat-pane", "file-tree"] DataRequires []string `json:"data_requires"` // data loader keys: ["workspace", "models"] Scripts []string `json:"scripts"` // JS files (surface-specific, beyond base.html common) Styles []string `json:"styles"` // CSS files (surface-specific) Auth string `json:"auth"` // "authenticated", "public", "session", "admin" Layout string `json:"layout"` // default pane layout preset: "single", "editor" Source string `json:"source"` // "core" or "extension" } // BannerConfig holds environment banner settings. type BannerConfig struct { Text string `json:"text"` Color string `json:"color"` Background string `json:"background"` Visible bool `json:"visible"` } // MessageConfig holds dismissible message bar settings. type MessageConfig struct { Text string `json:"text"` Variant string `json:"variant"` // info, warn, error, success Visible bool `json:"visible"` } // FooterConfig holds optional footer bar settings. type FooterConfig struct { Text string `json:"text"` Visible bool `json:"visible"` } // PageData is passed to every template render. type PageData struct { Banner BannerConfig Message MessageConfig Footer FooterConfig Surface string // active surface ID Section string // sub-section (for admin pages) CSPNonce string BasePath string Version string Environment string User *UserContext Data any // surface-specific data from loader // v0.22.7: Theme + settings injection Theme string // "dark", "light", or "" (= use default) SurfaceSettings any // JSON-serialized to window.__SETTINGS__ // v0.25.0: Surface manifest — layout preset, components, etc. Manifest *SurfaceManifest `json:"manifest,omitempty"` // v0.25.0: Enabled surface IDs for conditional nav rendering. EnabledSurfaces []string `json:"-"` // v0.27.0: Extension surfaces for sidebar nav rendering. ExtensionSurfaces []ExtensionNavItem `json:"-"` // v0.22.7: Login/splash page fields InstanceName string // branding: instance display name LogoURL string // branding: custom logo URL Tagline string // branding: tagline under instance name RegistrationOpen bool // whether self-registration is enabled AuthMode string // v0.24.1: "builtin", "mtls", "oidc" } // SurfaceEnabled returns true if the given surface ID is in the EnabledSurfaces list. // Used by templates: {{if .SurfaceEnabled "editor"}} func (pd PageData) SurfaceEnabled(id string) bool { for _, s := range pd.EnabledSurfaces { if s == id { return true } } return false } // ExtensionNavItem holds the minimal info needed to render an extension // surface link in the sidebar nav. type ExtensionNavItem struct { ID string Title string Route string } // ConfigSectionEntry describes a package-declared config section for // lazy-loading in Settings, Admin, or Team Admin surfaces. // v0.38.3: manifest-driven discovery — no new tables or endpoints. type ConfigSectionEntry struct { PackageID string `json:"package_id"` // section key & asset path prefix Label string `json:"label"` // nav link text Icon string `json:"icon"` // optional SVG path data (admin CatIcon format) Component string `json:"component"` // relative asset path, e.g. "js/config.js" Category string `json:"category"` // admin only: category tab (default "system") } // extensionNavItems returns enabled extension surfaces for sidebar rendering. // Queries the DB at render time so newly installed surfaces appear immediately. func (e *Engine) extensionNavItems() []ExtensionNavItem { if e.stores.Packages == nil { return nil } ctx := context.Background() surfaces, err := e.stores.Packages.List(ctx) if err != nil { log.Printf("[pages] Failed to load extension surfaces for nav: %v", err) return nil } var items []ExtensionNavItem for _, sr := range surfaces { if sr.Source == "core" || !sr.Enabled { continue } // Only surface and full types have routes — extensions are headless if sr.Type != "surface" && sr.Type != "full" { continue } route := "" if sr.Manifest != nil { route, _ = sr.Manifest["route"].(string) } if route == "" { route = "/s/" + sr.ID } items = append(items, ExtensionNavItem{ ID: sr.ID, Title: sr.Title, Route: route, }) } return items } // UserContext is the authenticated user's info available to templates. type UserContext struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Role string `json:"role"` Email string `json:"email"` } // DataLoaderFunc loads surface-specific data for template rendering. type DataLoaderFunc func(c *gin.Context, stores store.Stores) (any, error) // New creates a template engine. Call after config and stores are ready. func New(cfg *config.Config, stores store.Stores) *Engine { e := &Engine{ cfg: cfg, stores: stores, loaders: make(map[string]DataLoaderFunc), devMode: gin.Mode() == gin.DebugMode, } e.parseTemplates() e.registerLoaders() e.registerCoreSurfaces() e.SeedSurfaces() // v0.25.0: Write core manifests to DB (preserves admin toggle state) return e } // registerCoreSurfaces populates the surface manifest list with the kernel // surfaces. Extension surfaces are appended from DB at startup. func (e *Engine) registerCoreSurfaces() { e.surfaces = []SurfaceManifest{ { ID: "admin", Route: "/admin/:section", AltRoutes: []string{"/admin"}, Title: "Admin", Template: "surface-admin", Auth: "admin", DataRequires: []string{"admin"}, Layout: "single", Source: "core", }, { ID: "settings", Route: "/settings/:section", AltRoutes: []string{"/settings"}, Title: "Settings", Template: "surface-settings", Auth: "authenticated", DataRequires: []string{"settings"}, Layout: "single", Source: "core", }, { ID: "team-admin", Route: "/team-admin/:section", AltRoutes: []string{"/team-admin"}, Title: "Team Admin", Template: "surface-team-admin", Auth: "authenticated", DataRequires: []string{"team-admin"}, Layout: "single", Source: "core", }, { ID: "workflow", Route: "/w/:id", Title: "Workflow", Template: "workflow", Auth: "session", DataRequires: []string{"workflow"}, Layout: "single", Source: "core", }, { ID: "workflow-landing", Route: "/w/:id/:slug", Title: "Workflow Landing", Template: "workflow-landing", Auth: "public", Layout: "single", Source: "core", }, } } // GetSurface returns the manifest for a surface by ID, or nil if not found. func (e *Engine) GetSurface(id string) *SurfaceManifest { for i := range e.surfaces { if e.surfaces[i].ID == id { return &e.surfaces[i] } } return nil } // Surfaces returns all registered surface manifests. func (e *Engine) Surfaces() []SurfaceManifest { return e.surfaces } // parseTemplates compiles all embedded templates with the custom FuncMap. func (e *Engine) parseTemplates() { funcMap := template.FuncMap{ "toJSON": toJSON, "eq": func(a, b string) bool { return a == b }, "contains": stringContains, "roleFilterType": roleFilterType, "esc": template.HTMLEscapeString, "safeHTML": func(s string) template.HTML { return template.HTML(s) }, "join": strings.Join, "dict": dict, "or": tmplOr, } tmpl, err := template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.html", "templates/components/*.html", "templates/surfaces/*.html", ) if err != nil { log.Fatalf("❌ Failed to parse templates: %v", err) } e.mu.Lock() e.templates = tmpl e.mu.Unlock() var names []string for _, t := range tmpl.Templates() { if t.Name() != "" { names = append(names, t.Name()) } } log.Printf("[pages] Parsed %d templates", len(names)) } // RegisterLoader adds a named data loader for a surface. func (e *Engine) RegisterLoader(name string, fn DataLoaderFunc) { e.loaders[name] = fn } // Render renders a named template into the response. func (e *Engine) Render(c *gin.Context, name string, data PageData) { data.BasePath = e.cfg.BasePath data.Version = Version data.Environment = e.cfg.Environment data.CSPNonce = generateNonce() data.Banner = e.loadBanner() data.Message = e.loadMessage() data.Footer = e.loadFooter() // v0.22.7: Default theme if not set if data.Theme == "" { data.Theme = "dark" } e.mu.RLock() tmpl := e.templates e.mu.RUnlock() c.Header("Content-Type", "text/html; charset=utf-8") c.Status(http.StatusOK) if err := tmpl.ExecuteTemplate(c.Writer, name, data); err != nil { log.Printf("[pages] Template render error (%s): %v", name, err) } } // RenderSurface is a Gin handler factory for surface routes. func (e *Engine) RenderSurface(surfaceID string) gin.HandlerFunc { return func(c *gin.Context) { user := e.getUserContext(c) var data any if loader, ok := e.loaders[surfaceID]; ok { var err error data, err = loader(c, e.stores) if err != nil { log.Printf("[pages] Data loader error (%s): %v", surfaceID, err) c.String(http.StatusInternalServerError, "Failed to load page data") return } } section := c.Param("section") if section == "" && surfaceID == "admin" { section = "users" } if section == "" && surfaceID == "settings" { section = "general" } if section == "" && surfaceID == "team-admin" { section = "members" } e.Render(c, "base.html", PageData{ Surface: surfaceID, Section: section, User: user, Data: data, Manifest: e.GetSurface(surfaceID), EnabledSurfaces: e.EnabledSurfaceIDs(), ExtensionSurfaces: e.extensionNavItems(), }) } } // RenderExtensionSurface handles /s/:slug — dynamic DB lookup for extension surfaces. // No restart required after installing a surface via admin API. func (e *Engine) RenderExtensionSurface() gin.HandlerFunc { return func(c *gin.Context) { slug := c.Param("slug") if slug == "" { c.String(http.StatusNotFound, "Surface not found") return } if e.stores.Packages == nil { c.String(http.StatusNotFound, "Surface registry not available") return } // Look up by ID (slug == surface ID) sr, err := e.stores.Packages.Get(c.Request.Context(), slug) if err != nil || sr == nil { c.String(http.StatusNotFound, "Surface not found: "+slug) return } if !sr.Enabled { c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/") return } if sr.Source == "core" { // Core surfaces have their own routes — don't serve them here c.String(http.StatusNotFound, "Surface not found: "+slug) return } user := e.getUserContext(c) // Build manifest from DB record route, _ := sr.Manifest["route"].(string) if route == "" { route = "/s/" + sr.ID } layout, _ := sr.Manifest["layout"].(string) if layout == "" { layout = "single" } manifest := &SurfaceManifest{ ID: sr.ID, Route: route, Title: sr.Title, Template: "surface-extension", Layout: layout, Source: "extension", } e.Render(c, "base.html", PageData{ Surface: sr.ID, User: user, Manifest: manifest, EnabledSurfaces: e.EnabledSurfaceIDs(), ExtensionSurfaces: e.extensionNavItems(), }) } } // PageRouteMiddleware holds middleware handlers for each auth level. // Passed to RegisterPageRoutes by main.go. type PageRouteMiddleware struct { Authenticated gin.HandlerFunc // AuthOrRedirect — for chat, editor, notes, settings Admin []gin.HandlerFunc // AuthOrRedirect + RequireAdminPage Session gin.HandlerFunc // AuthOrSession — for workflow } // RegisterPageRoutes generates Gin routes from the registered surface manifests. // Replaces manual per-surface route blocks in main.go. // // Groups surfaces by auth level to avoid redundant middleware chains. // Workflow surfaces use RenderWorkflow() (separate template + data model). // All other surfaces use RenderSurface(). func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddleware) { // Group surfaces by auth type byAuth := map[string][]SurfaceManifest{} for _, s := range e.surfaces { byAuth[s.Auth] = append(byAuth[s.Auth], s) } // Authenticated surfaces — single middleware group if surfaces, ok := byAuth["authenticated"]; ok && mw.Authenticated != nil { group := base.Group("") group.Use(mw.Authenticated) for _, s := range surfaces { if !e.IsSurfaceEnabled(s.ID) { // Disabled surface → redirect to chat registerRoutes(group, s, e.disabledRedirect()) continue } handler := e.RenderSurface(s.ID) registerRoutes(group, s, handler) } // v0.27.0: Extension surface catch-all — DB lookup at request time. // No restart needed after installing new surfaces via admin API. group.GET("/s/:slug", e.RenderExtensionSurface()) } // Admin surfaces — auth + admin role middleware if surfaces, ok := byAuth["admin"]; ok && len(mw.Admin) > 0 { for _, s := range surfaces { prefix := routePrefix(s.Route) group := base.Group(prefix) for _, m := range mw.Admin { group.Use(m) } // Admin surfaces are always enabled (needed to manage other surfaces) handler := e.RenderSurface(s.ID) registerRoutesRelative(group, prefix, s, handler) } } // Session surfaces (workflow) — session middleware if surfaces, ok := byAuth["session"]; ok && mw.Session != nil { for _, s := range surfaces { if !e.IsSurfaceEnabled(s.ID) { prefix := routePrefix(s.Route) group := base.Group(prefix) group.Use(mw.Session) registerRoutesRelative(group, prefix, s, e.disabledRedirect()) continue } prefix := routePrefix(s.Route) group := base.Group(prefix) group.Use(mw.Session) handler := e.surfaceHandler(s) registerRoutesRelative(group, prefix, s, handler) } } // Public surfaces — no middleware if surfaces, ok := byAuth["public"]; ok { for _, s := range surfaces { if !e.IsSurfaceEnabled(s.ID) { registerRoutes(base, s, e.disabledRedirect()) continue } handler := e.surfaceHandler(s) registerRoutes(base, s, handler) } } log.Printf("[pages] Registered routes for %d surfaces", len(e.surfaces)) } // surfaceHandler returns the appropriate Gin handler for a surface. func (e *Engine) surfaceHandler(s SurfaceManifest) gin.HandlerFunc { if s.ID == "workflow" { return e.RenderWorkflow() } if s.ID == "workflow-landing" { return e.RenderWorkflowLanding() } return e.RenderSurface(s.ID) } // registerRoutes adds the primary route and alt routes for a surface. // Used for surfaces in the root group (authenticated). func registerRoutes(group *gin.RouterGroup, s SurfaceManifest, handler gin.HandlerFunc) { group.GET(s.Route, handler) for _, alt := range s.AltRoutes { if alt != s.Route { group.GET(alt, handler) } } } // registerRoutesRelative adds routes relative to a group prefix. // Used for admin/session surfaces with their own prefix group. // "/admin/:section" with prefix "/admin" → "/:section" func registerRoutesRelative(group *gin.RouterGroup, prefix string, s SurfaceManifest, handler gin.HandlerFunc) { rel := stripPrefix(s.Route, prefix) group.GET(rel, handler) for _, alt := range s.AltRoutes { r := stripPrefix(alt, prefix) if r != rel { group.GET(r, handler) } } } // routePrefix extracts the first path segment as a group prefix. // "/admin/:section" → "/admin", "/w/:id" → "/w", "/" → "" func routePrefix(route string) string { trimmed := strings.TrimPrefix(route, "/") parts := strings.SplitN(trimmed, "/", 2) if len(parts) == 0 || parts[0] == "" || strings.HasPrefix(parts[0], ":") { return "" } return "/" + parts[0] } // stripPrefix removes a prefix from a route path. Returns "" if the // entire path IS the prefix (which Gin interprets as the group root). func stripPrefix(route, prefix string) string { if prefix == "" { return route } rel := strings.TrimPrefix(route, prefix) if rel == "" { rel = "" } return rel } // RenderLogin serves the standalone login page. func (e *Engine) RenderLogin() gin.HandlerFunc { return func(c *gin.Context) { // v0.22.7: Populate splash/login fields from global config instanceName, logoURL, tagline := e.loadBranding() regOpen := e.isRegistrationOpen() e.Render(c, "login.html", PageData{ Surface: "login", InstanceName: instanceName, LogoURL: logoURL, Tagline: tagline, RegistrationOpen: regOpen, AuthMode: e.cfg.AuthMode, }) } } // WorkflowPageData is passed to the workflow.html template via PageData.Data. type WorkflowPageData struct { ChannelID string ChannelTitle string ChannelDescription string SessionID string SessionName string StageMode string // custom | form_only | form_chat | review StageName string FormTemplateJSON string // typed form template JSON (empty if custom) TotalStages int CurrentStage int SurfacePkgID string // v0.30.2: custom package surface override (empty = use StageMode) BrandingJSON string // v0.35.0: workflow branding JSON (accent_color, logo_url, tagline) } // WorkflowLandingPageData is passed to workflow-landing.html. type WorkflowLandingPageData struct { WorkflowID string Scope string Slug string Name string Description string Branding struct { AccentColor string `json:"accent_color"` LogoURL string `json:"logo_url"` Tagline string `json:"tagline"` } PersonaName string PersonaIcon string StageCount int FirstStageMode string // custom | form_only | form_chat (v0.29.3) ResumeURL string // non-empty if visitor has an active session } // RenderWorkflow renders the workflow entry page for anonymous session visitors. // The middleware (AuthOrSession) has already created/resumed the session. func (e *Engine) RenderWorkflow() gin.HandlerFunc { return func(c *gin.Context) { channelID := c.GetString("channel_id") sessionID := c.GetString("session_id") // Load channel metadata var title, description string if title == "" { title = "Workflow" } // Load session display name sessionName := "Visitor" // Load workflow stage info for form rendering (v0.29.3) var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string var totalStages, currentStage int stageMode = "custom" // default instanceName, _, _ := e.loadBranding() e.Render(c, "workflow.html", PageData{ Surface: "workflow", InstanceName: instanceName, Data: WorkflowPageData{ ChannelID: channelID, ChannelTitle: title, ChannelDescription: description, SessionID: sessionID, SessionName: sessionName, StageMode: stageMode, StageName: stageName, FormTemplateJSON: formTplJSON, TotalStages: totalStages, CurrentStage: currentStage, SurfacePkgID: surfacePkgID, BrandingJSON: brandingJSON, }, }) } } // RenderWorkflowLanding renders the public landing page for a workflow. // Visitors see the workflow name, description, persona info, and a Start button. func (e *Engine) RenderWorkflowLanding() gin.HandlerFunc { return func(c *gin.Context) { ctx := c.Request.Context() // Route: /w/:id/:slug — :id is scope (team-id or "global"), // :slug is the workflow slug. Same :id param name as /w/:id // (chat surface) to avoid Gin wildcard conflicts. scope := c.Param("id") slug := c.Param("slug") // Resolve scope to team_id var teamID *string if scope != "global" { teamID = &scope } if e.stores.Workflows == nil { c.String(http.StatusNotFound, "Workflows not available") return } wf, err := e.stores.Workflows.GetBySlug(ctx, teamID, slug) if err != nil || wf == nil { c.String(http.StatusNotFound, "Workflow not found") return } if !wf.IsActive { c.String(http.StatusNotFound, "Workflow not available") return } data := WorkflowLandingPageData{ WorkflowID: wf.ID, Scope: scope, Slug: slug, Name: wf.Name, Description: wf.Description, } // Parse branding if len(wf.Branding) > 0 { _ = json.Unmarshal(wf.Branding, &data.Branding) } // Load stages for count + first persona info + stage mode stages, _ := e.stores.Workflows.ListStages(ctx, wf.ID) data.StageCount = len(stages) if len(stages) > 0 { data.FirstStageMode = stages[0].StageMode if data.FirstStageMode == "" { data.FirstStageMode = "custom" } // Persona lookup deferred — personas are extensions now } instanceName, _, _ := e.loadBranding() e.Render(c, "workflow-landing.html", PageData{ Surface: "workflow-landing", InstanceName: instanceName, Data: data, }) } } // getUserContext extracts user info from gin context (set by auth middleware) // and enriches it with display name, username, and email from the database. func (e *Engine) getUserContext(c *gin.Context) *UserContext { userID, exists := c.Get("user_id") if !exists { return nil } uid := userID.(string) uc := &UserContext{ ID: uid, Email: c.GetString("email"), Role: c.GetString("role"), } // Enrich from DB — username, display_name, and email may not be in JWT claims. if e.stores.Users != nil { if u, err := e.stores.Users.GetByID(c.Request.Context(), uid); err == nil && u != nil { uc.Username = u.Username uc.DisplayName = u.DisplayName if uc.Email == "" { uc.Email = u.Email } } } return uc } // loadBanner reads banner config from global settings. func (e *Engine) loadBanner() BannerConfig { if e.stores.GlobalConfig == nil { return BannerConfig{} } raw, err := e.stores.GlobalConfig.Get(context.Background(), "banner") if err != nil || raw == nil { return BannerConfig{} } b := BannerConfig{} if v, ok := raw["enabled"].(bool); ok { b.Visible = v } if v, ok := raw["text"].(string); ok { b.Text = v } if v, ok := raw["fg"].(string); ok { b.Color = v } if v, ok := raw["bg"].(string); ok { b.Background = v } return b } // loadMessage reads dismissible message bar config from global settings. func (e *Engine) loadMessage() MessageConfig { if e.stores.GlobalConfig == nil { return MessageConfig{} } raw, err := e.stores.GlobalConfig.Get(context.Background(), "message") if err != nil || raw == nil { return MessageConfig{} } m := MessageConfig{} if v, ok := raw["enabled"].(bool); ok { m.Visible = v } if v, ok := raw["text"].(string); ok { m.Text = v } if v, ok := raw["variant"].(string); ok { m.Variant = v } if m.Variant == "" { m.Variant = "info" } return m } // loadFooter reads optional footer bar config from global settings. func (e *Engine) loadFooter() FooterConfig { if e.stores.GlobalConfig == nil { return FooterConfig{} } raw, err := e.stores.GlobalConfig.Get(context.Background(), "footer") if err != nil || raw == nil { return FooterConfig{} } f := FooterConfig{} if v, ok := raw["enabled"].(bool); ok { f.Visible = v } if v, ok := raw["text"].(string); ok { f.Text = v } return f } // loadBranding reads instance branding from global settings. func (e *Engine) loadBranding() (name, logoURL, tagline string) { name = "Switchboard Core" // default if e.stores.GlobalConfig == nil { return } raw, err := e.stores.GlobalConfig.Get(context.Background(), "branding") if err != nil || raw == nil { return } if v, ok := raw["instance_name"].(string); ok && v != "" { name = v } if v, ok := raw["logo_url"].(string); ok { logoURL = v } if v, ok := raw["tagline"].(string); ok { tagline = v } return } // isRegistrationOpen checks if self-registration is enabled. func (e *Engine) isRegistrationOpen() bool { if e.stores.GlobalConfig == nil { return false } raw, err := e.stores.GlobalConfig.Get(context.Background(), "registration") if err != nil || raw == nil { return false } if v, ok := raw["enabled"].(bool); ok { return v } return false } // Version is injected at build time via ldflags. var Version = "dev" // SetVersion allows main.go to inject the build version. func SetVersion(v string) { Version = v } // WriteError writes an error page directly to the response. func (e *Engine) WriteError(w http.ResponseWriter, status int, msg string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) fmt.Fprintf(w, `Error

%d

%s

`, status, template.HTMLEscapeString(msg)) } // ── Template FuncMap helpers ───────────────── func toJSON(v any) template.JS { b, err := json.Marshal(v) if err != nil { return template.JS("null") } return template.JS(b) } func stringContains(haystack []string, needle string) bool { for _, s := range haystack { if s == needle { return true } } return false } // roleFilterType returns the model_type filter for a given role name. func roleFilterType(role string) string { switch role { case "embedding": return "embedding" case "utility": return "chat" default: return "" } } // dict creates a map from alternating key/value pairs for template use. func dict(pairs ...any) map[string]any { m := make(map[string]any, len(pairs)/2) for i := 0; i < len(pairs)-1; i += 2 { key, ok := pairs[i].(string) if !ok { continue } m[key] = pairs[i+1] } return m } // tmplOr returns the first non-empty string argument. func tmplOr(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func generateNonce() string { b := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, b); err != nil { return "fallback-nonce" } return hex.EncodeToString(b) }