From d58b4182c5b02f1e5e371b9f80e4e218d8100e5d Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 26 Feb 2025 18:53:35 -0500 Subject: [PATCH 1/3] :sparkles: Add request blocking until UI is clicked/submitted --- cmd/ui-server/server.go | 311 +---------- cmd/ui-server/templates.templ | 28 +- cmd/ui-server/templates_templ.go | 552 +++++++++---------- cmd/ui-server/templates_templ.txt | 7 +- go.mod | 2 + go.sum | 4 +- pkg/events/types.go | 7 +- pkg/server/ui/handler.go | 401 ++++++++++++++ pkg/server/ui/registry.go | 73 +++ ttmp/2025-02-21/changelog.md | 10 + ttmp/2025-02-22/05-ui-action-handler.md | 686 ++++++++++++++++++++++++ ttmp/2025-02-22/changelog.md | 15 +- 12 files changed, 1513 insertions(+), 583 deletions(-) create mode 100644 pkg/server/ui/handler.go create mode 100644 pkg/server/ui/registry.go create mode 100644 ttmp/2025-02-22/05-ui-action-handler.md diff --git a/cmd/ui-server/server.go b/cmd/ui-server/server.go index 776fcd6..c663e72 100644 --- a/cmd/ui-server/server.go +++ b/cmd/ui-server/server.go @@ -19,6 +19,7 @@ import ( "github.com/go-go-golems/clay/pkg/watcher" "github.com/go-go-golems/go-go-mcp/pkg/events" "github.com/go-go-golems/go-go-mcp/pkg/server/sse" + "github.com/go-go-golems/go-go-mcp/pkg/server/ui" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" "gopkg.in/yaml.v3" @@ -26,7 +27,7 @@ import ( type Server struct { dir string - pages map[string]UIDefinition + pages map[string]ui.UIDefinition routes map[string]http.HandlerFunc watcher *watcher.Watcher mux *http.ServeMux @@ -35,10 +36,7 @@ type Server struct { subscriber message.Subscriber events events.EventManager sseHandler *sse.SSEHandler -} - -type UIDefinition struct { - Components []map[string]interface{} `yaml:"components"` + uiHandler *ui.UIHandler } func NewServer(dir string) (*Server, error) { @@ -57,15 +55,19 @@ func NewServer(dir string) (*Server, error) { // Create SSE handler sseHandler := sse.NewSSEHandler(eventManager, &log.Logger) + // Create UI handler + uiHandler := ui.NewUIHandler(eventManager, sseHandler, &log.Logger) + s := &Server{ dir: dir, - pages: make(map[string]UIDefinition), + pages: make(map[string]ui.UIDefinition), routes: make(map[string]http.HandlerFunc), mux: http.NewServeMux(), publisher: publisher, subscriber: publisher, events: eventManager, sseHandler: sseHandler, + uiHandler: uiHandler, } // Register component renderers @@ -88,14 +90,8 @@ func NewServer(dir string) (*Server, error) { // Set up static file handler s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - // Set up UI update endpoint - s.mux.Handle("/api/ui-update", s.handleUIUpdate()) - - // Set up UI action endpoint - s.mux.Handle("/api/ui-action", s.handleUIAction()) - - // Set up UI update page - s.mux.HandleFunc("/ui", s.handleUIUpdatePage()) + // Register UI handlers + uiHandler.RegisterHandlers(s.mux) // Set up dynamic page handler - must come before index handler s.mux.Handle("/pages/", s.handleAllPages()) @@ -178,7 +174,7 @@ func (s *Server) loadPages() error { // First, clear existing pages and routes s.mu.Lock() - s.pages = make(map[string]UIDefinition) + s.pages = make(map[string]ui.UIDefinition) s.routes = make(map[string]http.HandlerFunc) s.mu.Unlock() @@ -212,7 +208,7 @@ func (s *Server) loadPage(path string) error { return fmt.Errorf("failed to read file %s: %w", path, err) } - var def UIDefinition + var def ui.UIDefinition if err := yaml.Unmarshal(data, &def); err != nil { log.Error().Err(err).Str("path", path).Msg("Failed to parse YAML") return fmt.Errorf("failed to parse YAML in %s: %w", path, err) @@ -337,7 +333,7 @@ func (s *Server) registerComponentRenderers() { log.Debug().Str("pageID", pageID).Msg("Rendering page content template") // Get the page definition - var def UIDefinition + var def ui.UIDefinition // Try to extract definition from event data if compData, ok := data.(map[string]interface{}); ok { @@ -371,284 +367,3 @@ func (s *Server) registerComponentRenderers() { return buf.String(), nil }) } - -// handleUIUpdate handles POST requests to /api/ui-update -func (s *Server) handleUIUpdate() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Only accept POST requests - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Parse JSON body - var jsonData map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "json_parse_error", - "message": "Invalid JSON: " + err.Error(), - }, - }) - return - } - - // Convert to YAML for storage - yamlData, err := yaml.Marshal(jsonData) - if err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "yaml_conversion_error", - "message": "Failed to convert to YAML: " + err.Error(), - }, - }) - return - } - - // Parse into UIDefinition - var def UIDefinition - if err := yaml.Unmarshal(yamlData, &def); err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "ui_definition_error", - "message": "Invalid UI definition: " + err.Error(), - "yaml": string(yamlData), - }, - }) - return - } - - // Validate the UI definition - validationErrors := s.validateUIDefinition(def) - if len(validationErrors) > 0 { - // Return detailed JSON error response with validation errors - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "ui_validation_error", - "message": "UI definition validation failed", - "details": validationErrors, - }, - }) - return - } - - // Create and publish refresh-ui event - event := events.UIEvent{ - Type: "refresh-ui", - PageID: "ui-update", - Component: def, - } - - if err := s.events.Publish("ui-update", event); err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "event_publish_error", - "message": "Failed to publish event: " + err.Error(), - }, - }) - return - } - - // Return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - if err != nil { - http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) - } - } -} - -// validateUIDefinition checks a UI definition for common errors -func (s *Server) validateUIDefinition(def UIDefinition) []map[string]interface{} { - var errors []map[string]interface{} - - // Check if components exist - if len(def.Components) == 0 { - errors = append(errors, map[string]interface{}{ - "path": "components", - "message": "No components defined", - }) - return errors - } - - // Validate each component - for i, comp := range def.Components { - for typ, props := range comp { - propsMap, ok := props.(map[string]interface{}) - if !ok { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s", i, typ), - "message": "Component properties must be a map", - }) - continue - } - - // Check for required ID - if _, hasID := propsMap["id"]; !hasID && requiresID(typ) { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s", i, typ), - "message": "Component is missing required 'id' property", - }) - } - - // Validate nested components in forms - if typ == "form" { - if formComps, hasComps := propsMap["components"]; hasComps { - if formCompsList, ok := formComps.([]interface{}); ok { - for j, formComp := range formCompsList { - if formCompMap, ok := formComp.(map[string]interface{}); ok { - for formCompType, formCompProps := range formCompMap { - if _, ok := formCompProps.(map[string]interface{}); !ok { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.components[%d].%s", i, typ, j, formCompType), - "message": "Form component properties must be a map", - }) - } - } - } else { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.components[%d]", i, typ, j), - "message": "Form component must be a map", - }) - } - } - } else { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.components", i, typ), - "message": "Form components must be an array", - }) - } - } - } - - // Validate list items - if typ == "list" { - if items, hasItems := propsMap["items"]; hasItems { - if itemsList, ok := items.([]interface{}); ok { - for j, item := range itemsList { - if _, ok := item.(map[string]interface{}); !ok { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.items[%d]", i, typ, j), - "message": "List item must be a map", - }) - } - } - } else { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.items", i, typ), - "message": "List items must be an array", - }) - } - } - } - } - } - - return errors -} - -// requiresID returns true if the component type requires an ID -func requiresID(componentType string) bool { - switch componentType { - case "text", "title": - // These can optionally have IDs - return false - default: - // All other components require IDs - return true - } -} - -// handleUIAction handles POST requests to /api/ui-action -func (s *Server) handleUIAction() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Only accept POST requests - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Parse JSON body - var action struct { - ComponentID string `json:"componentId"` - Action string `json:"action"` - Data map[string]interface{} `json:"data"` - } - - if err := json.NewDecoder(r.Body).Decode(&action); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - return - } - - // Determine if this is an important event to log at INFO level - isImportantEvent := false - switch action.Action { - case "clicked", "changed", "submitted": - isImportantEvent = true - } - - // Log the action at appropriate level - logger := log.Debug() - if isImportantEvent { - logger = log.Info() - } - - // Create log entry with component and action info - logger = logger. - Str("componentId", action.ComponentID). - Str("action", action.Action) - - // Add data to log if it exists and is relevant - if len(action.Data) > 0 { - // For form submissions, log the form data in detail - if action.Action == "submitted" && action.Data["formData"] != nil { - logger = logger.Interface("formData", action.Data["formData"]) - } else if action.Action == "changed" { - // For changed events, log the new value - logger = logger.Interface("data", action.Data) - } else { - // For other events, just log that data exists - logger = logger.Bool("hasData", true) - } - } - - // Output the log message - logger.Msg("UI action received") - - // Return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - if err != nil { - http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) - } - } -} - -// handleUIUpdatePage renders the UI update page -func (s *Server) handleUIUpdatePage() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - component := uiUpdateTemplate() - _ = component.Render(r.Context(), w) - } -} diff --git a/cmd/ui-server/templates.templ b/cmd/ui-server/templates.templ index 6ada651..21586fd 100644 --- a/cmd/ui-server/templates.templ +++ b/cmd/ui-server/templates.templ @@ -5,6 +5,7 @@ import ( "gopkg.in/yaml.v3" "strings" "github.com/rs/zerolog/log" + "github.com/go-go-golems/go-go-mcp/pkg/server/ui" ) templ base(title string) { @@ -48,7 +49,10 @@ templ base(title string) { } function sendUIAction(componentId, action, data = {}) { - logToConsole(`Component ${componentId} ${action}`); + // Get the request ID from the page if it exists + const requestId = document.querySelector('[data-request-id]')?.getAttribute('data-request-id') || ''; + + logToConsole(`Component ${componentId} ${action}${requestId ? ' (request: ' + requestId + ')' : ''}`); // If this is a form submission, collect all form data if (action === 'submitted' && document.getElementById(componentId)) { @@ -94,7 +98,8 @@ templ base(title string) { body: JSON.stringify({ componentId: componentId, action: action, - data: data + data: data, + requestId: requestId // Include the request ID in the action data }) }) .then(response => response.json()) @@ -187,7 +192,7 @@ templ base(title string) { } -templ indexTemplate(pages map[string]UIDefinition) { +templ indexTemplate(pages map[string]ui.UIDefinition) { @base("UI Server - Index") {
@@ -204,7 +209,7 @@ templ indexTemplate(pages map[string]UIDefinition) { } } -templ pageTemplate(name string, def UIDefinition) { +templ pageTemplate(name string, def ui.UIDefinition) { @base("UI Server - " + name) {
Rendered UI
-
+
for _, component := range def.Components { for typ, props := range component { @renderComponent(typ, props.(map[string]interface{})) @@ -453,7 +458,7 @@ templ renderComponent(typ string, props map[string]interface{}) { } } -func yamlString(def UIDefinition) string { +func yamlString(def ui.UIDefinition) string { yamlBytes, err := yaml.Marshal(def) if err != nil { errMsg := fmt.Sprintf("Error marshaling YAML: %v", err) @@ -476,6 +481,7 @@ templ uiUpdateTemplate() { hx-ext="sse" sse-connect="/sse?page=ui-update" sse-swap="ui-update" + data-request-id="" >
Waiting for UI updates... @@ -493,6 +499,14 @@ templ uiUpdateTemplate() { // Re-highlight the code hljs.highlightElement(el); }); + + // Extract request ID from the updated content if available + const newRequestId = event.detail.target.querySelector('[data-request-id]')?.getAttribute('data-request-id'); + if (newRequestId) { + // Update the parent container with the new request ID + event.detail.target.closest('[data-request-id]')?.setAttribute('data-request-id', newRequestId); + console.log('Updated request ID to:', newRequestId); + } }); } diff --git a/cmd/ui-server/templates_templ.go b/cmd/ui-server/templates_templ.go index 2c7057f..4d7f8b4 100644 --- a/cmd/ui-server/templates_templ.go +++ b/cmd/ui-server/templates_templ.go @@ -10,6 +10,7 @@ import templruntime "github.com/a-h/templ/runtime" import ( "fmt" + "github.com/go-go-golems/go-go-mcp/pkg/server/ui" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" "strings" @@ -43,7 +44,7 @@ func base(title string) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 16, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 17, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -65,7 +66,7 @@ func base(title string) templ.Component { }) } -func indexTemplate(pages map[string]UIDefinition) templ.Component { +func indexTemplate(pages map[string]ui.UIDefinition) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -119,7 +120,7 @@ func indexTemplate(pages map[string]UIDefinition) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 198, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 203, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -144,7 +145,7 @@ func indexTemplate(pages map[string]UIDefinition) templ.Component { }) } -func pageTemplate(name string, def UIDefinition) templ.Component { +func pageTemplate(name string, def ui.UIDefinition) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -184,7 +185,7 @@ func pageTemplate(name string, def UIDefinition) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("/sse?page=" + name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 211, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 216, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -212,7 +213,7 @@ func pageTemplate(name string, def UIDefinition) templ.Component { }) } -func pageContentTemplate(name string, def UIDefinition) templ.Component { +func pageContentTemplate(name string, def ui.UIDefinition) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -237,6 +238,19 @@ func pageContentTemplate(name string, def UIDefinition) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(def.RequestID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 231, Col: 58} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 13) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } for _, component := range def.Components { for typ, props := range component { templ_7745c5c3_Err = renderComponent(typ, props.(map[string]interface{})).Render(ctx, templ_7745c5c3_Buffer) @@ -245,20 +259,20 @@ func pageContentTemplate(name string, def UIDefinition) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 13) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 14) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(yamlString(def)) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(yamlString(def)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 241, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 246, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 14) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 15) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -282,256 +296,243 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var12 := templ.GetChildren(ctx) - if templ_7745c5c3_Var12 == nil { - templ_7745c5c3_Var12 = templ.NopComponent + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent } ctx = templ.ClearChildren(ctx) switch typ { case "button": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 15) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 16) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 263, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 268, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 16) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 17) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 = []any{ + var templ_7745c5c3_Var15 = []any{ "btn", templ.KV("btn-primary", props["type"] == "primary"), templ.KV("btn-secondary", props["type"] == "secondary"), templ.KV("btn-danger", props["type"] == "danger"), templ.KV("btn-success", props["type"] == "success"), } - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 17) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 18) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 265, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 270, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 18) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 19) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 266, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 271, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 19) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 20) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if disabled, ok := props["disabled"].(bool); ok && disabled { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 20) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 21) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 21) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 22) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String()) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String()) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 22) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 23) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if text, ok := props["text"].(string); ok { - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(text) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 279, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 284, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 23) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 24) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "title": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 24) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 25) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 286, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 291, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 25) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 26) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 288, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 293, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 26) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 27) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 289, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 294, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 27) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 28) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if content, ok := props["content"].(string); ok { - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(content) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 292, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 297, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 28) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 29) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "text": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 29) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 30) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 299, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 304, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 30) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 31) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 301, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 306, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 31) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 32) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 302, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 307, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 32) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 33) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if content, ok := props["content"].(string); ok { - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(content) + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 305, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 310, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 33) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 34) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "input": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 34) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 312, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 35) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(id) + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 314, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 317, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -542,9 +543,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 315, Col: 95} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 319, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -555,9 +556,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 316, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 320, Col: 95} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { @@ -568,9 +569,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 317, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 321, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -580,116 +581,116 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 322, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 40) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } if typ, ok := props["type"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 40) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 41) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(typ) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(typ) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 319, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 324, Col: 17} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 41) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 42) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if placeholder, ok := props["placeholder"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 42) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 43) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 322, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 327, Col: 32} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 43) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 44) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if value, ok := props["value"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 44) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 45) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(value) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 325, Col: 20} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 330, Col: 20} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 45) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 46) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if required, ok := props["required"].(bool); ok && required { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 46) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 47) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if name, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 47) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 48) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(name) + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 331, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 336, Col: 18} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 48) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 49) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 49) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 50) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "textarea": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 50) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 339, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 51) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var37 string - templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(id) + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 341, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 344, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { @@ -700,9 +701,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) + templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 342, Col: 95} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 346, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { @@ -713,9 +714,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 343, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 347, Col: 95} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { @@ -726,9 +727,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var40 string - templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 344, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 348, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) if templ_7745c5c3_Err != nil { @@ -738,196 +739,209 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 349, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 56) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } if rows, ok := props["rows"].(int); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 56) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 57) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(rows)) + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(rows)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 346, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 351, Col: 30} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 57) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 58) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if cols, ok := props["cols"].(int); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 58) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 59) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cols)) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cols)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 349, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 354, Col: 30} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 59) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 60) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if placeholder, ok := props["placeholder"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 60) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 61) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 352, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 357, Col: 32} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 61) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 62) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 62) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 63) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if value, ok := props["value"].(string); ok { - var templ_7745c5c3_Var44 string - templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(value) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 357, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 362, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 63) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 64) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "checkbox": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 64) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 65) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var45 string - templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var46 string + templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 364, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 369, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 65) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 66) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var47 string + templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 368, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 373, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 66) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 67) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var47 string - templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {checked: this.checked})", id)) + var templ_7745c5c3_Var48 string + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {checked: this.checked})", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 369, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 374, Col: 100} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 67) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 68) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if name, ok := props["name"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 68) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 69) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(name) + var templ_7745c5c3_Var49 string + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 371, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 376, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 69) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 70) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if checked, ok := props["checked"].(bool); ok && checked { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 70) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 71) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if required, ok := props["required"].(bool); ok && required { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 71) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 72) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 72) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 73) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if label, ok := props["label"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 73) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 74) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var49 string - templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var50 string + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 382, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 387, Col: 47} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 74) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 75) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var50 string - templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(label) + var templ_7745c5c3_Var51 string + templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 382, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 387, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 75) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 76) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 76) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 77) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -935,61 +949,61 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { case "list": if typ, ok := props["type"].(string); ok { if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 77) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 78) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var51 string - templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var52 string + templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 390, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 395, Col: 46} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 78) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 79) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if title, ok := props["title"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 79) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 80) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var52 string - templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(title) + var templ_7745c5c3_Var53 string + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 392, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 397, Col: 31} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 80) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 81) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if typ == "ul" { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 81) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 82) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if items, ok := props["items"].([]interface{}); ok { for _, item := range items { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 82) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 83) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch i := item.(type) { case string: - var templ_7745c5c3_Var53 string - templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(i) + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(i) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 401, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 406, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1001,35 +1015,35 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 83) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 84) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 84) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 85) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if typ == "ol" { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 85) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 86) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if items, ok := props["items"].([]interface{}); ok { for _, item := range items { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 86) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 87) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch i := item.(type) { case string: - var templ_7745c5c3_Var54 string - templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(i) + var templ_7745c5c3_Var55 string + templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(i) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 418, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 423, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1041,18 +1055,18 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 87) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 88) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 88) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 89) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 89) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 90) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1060,46 +1074,46 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } case "form": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 90) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 91) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var55 string - templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var56 string + templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 434, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 439, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 91) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 92) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var57 string + templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 436, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 441, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 92) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 93) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var57 string - templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("event.preventDefault(); sendUIAction('%s', 'submitted')", id)) + var templ_7745c5c3_Var58 string + templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("event.preventDefault(); sendUIAction('%s', 'submitted')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 437, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 442, Col: 100} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 93) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 94) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1115,7 +1129,7 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 94) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 95) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1125,7 +1139,7 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { }) } -func yamlString(def UIDefinition) string { +func yamlString(def ui.UIDefinition) string { yamlBytes, err := yaml.Marshal(def) if err != nil { errMsg := fmt.Sprintf("Error marshaling YAML: %v", err) @@ -1152,12 +1166,12 @@ func uiUpdateTemplate() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var58 := templ.GetChildren(ctx) - if templ_7745c5c3_Var58 == nil { - templ_7745c5c3_Var58 = templ.NopComponent + templ_7745c5c3_Var59 := templ.GetChildren(ctx) + if templ_7745c5c3_Var59 == nil { + templ_7745c5c3_Var59 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var60 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -1169,13 +1183,13 @@ func uiUpdateTemplate() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 95) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 96) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = base("Dynamic UI").Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = base("Dynamic UI").Render(templ.WithChildren(ctx, templ_7745c5c3_Var60), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/cmd/ui-server/templates_templ.txt b/cmd/ui-server/templates_templ.txt index 946bacf..c7673ac 100644 --- a/cmd/ui-server/templates_templ.txt +++ b/cmd/ui-server/templates_templ.txt @@ -1,5 +1,5 @@ - +

Available Pages

-
Rendered UI
+
Rendered UI
YAML Source

 
-
Dynamic UI
Waiting for UI updates...
+
Dynamic UI
Waiting for UI updates...
diff --git a/go.mod b/go.mod index e2c31ff..26158da 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sebdah/goldie/v2 v2.5.5 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect diff --git a/go.sum b/go.sum index 651adb3..ddff081 100644 --- a/go.sum +++ b/go.sum @@ -212,11 +212,11 @@ github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgY github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= -github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= diff --git a/pkg/events/types.go b/pkg/events/types.go index 22dc79d..2467399 100644 --- a/pkg/events/types.go +++ b/pkg/events/types.go @@ -8,9 +8,10 @@ import ( // UIEvent represents an event in the UI system that can trigger updates type UIEvent struct { - Type string `json:"type"` // e.g. "component-update", "page-reload" - PageID string `json:"pageId"` // Which page is being updated - Component interface{} `json:"component"` // The updated component data + Type string `json:"type"` // e.g. "component-update", "page-reload" + PageID string `json:"pageId"` // Which page is being updated + Component interface{} `json:"component"` // The updated component data + RequestID string `json:"requestId,omitempty"` // Optional request ID for tracking user actions } // EventManager defines the interface for managing UI events diff --git a/pkg/server/ui/handler.go b/pkg/server/ui/handler.go new file mode 100644 index 0000000..8f5a578 --- /dev/null +++ b/pkg/server/ui/handler.go @@ -0,0 +1,401 @@ +package ui + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/go-go-golems/go-go-mcp/pkg/events" + "github.com/go-go-golems/go-go-mcp/pkg/server/sse" + "github.com/google/uuid" + "github.com/rs/zerolog" + "gopkg.in/yaml.v3" +) + +// UIDefinition represents a UI page definition +type UIDefinition struct { + Components []map[string]interface{} `yaml:"components"` + RequestID string `yaml:"requestID,omitempty"` +} + +// UIHandler manages all UI-related functionality +type UIHandler struct { + waitRegistry *WaitRegistry + events events.EventManager + sseHandler *sse.SSEHandler + logger *zerolog.Logger + + // Configuration + timeout time.Duration +} + +// NewUIHandler creates a new UI handler with the given dependencies +func NewUIHandler(events events.EventManager, sseHandler *sse.SSEHandler, logger *zerolog.Logger) *UIHandler { + h := &UIHandler{ + waitRegistry: NewWaitRegistry(30 * time.Second), // 30 second default timeout + events: events, + sseHandler: sseHandler, + logger: logger, + timeout: 30 * time.Second, + } + + // Start background cleanup for orphaned requests + go h.cleanupOrphanedRequests(context.Background()) + + return h +} + +// RegisterHandlers registers all UI-related HTTP handlers with the given mux +func (h *UIHandler) RegisterHandlers(mux *http.ServeMux) { + mux.Handle("/api/ui-update", h.handleUIUpdate()) + mux.Handle("/api/ui-action", h.handleUIAction()) +} + +// cleanupOrphanedRequests periodically cleans up stale requests +func (h *UIHandler) cleanupOrphanedRequests(ctx context.Context) { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Perform cleanup + count := h.waitRegistry.CleanupStale() + if count > 0 { + h.logger.Debug().Int("count", count).Msg("Cleaned up orphaned requests") + } + + case <-ctx.Done(): + return + } + } +} + +// Helper method for sending error responses +func (h *UIHandler) sendErrorResponse(w http.ResponseWriter, status int, errorType, message string, details map[string]interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + errorResponse := map[string]interface{}{ + "status": "error", + "error": map[string]interface{}{ + "type": errorType, + "message": message, + }, + } + + // Add any additional details if provided + for k, v := range details { + errorResponse["error"].(map[string]interface{})[k] = v + } + + _ = json.NewEncoder(w).Encode(errorResponse) +} + +// Placeholder methods that will be implemented later +func (h *UIHandler) handleUIUpdate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var jsonData map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil { + // Return detailed JSON error response + h.sendErrorResponse(w, http.StatusBadRequest, "json_parse_error", "Invalid JSON: "+err.Error(), nil) + return + } + + // Generate a unique request ID + requestID := uuid.New().String() + + // Add the request ID to the UI definition + // This will be used by the frontend to associate actions with this request + jsonData["requestID"] = requestID + + // Convert to YAML for storage + yamlData, err := yaml.Marshal(jsonData) + if err != nil { + h.sendErrorResponse(w, http.StatusInternalServerError, "yaml_conversion_error", "Failed to convert to YAML: "+err.Error(), nil) + return + } + + // Parse into UIDefinition + var def UIDefinition + if err := yaml.Unmarshal(yamlData, &def); err != nil { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_definition_error", "Invalid UI definition: "+err.Error(), map[string]interface{}{ + "yaml": string(yamlData), + }) + return + } + + // Validate the UI definition + validationErrors := h.validateUIDefinition(def) + if len(validationErrors) > 0 { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_validation_error", "UI definition validation failed", map[string]interface{}{ + "details": validationErrors, + }) + return + } + + // Register this request in the wait registry + responseChan := h.waitRegistry.Register(requestID) + + // Create and publish refresh-ui event with the request ID + event := events.UIEvent{ + Type: "refresh-ui", + PageID: "ui-update", + Component: def, + } + + // Add the request ID to the event data if possible + if compData, ok := event.Component.(UIDefinition); ok { + compData.RequestID = requestID + event.Component = compData + } + + if err := h.events.Publish("ui-update", event); err != nil { + // Clean up the registry entry + h.waitRegistry.Cleanup(requestID) + h.sendErrorResponse(w, http.StatusInternalServerError, "event_publish_error", "Failed to publish event: "+err.Error(), nil) + return + } + + // Log that we're waiting for a response + h.logger.Debug(). + Str("requestId", requestID). + Msg("Waiting for UI action response") + + // Wait for the response or timeout + select { + case response := <-responseChan: + // Request was resolved with a user action + if response.Error != nil { + // There was an error processing the action + h.sendErrorResponse(w, http.StatusInternalServerError, "action_processing_error", response.Error.Error(), nil) + return + } + + // Log the successful response + h.logger.Info(). + Str("requestId", requestID). + Str("action", response.Action). + Str("componentId", response.ComponentID). + Msg("UI action received, completing request") + + // Return success with the action data + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "action": response.Action, + "componentId": response.ComponentID, + "data": response.Data, + }) + + case <-time.After(h.timeout): + // Request timed out + h.waitRegistry.Cleanup(requestID) + + h.logger.Warn(). + Str("requestId", requestID). + Dur("timeout", h.timeout). + Msg("Request timed out waiting for UI action") + + h.sendErrorResponse(w, http.StatusRequestTimeout, "timeout", "No user action received within the timeout period", nil) + } + } +} + +func (h *UIHandler) handleUIAction() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var action struct { + ComponentID string `json:"componentId"` + Action string `json:"action"` + Data map[string]interface{} `json:"data"` + RequestID string `json:"requestId"` // Added field for request ID + } + + if err := json.NewDecoder(r.Body).Decode(&action); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + // Determine if this is an important event to log at INFO level + isImportantEvent := false + switch action.Action { + case "clicked", "changed", "submitted": + isImportantEvent = true + } + + // Log the action at appropriate level + logger := h.logger.Debug() + if isImportantEvent { + logger = h.logger.Info() + } + + // Create log entry with component and action info + logger = logger. + Str("componentId", action.ComponentID). + Str("action", action.Action) + + // Add request ID to log if present + if action.RequestID != "" { + logger = logger.Str("requestId", action.RequestID) + } + + // Add data to log if it exists and is relevant + if len(action.Data) > 0 { + // For form submissions, log the form data in detail + if action.Action == "submitted" && action.Data["formData"] != nil { + logger = logger.Interface("formData", action.Data["formData"]) + } else if action.Action == "changed" { + // For changed events, log the new value + logger = logger.Interface("data", action.Data) + } else { + // For other events, just log that data exists + logger = logger.Bool("hasData", true) + } + } + + // Output the log message + logger.Msg("UI action received") + + // Check if this action is associated with a waiting request + if action.RequestID != "" && (action.Action == "clicked" || action.Action == "submitted") { + // Try to resolve the waiting request + resolved := h.waitRegistry.Resolve(action.RequestID, UIActionResponse{ + Action: action.Action, + ComponentID: action.ComponentID, + Data: action.Data, + Error: nil, + Timestamp: time.Now(), + }) + + if resolved { + logger.Bool("waitingRequestResolved", true).Msg("Resolved waiting request") + } + } + + // Return success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + if err != nil { + http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } + } +} + +// validateUIDefinition checks a UI definition for common errors +func (h *UIHandler) validateUIDefinition(def UIDefinition) []map[string]interface{} { + var errors []map[string]interface{} + + // Check if components exist + if len(def.Components) == 0 { + errors = append(errors, map[string]interface{}{ + "path": "components", + "message": "No components defined", + }) + return errors + } + + // Validate each component + for i, comp := range def.Components { + for typ, props := range comp { + propsMap, ok := props.(map[string]interface{}) + if !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component properties must be a map", + }) + continue + } + + // Check for required ID + if _, hasID := propsMap["id"]; !hasID && h.requiresID(typ) { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component is missing required 'id' property", + }) + } + + // Validate nested components in forms + if typ == "form" { + if formComps, hasComps := propsMap["components"]; hasComps { + if formCompsList, ok := formComps.([]interface{}); ok { + for j, formComp := range formCompsList { + if formCompMap, ok := formComp.(map[string]interface{}); ok { + for formCompType, formCompProps := range formCompMap { + if _, ok := formCompProps.(map[string]interface{}); !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.components[%d].%s", i, typ, j, formCompType), + "message": "Form component properties must be a map", + }) + } + } + } else { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.components[%d]", i, typ, j), + "message": "Form component must be a map", + }) + } + } + } else { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.components", i, typ), + "message": "Form components must be an array", + }) + } + } + } + + // Validate list items + if typ == "list" { + if items, hasItems := propsMap["items"]; hasItems { + if itemsList, ok := items.([]interface{}); ok { + for j, item := range itemsList { + if _, ok := item.(map[string]interface{}); !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.items[%d]", i, typ, j), + "message": "List item must be a map", + }) + } + } + } else { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.items", i, typ), + "message": "List items must be an array", + }) + } + } + } + } + } + + return errors +} + +// requiresID returns true if the component type requires an ID +func (h *UIHandler) requiresID(componentType string) bool { + switch componentType { + case "text", "title": + // These can optionally have IDs + return false + default: + // All other components require IDs + return true + } +} diff --git a/pkg/server/ui/registry.go b/pkg/server/ui/registry.go new file mode 100644 index 0000000..2257c6d --- /dev/null +++ b/pkg/server/ui/registry.go @@ -0,0 +1,73 @@ +package ui + +import ( + "sync" + "time" +) + +// UIActionResponse represents the response from a UI action +type UIActionResponse struct { + Action string + ComponentID string + Data map[string]interface{} + Error error + Timestamp time.Time +} + +// WaitRegistry tracks pending UI update requests waiting for user actions +type WaitRegistry struct { + pending map[string]chan UIActionResponse + mu sync.RWMutex + timeout time.Duration +} + +// NewWaitRegistry creates a new registry with the specified timeout +func NewWaitRegistry(timeout time.Duration) *WaitRegistry { + return &WaitRegistry{ + pending: make(map[string]chan UIActionResponse), + timeout: timeout, + } +} + +// Register adds a new request to the registry and returns a channel for the response +func (wr *WaitRegistry) Register(requestID string) chan UIActionResponse { + wr.mu.Lock() + defer wr.mu.Unlock() + + responseChan := make(chan UIActionResponse, 1) + wr.pending[requestID] = responseChan + + return responseChan +} + +// Resolve completes a pending request with the given action response +func (wr *WaitRegistry) Resolve(requestID string, response UIActionResponse) bool { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + ch <- response + delete(wr.pending, requestID) + return true + } + return false +} + +// Cleanup removes a request from the registry +func (wr *WaitRegistry) Cleanup(requestID string) { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + close(ch) + delete(wr.pending, requestID) + } +} + +// CleanupStale removes requests that have been in the registry longer than the timeout +// This is a placeholder implementation that would need to be enhanced with timestamp tracking +func (wr *WaitRegistry) CleanupStale() int { + // This would require tracking timestamps for each request + // For now, we'll just return 0 as we're not implementing the full functionality yet + return 0 +} diff --git a/ttmp/2025-02-21/changelog.md b/ttmp/2025-02-21/changelog.md index 3e24597..cf23c95 100644 --- a/ttmp/2025-02-21/changelog.md +++ b/ttmp/2025-02-21/changelog.md @@ -8,6 +8,16 @@ Enhanced the UI action handling documentation with structured metadata: - Added maintenance triggers to identify when document updates are needed - Structured questions the document answers for better discoverability +UI Action Request ID Tracking + +Implemented request ID tracking in UI actions to enable synchronous wait-for-response behavior: +- Updated sendUIAction JavaScript function to include request ID in action data +- Added request ID extraction from UI definition in page templates +- Enhanced UI update template to maintain request ID during SSE updates +- Improved logging to show request ID in console messages +- Added data attributes to store request ID in the DOM +- Ensured proper propagation of request ID between server and client + UI DSL Documentation Added comprehensive documentation for the UI DSL system in pkg/doc/topics/05-ui-dsl.md. The documentation includes: diff --git a/ttmp/2025-02-22/05-ui-action-handler.md b/ttmp/2025-02-22/05-ui-action-handler.md new file mode 100644 index 0000000..63b3706 --- /dev/null +++ b/ttmp/2025-02-22/05-ui-action-handler.md @@ -0,0 +1,686 @@ +# Technical Plan: Implementing Wait-for-Response in UI Update Endpoint + +## Overview + +The goal is to enhance the `/api/ui-update` endpoint to wait for a corresponding user action (submit or click) before completing the request. This creates a synchronous flow between UI updates and user interactions. Additionally, we'll refactor the UI handlers into their own struct to improve code organization. + +## Key Components + +1. A wait registry to track pending requests +2. UUID generation for request tracking +3. Timeout handling for requests without responses +4. A dedicated UI handler struct for better code organization +5. Frontend modifications to pass request IDs with actions + +## Detailed Implementation Plan + +### 1. Create a UIHandler Struct + +- [x] Define a new `UIHandler` struct to encapsulate all UI-related functionality +- [x] Move UI-specific methods from the Server struct to the UIHandler +- [x] Create a clean interface between Server and UIHandler + +```go +// UIHandler manages all UI-related functionality +type UIHandler struct { + waitRegistry *WaitRegistry + events events.EventManager + sseHandler *sse.SSEHandler + logger *zerolog.Logger + + // Configuration + timeout time.Duration +} + +// NewUIHandler creates a new UI handler with the given dependencies +func NewUIHandler(events events.EventManager, sseHandler *sse.SSEHandler, logger *zerolog.Logger) *UIHandler { + return &UIHandler{ + waitRegistry: NewWaitRegistry(30 * time.Second), // 30 second default timeout + events: events, + sseHandler: sseHandler, + logger: logger, + timeout: 30 * time.Second, + } +} + +// RegisterHandlers registers all UI-related HTTP handlers with the given mux +func (h *UIHandler) RegisterHandlers(mux *http.ServeMux) { + mux.Handle("/api/ui-update", h.handleUIUpdate()) + mux.Handle("/api/ui-action", h.handleUIAction()) +} +``` + +### 2. Create a Wait Registry Structure + +- [x] Define a `WaitRegistry` struct to track pending requests +- [x] Implement methods to register, resolve, and timeout requests +- [x] Use channels for synchronization between the update endpoint and action handler + +```go +// WaitRegistry tracks pending UI update requests waiting for user actions +type WaitRegistry struct { + pending map[string]chan UIActionResponse + mu sync.RWMutex + timeout time.Duration +} + +// UIActionResponse represents the response from a UI action +type UIActionResponse struct { + Action string + ComponentID string + Data map[string]interface{} + Error error + Timestamp time.Time +} + +// NewWaitRegistry creates a new registry with the specified timeout +func NewWaitRegistry(timeout time.Duration) *WaitRegistry { + return &WaitRegistry{ + pending: make(map[string]chan UIActionResponse), + timeout: timeout, + } +} + +// Register adds a new request to the registry and returns a channel for the response +func (wr *WaitRegistry) Register(requestID string) chan UIActionResponse { + wr.mu.Lock() + defer wr.mu.Unlock() + + responseChan := make(chan UIActionResponse, 1) + wr.pending[requestID] = responseChan + + return responseChan +} + +// Resolve completes a pending request with the given action response +func (wr *WaitRegistry) Resolve(requestID string, response UIActionResponse) bool { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + ch <- response + delete(wr.pending, requestID) + return true + } + return false +} + +// Cleanup removes a request from the registry +func (wr *WaitRegistry) Cleanup(requestID string) { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + close(ch) + delete(wr.pending, requestID) + } +} + +// CleanupStale removes requests that have been in the registry longer than the timeout +func (wr *WaitRegistry) CleanupStale() int { + wr.mu.Lock() + defer wr.mu.Unlock() + + // This would require tracking timestamps for each request + // Implementation would depend on how we track request timestamps + return 0 +} +``` + +### 3. Update the Server Struct + +- [x] Remove UI-specific handlers from the Server struct +- [x] Add the UIHandler as a field in the Server struct +- [x] Update the Server initialization to create and use the UIHandler + +```go +type Server struct { + dir string + pages map[string]UIDefinition + routes map[string]http.HandlerFunc + watcher *watcher.Watcher + mux *http.ServeMux + mu sync.RWMutex + publisher message.Publisher + subscriber message.Subscriber + events events.EventManager + sseHandler *sse.SSEHandler + uiHandler *UIHandler // New field for UI handler +} + +func NewServer(dir string) (*Server, error) { + // ... existing initialization + + // Create SSE handler + sseHandler := sse.NewSSEHandler(eventManager, &log.Logger) + + // Create UI handler + uiHandler := NewUIHandler(eventManager, sseHandler, &log.Logger) + + s := &Server{ + dir: dir, + pages: make(map[string]UIDefinition), + routes: make(map[string]http.HandlerFunc), + mux: http.NewServeMux(), + publisher: publisher, + subscriber: publisher, + events: eventManager, + sseHandler: sseHandler, + uiHandler: uiHandler, + } + + // Register component renderers + s.registerComponentRenderers() + + // ... rest of initialization + + // Set up SSE endpoint + s.mux.Handle("/sse", sseHandler) + + // Set up static file handler + s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + // Register UI handlers + uiHandler.RegisterHandlers(s.mux) + + // ... rest of initialization + + return s, nil +} +``` + +### 4. Implement the UI Update Handler in UIHandler + +- [x] Move the UI update handler from Server to UIHandler +- [x] Generate a unique request ID for each update +- [x] Add the request ID to the UI definition sent to the frontend +- [x] Register the request in the wait registry +- [x] Wait for a response or timeout + +```go +func (h *UIHandler) handleUIUpdate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var jsonData map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil { + // Return detailed JSON error response + h.sendErrorResponse(w, http.StatusBadRequest, "json_parse_error", "Invalid JSON: "+err.Error(), nil) + return + } + + // Generate a unique request ID + requestID := uuid.New().String() + + // Add the request ID to the UI definition + // This will be used by the frontend to associate actions with this request + jsonData["requestID"] = requestID + + // Convert to YAML for storage + yamlData, err := yaml.Marshal(jsonData) + if err != nil { + h.sendErrorResponse(w, http.StatusInternalServerError, "yaml_conversion_error", "Failed to convert to YAML: "+err.Error(), nil) + return + } + + // Parse into UIDefinition + var def UIDefinition + if err := yaml.Unmarshal(yamlData, &def); err != nil { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_definition_error", "Invalid UI definition: "+err.Error(), map[string]interface{}{ + "yaml": string(yamlData), + }) + return + } + + // Validate the UI definition + validationErrors := h.validateUIDefinition(def) + if len(validationErrors) > 0 { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_validation_error", "UI definition validation failed", map[string]interface{}{ + "details": validationErrors, + }) + return + } + + // Register this request in the wait registry + responseChan := h.waitRegistry.Register(requestID) + + // Create and publish refresh-ui event with the request ID + event := events.UIEvent{ + Type: "refresh-ui", + PageID: "ui-update", + Component: def, + } + + // Add the request ID to the event data if possible + if compData, ok := event.Component.(UIDefinition); ok { + compData.RequestID = requestID + event.Component = compData + } + + if err := h.events.Publish("ui-update", event); err != nil { + // Clean up the registry entry + h.waitRegistry.Cleanup(requestID) + h.sendErrorResponse(w, http.StatusInternalServerError, "event_publish_error", "Failed to publish event: "+err.Error(), nil) + return + } + + // Log that we're waiting for a response + h.logger.Debug(). + Str("requestId", requestID). + Msg("Waiting for UI action response") + + // Wait for the response or timeout + select { + case response := <-responseChan: + // Request was resolved with a user action + if response.Error != nil { + // There was an error processing the action + h.sendErrorResponse(w, http.StatusInternalServerError, "action_processing_error", response.Error.Error(), nil) + return + } + + // Log the successful response + h.logger.Info(). + Str("requestId", requestID). + Str("action", response.Action). + Str("componentId", response.ComponentID). + Msg("UI action received, completing request") + + // Return success with the action data + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "action": response.Action, + "componentId": response.ComponentID, + "data": response.Data, + }) + + case <-time.After(h.timeout): + // Request timed out + h.waitRegistry.Cleanup(requestID) + + h.logger.Warn(). + Str("requestId", requestID). + Dur("timeout", h.timeout). + Msg("Request timed out waiting for UI action") + + h.sendErrorResponse(w, http.StatusRequestTimeout, "timeout", "No user action received within the timeout period", nil) + } + } +} +``` + +### 5. Implement the UI Action Handler in UIHandler + +- [x] Move the UI action handler from Server to UIHandler +- [x] Update to check for request IDs +- [x] Resolve waiting requests when matching actions are received + +```go +func (h *UIHandler) handleUIAction() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var action struct { + ComponentID string `json:"componentId"` + Action string `json:"action"` + Data map[string]interface{} `json:"data"` + RequestID string `json:"requestId"` // Added field for request ID + } + + if err := json.NewDecoder(r.Body).Decode(&action); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + // Determine if this is an important event to log at INFO level + isImportantEvent := false + switch action.Action { + case "clicked", "changed", "submitted": + isImportantEvent = true + } + + // Log the action at appropriate level + logger := h.logger.Debug() + if isImportantEvent { + logger = h.logger.Info() + } + + // Create log entry with component and action info + logger = logger. + Str("componentId", action.ComponentID). + Str("action", action.Action) + + // Add request ID to log if present + if action.RequestID != "" { + logger = logger.Str("requestId", action.RequestID) + } + + // Add data to log if it exists and is relevant + if len(action.Data) > 0 { + // For form submissions, log the form data in detail + if action.Action == "submitted" && action.Data["formData"] != nil { + logger = logger.Interface("formData", action.Data["formData"]) + } else if action.Action == "changed" { + // For changed events, log the new value + logger = logger.Interface("data", action.Data) + } else { + // For other events, just log that data exists + logger = logger.Bool("hasData", true) + } + } + + // Output the log message + logger.Msg("UI action received") + + // Check if this action is associated with a waiting request + if action.RequestID != "" && (action.Action == "clicked" || action.Action == "submitted") { + // Try to resolve the waiting request + resolved := h.waitRegistry.Resolve(action.RequestID, UIActionResponse{ + Action: action.Action, + ComponentID: action.ComponentID, + Data: action.Data, + Error: nil, + Timestamp: time.Now(), + }) + + if resolved { + logger.Bool("waitingRequestResolved", true).Msg("Resolved waiting request") + } + } + + // Return success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + if err != nil { + http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } + } +} +``` + +### 6. Move UI Update Page Handler to UIHandler + +- [x] Move the UI update page handler from Server to UIHandler + + +### 7. Move UI Definition Validation to UIHandler + +- [x] Move the validation logic from Server to UIHandler + +```go +// validateUIDefinition checks a UI definition for common errors +func (h *UIHandler) validateUIDefinition(def UIDefinition) []map[string]interface{} { + var errors []map[string]interface{} + + // Check if components exist + if len(def.Components) == 0 { + errors = append(errors, map[string]interface{}{ + "path": "components", + "message": "No components defined", + }) + return errors + } + + // Validate each component + for i, comp := range def.Components { + for typ, props := range comp { + propsMap, ok := props.(map[string]interface{}) + if !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component properties must be a map", + }) + continue + } + + // Check for required ID + if _, hasID := propsMap["id"]; !hasID && h.requiresID(typ) { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component is missing required 'id' property", + }) + } + + // Validate nested components in forms + if typ == "form" { + // ... existing validation logic for forms + } + + // Validate list items + if typ == "list" { + // ... existing validation logic for lists + } + } + } + + return errors +} + +// requiresID returns true if the component type requires an ID +func (h *UIHandler) requiresID(componentType string) bool { + switch componentType { + case "text", "title": + // These can optionally have IDs + return false + default: + // All other components require IDs + return true + } +} +``` + +### 8. Update the UIEvent Structure + +- [x] Add RequestID field to the UIEvent struct + +```go +// In pkg/events/types.go +type UIEvent struct { + Type string `json:"type"` // e.g. "component-update", "page-reload" + PageID string `json:"pageId"` // Which page is being updated + Component interface{} `json:"component"` // The updated component data + RequestID string `json:"requestId,omitempty"` // Optional request ID for tracking user actions +} +``` + +### 9. Add Cleanup Mechanism for Orphaned Requests + +- [x] Implement a background goroutine to clean up stale requests + +```go +// Add to UIHandler initialization +func NewUIHandler(events events.EventManager, sseHandler *sse.SSEHandler, logger *zerolog.Logger) *UIHandler { + h := &UIHandler{ + waitRegistry: NewWaitRegistry(30 * time.Second), + events: events, + sseHandler: sseHandler, + logger: logger, + timeout: 30 * time.Second, + } + + // Start background cleanup for orphaned requests + go h.cleanupOrphanedRequests(context.Background()) + + return h +} + +// Add cleanup function +func (h *UIHandler) cleanupOrphanedRequests(ctx context.Context) { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Perform cleanup + count := h.waitRegistry.CleanupStale() + if count > 0 { + h.logger.Debug().Int("count", count).Msg("Cleaned up orphaned requests") + } + + case <-ctx.Done(): + return + } + } +} +``` + +### 10. Modify the Frontend JavaScript to Include Request ID + +- [x] Update the client-side code to extract and include the request ID in action events + +```javascript +// Add to the page template or UI update JavaScript +function sendUIAction(componentId, action, data = {}) { + // Get the request ID from the page if it exists + const requestId = document.body.getAttribute('data-request-id') || + document.querySelector('[data-request-id]')?.getAttribute('data-request-id') || + ''; + + logToConsole(`Component ${componentId} ${action}${requestId ? ' (request: ' + requestId + ')' : ''}`); + + // Rest of the existing function... + + // Include the request ID in the action data + fetch('/api/ui-action', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + componentId: componentId, + action: action, + data: data, + requestId: requestId // Include the request ID + }) + }) + // Rest of the existing function... +} +``` + +### 11. Update the Page Template to Include Request ID + +- [x] Modify the page template to include the request ID as a data attribute + +```go +// In pageTemplate or pageContentTemplate +func pageContentTemplate(pageID string, def UIDefinition) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + // Extract request ID if present + requestID := extractRequestID(def) + + // Include the request ID as a data attribute on the container + if requestID != "" { + fmt.Fprintf(w, `
`, requestID) + } else { + fmt.Fprintf(w, `
`) + } + + // Rest of the existing template... + + fmt.Fprintf(w, `
`) + return nil + }) +} + +// Helper function to extract request ID from UI definition +func extractRequestID(def UIDefinition) string { + // Try to extract from the component data + for _, comp := range def.Components { + for _, props := range comp { + if propsMap, ok := props.(map[string]interface{}); ok { + if rid, ok := propsMap["requestID"].(string); ok { + return rid + } + } + } + } + return "" +} +``` + +## Configuration Options + +- [ ] Make timeout configurable via environment variables or config file +- [ ] Add option to disable wait-for-response behavior for specific endpoints +- [ ] Configure logging verbosity for UI actions + +```go +type UIHandlerConfig struct { + Timeout time.Duration + CleanupInterval time.Duration + WaitForResponse bool + LogFormData bool + LogDebugEvents bool +} + +func DefaultUIHandlerConfig() UIHandlerConfig { + return UIHandlerConfig{ + Timeout: 30 * time.Second, + CleanupInterval: 5 * time.Minute, + WaitForResponse: true, + LogFormData: true, + LogDebugEvents: false, + } +} +``` + +## Testing Plan + +1. Test the basic flow: + - [ ] Send a UI update request + - [ ] Verify the UI is updated with the request ID + - [ ] Trigger a button click or form submission + - [ ] Verify the original request completes with the action data + +2. Test timeout handling: + - [ ] Send a UI update request + - [ ] Don't trigger any user action + - [ ] Verify the request times out after the configured duration + +3. Test error handling: + - [ ] Test with invalid UI definitions + - [ ] Test with network errors during event publishing + - [ ] Test with concurrent requests for the same component + +4. Test the refactored code structure: + - [ ] Verify all UI handlers are properly registered + - [ ] Verify the Server struct no longer has UI-specific logic + - [ ] Verify the UIHandler properly encapsulates all UI functionality + +## Implementation Considerations + +1. **Performance**: The wait registry should be efficient for concurrent requests +2. **Memory Management**: Ensure proper cleanup of channels and registry entries +3. **Error Handling**: Provide clear error messages for all failure scenarios +4. **Timeout Configuration**: Make the timeout configurable via server options +5. **Logging**: Add detailed logging for debugging and monitoring +6. **Code Organization**: Keep the UIHandler focused on UI concerns only +7. **Testability**: Design the code to be easily testable with mocks + +## Benefits of the Refactoring + +1. **Separation of Concerns**: UI handling logic is separated from the core server functionality +2. **Improved Maintainability**: Smaller, focused structs are easier to understand and maintain +3. **Better Testability**: UI handlers can be tested independently from the server +4. **Reduced Complexity**: The Server struct is simplified by removing UI-specific logic +5. **Extensibility**: New UI features can be added to the UIHandler without modifying the Server + +## Conclusion + +This implementation will create a synchronous flow between UI updates and user actions, allowing the server to wait for user interaction before completing the request. The use of channels and a wait registry provides a clean way to handle the asynchronous nature of user interactions while maintaining a synchronous API for the caller. + +Additionally, the refactoring to move UI handlers into their own struct will significantly improve the code organization and maintainability of the codebase. \ No newline at end of file diff --git a/ttmp/2025-02-22/changelog.md b/ttmp/2025-02-22/changelog.md index 249d53b..443e8c9 100644 --- a/ttmp/2025-02-22/changelog.md +++ b/ttmp/2025-02-22/changelog.md @@ -28,4 +28,17 @@ Created comprehensive documentation to help new developers understand the SSE an - Explained how templates are selected and rendered - Described how page data is managed and passed through the system - Included code examples and a complete walkthrough of the update process -- Created `ttmp/2025-02-22/03-sse-page-handling.md` as a developer guide \ No newline at end of file +- Created `ttmp/2025-02-22/03-sse-page-handling.md` as a developer guide + +# UI Action Handler with Wait-for-Response + +Added a new UI handler implementation that waits for user actions before completing requests. This creates a synchronous flow between UI updates and user interactions, making it easier to build interactive applications. + +- Created a new `UIHandler` struct to encapsulate all UI-related functionality +- Implemented a `WaitRegistry` to track pending requests using channels +- Added request ID generation and tracking between UI updates and actions +- Implemented timeout handling for requests without responses +- Integrated the UIHandler with the main server by removing UI-specific handlers from the Server struct +- Updated the Server initialization to create and use the UIHandler +- Added RequestID field to the UIEvent struct for better tracking of user interactions +- Implemented background cleanup for orphaned requests \ No newline at end of file From d1cadf66d395585d6c7143fbd880f30300b757a6 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 26 Feb 2025 19:54:21 -0500 Subject: [PATCH 2/3] :lipstick: :sparkles: Polish the UI handling --- changelog.md | 10 + cmd/ui-server/server.go | 15 +- cmd/ui-server/templates.templ | 91 +++-- cmd/ui-server/templates_templ.go | 573 ++++++++++++++++-------------- cmd/ui-server/templates_templ.txt | 49 +-- examples/ui/update-ui.yaml | 12 + pkg/server/ui/handler.go | 147 +++++++- pkg/server/ui/registry.go | 20 +- ttmp/2025-02-21/changelog.md | 10 +- ttmp/2025-02-22/changelog.md | 33 +- 10 files changed, 612 insertions(+), 348 deletions(-) diff --git a/changelog.md b/changelog.md index f4ea4eb..3d95836 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,16 @@ Added structured metadata to technical documentation for better maintainability: - Structured questions the document answers for better discoverability - Improved documentation organization and searchability +# UI Action Resolved Status and Auto-Reset + +Enhanced the UI action handling to return the resolved status in the response and automatically reset the UI when an action resolves a waiting request: +- Modified the handleUIAction function to include the resolved status in the response +- Updated the sendUIAction JavaScript function to handle resolved actions +- Added automatic form reset after a successful form submission that resolves a request +- Implemented UI reset to show a waiting message after a resolved action +- Added YAML source display clearing when an action resolves a request +- This improves the user experience by providing immediate feedback and resetting the UI state + # UI Action Handling Documentation Added comprehensive documentation for the UI action handling system: diff --git a/cmd/ui-server/server.go b/cmd/ui-server/server.go index c663e72..8a4dbe7 100644 --- a/cmd/ui-server/server.go +++ b/cmd/ui-server/server.go @@ -93,6 +93,9 @@ func NewServer(dir string) (*Server, error) { // Register UI handlers uiHandler.RegisterHandlers(s.mux) + // Set up UI update page + s.mux.HandleFunc("/ui", s.handleUIUpdatePage()) + // Set up dynamic page handler - must come before index handler s.mux.Handle("/pages/", s.handleAllPages()) @@ -192,8 +195,8 @@ func (s *Server) loadPages() error { if strings.HasSuffix(d.Name(), ".yaml") { log.Debug().Str("path", path).Msg("Found YAML page") if err := s.loadPage(path); err != nil { - log.Error().Err(err).Str("path", path).Msg("Failed to load page") - return err + log.Warn().Err(err).Str("path", path).Msg("Failed to load page") + return nil } } return nil @@ -367,3 +370,11 @@ func (s *Server) registerComponentRenderers() { return buf.String(), nil }) } + +// handleUIUpdatePage renders the UI update page +func (s *Server) handleUIUpdatePage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + component := uiUpdateTemplate() + _ = component.Render(r.Context(), w) + } +} diff --git a/cmd/ui-server/templates.templ b/cmd/ui-server/templates.templ index 21586fd..825fb56 100644 --- a/cmd/ui-server/templates.templ +++ b/cmd/ui-server/templates.templ @@ -48,6 +48,17 @@ templ base(title string) { } } + function sendUIActionPreventDefault(event, componentId, action, data = {}) { + // Prevent the default form submission + event.preventDefault(); + + // Call the regular sendUIAction function + sendUIAction(componentId, action, data); + + // Return false to ensure the form doesn't submit + return false; + } + function sendUIAction(componentId, action, data = {}) { // Get the request ID from the page if it exists const requestId = document.querySelector('[data-request-id]')?.getAttribute('data-request-id') || ''; @@ -105,6 +116,44 @@ templ base(title string) { .then(response => response.json()) .then(data => { console.log('Action response:', data); + + // If the action resolved a waiting request, handle it + if (data.resolved) { + logToConsole(`Action resolved a waiting request`); + + // If this was a form submission, reset the form + if (action === 'submitted' && document.getElementById(componentId)) { + const form = document.getElementById(componentId); + form.reset(); + + // Clear any custom state + form._lastClickedButton = null; + } + + // Find the card body containing the UI components + const cardBody = document.querySelector('.card-body[data-request-id]'); + if (cardBody) { + // Clear the request ID + cardBody.setAttribute('data-request-id', ''); + + // Show waiting message + cardBody.innerHTML = '
Waiting for UI updates...
'; + logToConsole('UI reset to waiting state'); + } + + // Remove the YAML source display section completely + const yamlColumn = document.querySelector('.col-md-6:nth-child(2)'); + if (yamlColumn) { + yamlColumn.remove(); + logToConsole('Removed YAML source display'); + } + + // Make the UI column take full width + const uiColumn = document.querySelector('.col-md-6'); + if (uiColumn) { + uiColumn.className = 'col-12'; + } + } }) .catch(error => { console.error('Error sending action:', error); @@ -222,7 +271,9 @@ templ pageTemplate(name string, def ui.UIDefinition) { } templ pageContentTemplate(name string, def ui.UIDefinition) { -
+
@@ -248,17 +299,6 @@ templ pageContentTemplate(name string, def ui.UIDefinition) {
- } templ renderComponent(typ string, props map[string]interface{}) { @@ -268,7 +308,7 @@ templ renderComponent(typ string, props map[string]interface{}) {
-
-

+

-
-

+

@@ -81,8 +73,7 @@ \">
-
+

    @@ -100,4 +91,4 @@ \" onsubmit=\" \" class=\"needs-validation\" novalidate>
-
Dynamic UI
Waiting for UI updates...
+
Dynamic UI
Waiting for UI updates...
diff --git a/examples/ui/update-ui.yaml b/examples/ui/update-ui.yaml index a2f1060..c108c91 100644 --- a/examples/ui/update-ui.yaml +++ b/examples/ui/update-ui.yaml @@ -45,7 +45,7 @@ long: | All components support these common properties: - - `id`: Unique identifier for the component (required) + - `id`: Unique identifier for the component (required for all components EXCEPT title, list, and text) - `disabled`: Boolean to disable the component (optional) - `data`: Map of data attributes (optional) @@ -228,16 +228,77 @@ long: | ## Best Practices - 1. Always provide meaningful IDs for components that need to be referenced - 2. Use semantic naming for form fields - 3. Group related components inside forms - 4. Use appropriate button types for different actions: + 1. Always provide IDs for all components (except title and text where they're optional) + 2. Use meaningful, descriptive IDs for components that need to be referenced + 3. Use semantic naming for form fields + 4. Group related components inside forms + 5. Use appropriate button types for different actions: - `primary`: Main actions - `secondary`: Alternative actions - `danger`: Destructive actions - `success`: Confirmation actions - 5. Provide clear labels and placeholders for form inputs + 6. Provide clear labels and placeholders for form inputs + ## Form Submission Model + + The UI system operates on a single form submission model with the following characteristics: + + 1. **Single Response**: Each UI update waits for a single user action (click or form submission) before responding. + 2. **No Intermediate State Updates**: Buttons outside of forms can trigger actions, but buttons cannot update state before a form submission. + 3. **Click-Submit Sequence**: The system has a brief delay after click events to detect if a form submission follows, but this is only to capture the submission as the primary response. + 4. **Form-Centric Design**: For complex interactions requiring multiple steps, design your UI to use separate form submissions rather than trying to update state between steps. + + ### Example: What Not To Do + + ```yaml + # This won't work as expected - the "Calculate" button cannot update the total before form submission + - form: + id: order-form + components: + - input: + id: quantity + type: number + value: 1 + - input: + id: price + type: number + value: 10.00 + - button: + id: calculate-btn + text: "Calculate Total" # This won't update the total field before submission + type: secondary + - input: + id: total + type: number + value: 0 + - button: + id: submit-order-btn + text: "Submit Order" + type: primary + ``` + + ### Example: Correct Approach + + ```yaml + # Better approach - separate the calculation as its own form submission + - form: + id: calculate-form + components: + - input: + id: quantity + type: number + value: 1 + - input: + id: price + type: number + value: 10.00 + - button: + id: calculate-btn + text: "Calculate Total" + type: primary + + # The result would be shown in a separate UI update after the calculation + ``` flags: - name: components @@ -253,10 +314,11 @@ flags: help: Show verbose output default: false +capture-stderr: true shell-script: | #!/bin/bash set -euo pipefail - + # Log the operation if verbose is enabled if [[ "{{ .Args.verbose }}" == "true" ]]; then echo "Updating UI components..." @@ -268,9 +330,9 @@ shell-script: | JSON_FILE=$(mktemp) RESPONSE_FILE=$(mktemp) trap 'rm -f "$YAML_FILE" "$JSON_FILE" "$RESPONSE_FILE"' EXIT - + # Write the YAML components to the temporary file - cat < "$YAML_FILE" + cat <<'EOF' > "$YAML_FILE" {{ .Args.components }} EOF @@ -314,7 +376,6 @@ shell-script: | cat "$YAML_FILE" | sed 's/: /": "/g' | sed 's/$/"/g' | sed 's/^/"/g' > "$JSON_FILE" echo "⚠️ Warning: Using basic YAML to JSON conversion. Install python3 with PyYAML or yq for better results." fi - echo "FOOBAOOAR" # Send the components to the UI server and capture the response if [[ "{{ .Args.verbose }}" == "true" ]]; then diff --git a/pkg/cmds/cmd.go b/pkg/cmds/cmd.go index 438f3c3..d4c3d40 100644 --- a/pkg/cmds/cmd.go +++ b/pkg/cmds/cmd.go @@ -7,6 +7,7 @@ import ( "io" "os" "os/exec" + "time" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/layers" @@ -139,6 +140,7 @@ func (c *ShellCommand) ExecuteCommand( // Process script template script, err := c.processTemplate(c.ShellScript, args) if err != nil { + log.Error().Err(err).Str("shell_script", c.ShellScript).Msg("failed to process shell script template") return errors.Wrap(err, "failed to process shell script template") } @@ -149,22 +151,31 @@ func (c *ShellCommand) ExecuteCommand( // Create temporary script file tmpFile, err := os.CreateTemp("", "shell-*.sh") if err != nil { + log.Error().Err(err).Msg("failed to create temporary script file") return errors.Wrap(err, "failed to create temporary script file") } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(script); err != nil { + log.Error().Err(err).Msg("failed to write script to temporary file") return errors.Wrap(err, "failed to write script to temporary file") } if err := tmpFile.Close(); err != nil { + log.Error().Err(err).Msg("failed to close temporary file") return errors.Wrap(err, "failed to close temporary file") } // Make the script executable if err := os.Chmod(tmpFile.Name(), 0755); err != nil { + log.Error().Err(err).Msg("failed to make script executable") return errors.Wrap(err, "failed to make script executable") } + // Copy script to debug file with timestamp + debugFile := fmt.Sprintf("/tmp/debug-%s.sh", time.Now().Format("20060102-150405")) + if err := os.WriteFile(debugFile, []byte(script), 0644); err != nil { + log.Warn().Err(err).Str("debug_file", debugFile).Msg("failed to write debug script file") + } cmd = exec.CommandContext(ctx, "bash", tmpFile.Name()) } else { // Process command template @@ -172,6 +183,7 @@ func (c *ShellCommand) ExecuteCommand( for i, arg := range c.Command { processed, err := c.processTemplate(arg, args) if err != nil { + log.Error().Err(err).Str("command_argument", arg).Msg("failed to process command argument template") return errors.Wrapf(err, "failed to process command argument template: %s", arg) } processedArgs[i] = processed @@ -191,6 +203,7 @@ func (c *ShellCommand) ExecuteCommand( for k, v := range c.Environment { processed, err := c.processTemplate(v, args) if err != nil { + log.Error().Err(err).Str("environment_variable", k).Msg("failed to process environment variable template") return errors.Wrapf(err, "failed to process environment variable template: %s", k) } env = append(env, fmt.Sprintf("%s=%s", k, processed)) @@ -206,6 +219,8 @@ func (c *ShellCommand) ExecuteCommand( cmd.Stderr = os.Stderr } + log.Info().Str("command", fmt.Sprintf("%v", cmd.Args)).Msg("executing command") + return cmd.Run() } diff --git a/pkg/doc/topics/02-shell-commands.md b/pkg/doc/topics/02-shell-commands.md index 1439592..d24eade 100644 --- a/pkg/doc/topics/02-shell-commands.md +++ b/pkg/doc/topics/02-shell-commands.md @@ -463,6 +463,15 @@ flags: ``` Solution: Install required dependencies or add error checking. +4. **Expansion errors in heredoc strings** + ``` + Error: $1: unbound variable + ``` + Solution: Use single quotes for heredoc strings. + cat <<'EOF' > FILE + {{ .data }} + EOF + ### Debugging Tips 1. Enable verbose output: diff --git a/pkg/doc/topics/03-mcp-in-practice.md b/pkg/doc/topics/03-mcp-in-practice.md index dde4f76..dc8d8a4 100644 --- a/pkg/doc/topics/03-mcp-in-practice.md +++ b/pkg/doc/topics/03-mcp-in-practice.md @@ -280,7 +280,7 @@ shell-script: | set -euo pipefail # Format the event data - EVENT_DATA=$(cat << EOF + EVENT_DATA=$(cat << 'EOF' { "summary": "{{ .Args.title }}", "start": { diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 165a41c..d928712 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -278,16 +278,22 @@ func (h *RequestHandler) handleToolsList(ctx context.Context, req *protocol.Requ } func (h *RequestHandler) handleToolsCall(ctx context.Context, req *protocol.Request) (*protocol.Response, error) { + h.server.logger.Info().Str("method", req.Method).Str("params", string(req.Params)).Msg("handleToolsCall") + var params struct { Name string `json:"name"` Arguments map[string]interface{} `json:"arguments"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { + h.server.logger.Error().Err(err).Str("params", string(req.Params)).Msg("failed to unmarshal tool call arguments") return nil, transport.NewInvalidParamsError(err.Error()) } + h.server.logger.Info().Str("name", params.Name).Str("arguments", fmt.Sprintf("%v", params.Arguments)).Msg("calling tool") + result, err := h.server.toolProvider.CallTool(ctx, params.Name, params.Arguments) if err != nil { + h.server.logger.Error().Err(err).Str("name", params.Name).Str("arguments", fmt.Sprintf("%v", params.Arguments)).Msg("failed to call tool") return nil, transport.NewInternalError(err.Error()) } diff --git a/pkg/server/ui/handler.go b/pkg/server/ui/handler.go index bf7b907..b358a4e 100644 --- a/pkg/server/ui/handler.go +++ b/pkg/server/ui/handler.go @@ -476,10 +476,13 @@ func (h *UIHandler) validateUIDefinition(def UIDefinition) []map[string]interfac if items, hasItems := propsMap["items"]; hasItems { if itemsList, ok := items.([]interface{}); ok { for j, item := range itemsList { - if _, ok := item.(map[string]interface{}); !ok { + switch item.(type) { + case string, map[string]interface{}: + // These types are allowed + default: errors = append(errors, map[string]interface{}{ "path": fmt.Sprintf("components[%d].%s.items[%d]", i, typ, j), - "message": "List item must be a map", + "message": "List item must be a string or a map", }) } } @@ -500,7 +503,7 @@ func (h *UIHandler) validateUIDefinition(def UIDefinition) []map[string]interfac // requiresID returns true if the component type requires an ID func (h *UIHandler) requiresID(componentType string) bool { switch componentType { - case "text", "title": + case "text", "title", "list": // These can optionally have IDs return false default: diff --git a/ttmp/2025-02-22/changelog.md b/ttmp/2025-02-22/changelog.md index e9a6fd7..03e2196 100644 --- a/ttmp/2025-02-22/changelog.md +++ b/ttmp/2025-02-22/changelog.md @@ -17,8 +17,7 @@ Enhanced the UI action handling to return the resolved status in the response an - Modified the handleUIAction function to include the resolved status in the response - Updated the sendUIAction JavaScript function to handle resolved actions - Added automatic form reset after a successful form submission that resolves a request -- Implemented UI reset to show a waiting message after a resolved action -- Added YAML source display clearing when an action resolves a request +- Replaced the entire UI with a simplified waiting message after a resolved action - This improves the user experience by providing immediate feedback and resetting the UI state # Enhanced Page Reload Events with Page Definitions @@ -72,4 +71,15 @@ Improved the UI action response to include related events, allowing clients to r - Created a new `UIActionEvent` type to represent individual events - Modified the click-submit delay logic to store click events as related events when a submission follows - Updated the response JSON format to include the related events array -- This enhancement provides clients with a complete history of user interactions leading to the final action \ No newline at end of file +- This enhancement provides clients with a complete history of user interactions leading to the final action + +# Clarified UI DSL Documentation on Form Submission Model + +Updated the UI DSL documentation to clearly explain the single form submission model and its limitations. + +- Added a new "Form Submission Model" section to the documentation +- Clarified that the UI system only supports a single response per UI update +- Explained that buttons cannot update state before form submission +- Added examples of incorrect and correct approaches to multi-step interactions +- Provided guidance on designing UIs with separate form submissions for complex interactions +- This helps developers avoid common pitfalls when designing UI components \ No newline at end of file